支付包第一期完成

This commit is contained in:
tangxinyue 2026-01-15 17:13:34 +08:00
parent 0439d91974
commit b90e9cb22e
23 changed files with 1875 additions and 362 deletions

63
App.vue
View File

@ -10,9 +10,6 @@ export default {
// 1. // 1.
this.initConfig(options) this.initConfig(options)
// 2.
this.fetchUserDataAsync()
// //
console.log(`App 启动耗时: ${Date.now() - startTime}ms`) console.log(`App 启动耗时: ${Date.now() - startTime}ms`)
}, },
@ -119,66 +116,6 @@ export default {
// #endif // #endif
} }
}) })
},
/**
* 异步获取用户数据不阻塞启动
*/
fetchUserDataAsync() {
//
setTimeout(async () => {
const fetchStart = Date.now()
try {
//
const [userResult, configResult] = await Promise.allSettled([
this.fetchUserInfo(),
this.fetchUserConfig()
])
//
if (userResult.status === 'rejected') {
console.error('获取用户信息失败:', userResult.reason)
}
//
if (configResult.status === 'rejected') {
console.error('获取用户配置失败:', configResult.reason)
}
console.log(`用户数据获取耗时: ${Date.now() - fetchStart}ms`)
} catch (error) {
console.error('获取用户数据异常:', error)
//
}
}, 0)
},
/**
* 获取用户信息
*/
async fetchUserInfo() {
const data = await get('', 'api/user', {})
if (data.code === 0) {
uni.setStorageSync('userInfo', data.data)
console.log('用户信息获取成功')
return data.data
} else {
throw new Error(data.message || '获取用户信息失败')
}
},
/**
* 获取用户配置
*/
async fetchUserConfig() {
const data = await get('', 'api/user/config', {})
if (data.code === 0) {
uni.setStorageSync('config', data.data)
console.log('用户配置获取成功')
return data.data
} else {
throw new Error(data.message || '获取用户配置失败')
}
} }
} }
} }

View File

@ -0,0 +1,293 @@
<template>
<view>
<!-- 底部弹出层 -->
<uni-popup ref="timepopup" type="bottom">
<!-- 账单分类 -->
<view v-if="data.popupType == 'billClassify'" class="bill-classify-box">
<view class="title-box">
<view class="title">
账单分类
</view>
<view class="btn" @click="closeTimePicker">
<image class="close-image" src="/static/image/common/close.png"></image>
</view>
</view>
<view class="bill-classify-content">
<view class="bill-classify-item" v-for="item in billClassifyOptions" :key="item.id"
@click="data.currentClassifyName = item.name">
<view class="bill-classify-item-text"
:class="{ 'active-item': data.currentClassifyName == item.name }">
{{ item.name }}
</view>
</view>
</view>
<view class="confirm-btn" @click="confirmClassify">
确定
</view>
</view>
<!-- 标签和备注 -->
<view v-if="data.popupType == 'tagAndNote'" class="bill-classify-box">
<view class="title-box">
<view class="title">
标签
<text class="text">(最多10个标签)</text>
</view>
<view class="btn" @click="closeTimePicker">
<image class="close-image" src="/static/image/common/close.png"></image>
</view>
</view>
<view class="bill-classify-content tag-content flex-wrap">
<view class="tag-item" v-for="(tag, index) in data.tempTags" :key="index">
<text>{{ tag }}</text>
<view class="delete-tag" @click="deleteTag(index)">
<image src="/static/image/common/close.png" mode="widthFix" class="close-icon-img">
</image>
</view>
</view>
<view class="add-tag-box" v-if="!data.showTagInput && data.tempTags.length < 10"
@click="showTagInputFunc">
<text class="add-symbol">+</text>
</view>
<view class="input-box" v-if="data.showTagInput">
<input class="tag-input-field" v-model="data.tagInputValue" :focus="data.showTagInput"
@confirm="confirmAddTag" @blur="confirmAddTag" placeholder="请输入标签" maxlength="5" />
</view>
</view>
<view class="title-box">
<view class="title">
备注
</view>
</view>
<view class="bill-classify-content note-content">
<textarea class="note-textarea" auto-height v-model="data.tempNote.text" placeholder="随便说点什么..."
maxlength="50" disable-default-padding></textarea>
</view>
<view class="title-box">
<view class="title">
备注图片
</view>
<switch :checked="data.tempNote.isImage" color="#1676FE" style="transform: scale(0.7);"
@change="(e) => data.tempNote.isImage = e.detail.value"></switch>
</view>
<!-- <view class="bill-classify-content" v-if="data.tempNote.isImage"> -->
<!-- 此处展示图片上传或其他逻辑暂时留空或添加提示 -->
<!-- </view> -->
<view class="confirm-btn" @click="confirmClassify">
确定
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
reactive,
toRefs,
ref,
defineExpose,
defineProps,
defineEmits
} from 'vue'
import addBillJson from '@/static/json/add-bill.json'
const props = defineProps({
billClassifyOptions: {
type: Array,
default: () => addBillJson.billClassify
}
})
const emit = defineEmits(['confirm', 'update:show'])
const timepopup = ref(null)
const data = reactive({
popupType: '', // 'billClassify' or 'tagAndNote'
currentClassifyName: '',
tempTags: [],
tagInputValue: '',
showTagInput: false,
tempNote: {
text: '',
isImage: false
}
})
/**
* 打开弹窗
* @param {String} type 弹窗类型 'billClassify' | 'tagAndNote'
* @param {Object} initData 初始化数据
*/
const open = (type, initData = {}) => {
data.popupType = type
if (type === 'billClassify') {
data.currentClassifyName = initData.classifyName || ''
} else if (type === 'tagAndNote') {
// Deep copy to prevent direct mutation of prop data before confirm
data.tempTags = initData.tags ? JSON.parse(JSON.stringify(initData.tags)) : []
data.tempNote = initData.note ? JSON.parse(JSON.stringify(initData.note)) : { text: '', isImage: false }
data.showTagInput = false
data.tagInputValue = ''
}
timepopup.value.open()
}
/**
* 关闭弹窗
*/
const closeTimePicker = () => {
timepopup.value.close()
}
/**
* 确认操作
*/
const confirmClassify = () => {
if (data.popupType === 'billClassify') {
const selected = props.billClassifyOptions.find(item => item.name === data.currentClassifyName)
emit('confirm', { type: 'billClassify', value: data.currentClassifyName, id: selected ? selected.id : 0 })
} else if (data.popupType === 'tagAndNote') {
emit('confirm', {
type: 'tagAndNote',
tags: data.tempTags,
note: data.tempNote
})
}
closeTimePicker()
}
/**
* 删除标签
*/
const deleteTag = (index) => {
data.tempTags.splice(index, 1)
}
/**
* 显示标签输入框
*/
const showTagInputFunc = () => {
data.showTagInput = true
}
/**
* 确认添加标签
*/
const confirmAddTag = () => {
if (data.tagInputValue.trim()) {
data.tempTags.push(data.tagInputValue.trim())
}
data.tagInputValue = ''
data.showTagInput = false
}
defineExpose({
open,
close: closeTimePicker
})
</script>
<style lang="less" scoped>
@import "@/common/specify-style.less";
.bill-classify-box {
.tag-content {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 20rpx 16rpx 0 16rpx;
margin-bottom: 24rpx;
.tag-item {
position: relative;
background-color: #E6F2FF;
color: #2788D1;
font-size: 26rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0 24rpx;
border-radius: 8rpx;
margin-right: 24rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
.delete-tag {
position: absolute;
top: -12rpx;
right: -12rpx;
width: 32rpx;
height: 32rpx;
background-color: #FE4141;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
border: 2rpx solid #ffffff;
z-index: 10;
.close-icon-img {
width: 14rpx;
height: 14rpx;
filter: brightness(0) invert(1);
}
}
}
.add-tag-box {
height: 52rpx;
line-height: 52rpx;
background-color: #F4F9FF;
border-radius: 8rpx;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 24rpx;
padding: 0 16rpx;
.add-symbol {
color: #2788D1;
font-size: 40rpx;
line-height: 52rpx;
font-weight: 300;
}
}
.input-box {
width: 160rpx;
height: 60rpx;
background-color: #F4F9FF;
border-radius: 8rpx;
margin-bottom: 24rpx;
padding: 0 16rpx;
display: flex;
align-items: center;
.tag-input-field {
width: 100%;
font-size: 26rpx;
color: #2788D1;
}
}
}
.note-content {
border-bottom: 1rpx solid #EDEDED;
margin-bottom: 24rpx;
.note-textarea {
width: 100%;
height: 160rpx;
// background-color: #F9F9F9;
font-size: 28rpx;
color: #333;
}
}
}
.flex-wrap {
flex-wrap: wrap;
}
</style>

View File

@ -50,6 +50,20 @@
"navigationBarTitleText": "账单详情页", "navigationBarTitleText": "账单详情页",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/index/alipay-annual-bill/alipay-annual-bill",
"style": {
"navigationBarTitleText": "支付宝年度账单",
"navigationStyle": "custom"
}
},
{
"path": "pages/common/webview/webview",
"style": {
"navigationBarTitleText": "webView页面",
"navigationStyle": "custom"
}
} }
], ],
"globalStyle": { "globalStyle": {
@ -61,6 +75,9 @@
"navigationBarBackgroundColor": "#00000000", // "navigationBarBackgroundColor": "#00000000", //
"navigationBarTextStyle": "white", // "navigationBarTextStyle": "white", //
"backgroundColor": "#00000000" // "backgroundColor": "#00000000" //
},
"app-plus": {
"bounce": "none" //
} }
}, },
"uniIdRouter": {} "uniIdRouter": {}

View File

@ -238,6 +238,8 @@
</view> </view>
<!-- 底部弹出层 --> <!-- 底部弹出层 -->
<billManagementPopup ref="billManagementPopupRef" @confirm="handleBillManagementConfirm"></billManagementPopup>
<uni-popup ref="timepopup" type="bottom"> <uni-popup ref="timepopup" type="bottom">
<!-- 时间选择器 --> <!-- 时间选择器 -->
<view v-if="selectItemInfo.type == 'time'" class="timeBox"> <view v-if="selectItemInfo.type == 'time'" class="timeBox">
@ -252,82 +254,6 @@
<DateTimePicker :defaultDate="datePickerData.selectDate" :minDate="datePickerData.startDate" <DateTimePicker :defaultDate="datePickerData.selectDate" :minDate="datePickerData.startDate"
:maxDate="datePickerData.endDate" :mode="4" @onChange="onChangeStartDate" /> :maxDate="datePickerData.endDate" :mode="4" @onChange="onChangeStartDate" />
</view> </view>
<template v-else>
<!-- 账单分类 -->
<view v-if="data.popupType == 'billClassify'" class="bill-classify-box">
<view class="title-box">
<view class="title">
账单分类
</view>
<view class="btn" @click="closeTimePicker">
<image class="close-image" src="/static/image/common/close.png"></image>
</view>
</view>
<view class="bill-classify-content">
<view class="bill-classify-item" v-for="item in billClassifyOptions" :key="item.id"
@click="data.currentClassifyId = item.id">
<view class="bill-classify-item-text"
:class="{ 'active-item': data.currentClassifyId == item.id }">
{{ item.name }}
</view>
</view>
</view>
<view class="confirm-btn" @click="confirmClassify">
确定
</view>
</view>
<!-- 标签和备注 -->
<view v-if="data.popupType == 'tagAndNote'" class="bill-classify-box">
<view class="title-box">
<view class="title">
标签
<text class="text">(最多10个标签)</text>
</view>
<view class="btn" @click="closeTimePicker">
<image class="close-image" src="/static/image/common/close.png"></image>
</view>
</view>
<view class="bill-classify-content tag-content flex-wrap">
<view class="tag-item" v-for="(tag, index) in data.tempTags" :key="index">
<text>{{ tag }}</text>
<view class="delete-tag" @click="deleteTag(index)">
<image src="/static/image/common/close.png" mode="widthFix" class="close-icon-img"></image>
</view>
</view>
<view class="add-tag-box" v-if="!data.showTagInput && data.tempTags.length < 10"
@click="showTagInputFunc">
<text class="add-symbol">+</text>
</view>
<view class="input-box" v-if="data.showTagInput">
<input class="tag-input-field" v-model="data.tagInputValue" :focus="data.showTagInput"
@confirm="confirmAddTag" @blur="confirmAddTag" placeholder="请输入标签" maxlength="5" />
</view>
</view>
<view class="title-box">
<view class="title">
备注
</view>
</view>
<view class="bill-classify-content note-content">
<textarea class="note-textarea" auto-height v-model="data.tempNote.text" placeholder="随便说点什么..."
maxlength="50" disable-default-padding></textarea>
</view>
<view class="title-box">
<view class="title">
备注图片
</view>
<switch :checked="data.tempNote.isImage" color="#1676FE" style="transform: scale(0.7);"
@change="(e) => data.tempNote.isImage = e.detail.value"></switch>
</view>
<!-- <view class="bill-classify-content" v-if="data.tempNote.isImage"> -->
<!-- 此处展示图片上传或其他逻辑暂时留空或添加提示 -->
<!-- </view> -->
<view class="confirm-btn" @click="confirmClassify">
确定
</view>
</view>
</template>
</uni-popup> </uni-popup>
</template> </template>
@ -335,9 +261,9 @@
import navBar from '@/components/nav-bar/nav-bar.vue' import navBar from '@/components/nav-bar/nav-bar.vue'
import addBillJson from '@/static/json/add-bill.json' import addBillJson from '@/static/json/add-bill.json'
import DateTimePicker from '@/components/dengrq-datetime-picker/dateTimePicker/index.vue'; import DateTimePicker from '@/components/dengrq-datetime-picker/dateTimePicker/index.vue';
import billManagementPopup from '@/components/bill-management-popup/bill-management-popup.vue'
import { stringUtil, randomUtil, util, uiUtil } from '@/utils/common.js'; import { stringUtil, randomUtil, util, uiUtil } from '@/utils/common.js';
import hotIcon from "@/static/json/hot-icon.json" import hotIcon from "@/static/json/hot-icon.json"
import { storage } from '@/utils/storage.js'
import { useStore } from '@/store/index.js' import { useStore } from '@/store/index.js'
import { import {
@ -357,6 +283,8 @@ import {
// //
const timepopup = ref(null) const timepopup = ref(null)
//
const billManagementPopupRef = ref(null)
const { const {
addBill, addBill,
@ -884,9 +812,11 @@ const onClickItemInfo = async (item, action) => {
selectItemInfo.value = item selectItemInfo.value = item
timepopup.value.open() timepopup.value.open()
} else if (item.key == 'billClassify') { } else if (item.key == 'billClassify') {
// selectItemInfo.value = item // Use bill-management-popup component
data.popupType = "billClassify" const initData = {
timepopup.value.open() classifyName: billData.value.merchantOption.billClassify
}
billManagementPopupRef.value.open('billClassify', initData)
} else if (item.type == 'text' || item.type == 'number' || item.type == "digit") { } else if (item.type == 'text' || item.type == 'number' || item.type == "digit") {
item.focus = false item.focus = false
nextTick(() => { nextTick(() => {
@ -902,10 +832,12 @@ const onClickItemInfo = async (item, action) => {
}) })
} }
} else if (item.key == 'tag' || item.key == 'note') { } else if (item.key == 'tag' || item.key == 'note') {
data.tempTags = JSON.parse(JSON.stringify(billData.value.merchantOption.tag)) // Use bill-management-popup component
data.tempNote = JSON.parse(JSON.stringify(billData.value.merchantOption.note)) const initData = {
data.popupType = "tagAndNote" tags: billData.value.merchantOption.tag,
timepopup.value.open() note: billData.value.merchantOption.note
}
billManagementPopupRef.value.open('tagAndNote', initData)
} }
} }
@ -1030,46 +962,6 @@ const settmes = () => {
data.popupType = '' data.popupType = ''
} }
//
const deleteTag = (index) => {
data.tempTags.splice(index, 1)
}
//
const showTagInputFunc = () => {
data.showTagInput = true
data.tagInputValue = ""
}
//
const confirmAddTag = () => {
if (data.tagInputValue && data.tagInputValue.trim()) {
data.tempTags.push(data.tagInputValue.trim())
}
data.showTagInput = false
data.tagInputValue = ""
}
/**
* 设置分类选择
*/
const confirmClassify = () => {
if (data.popupType == 'billClassify') {
billClassifyOptions.value.forEach(option => {
if (option.id == data.currentClassifyId) {
billData.value.merchantOption.billClassify = option.name
}
})
} else if (data.popupType == 'tagAndNote') {
billData.value.merchantOption.tag = JSON.parse(JSON.stringify(data.tempTags))
billData.value.merchantOption.note = JSON.parse(JSON.stringify(data.tempNote))
}
timepopup.value.close()
data.popupType = ''
}
/** /**
* @param {Object} date * @param {Object} date
* 切换时间 * 切换时间
@ -1077,6 +969,20 @@ const confirmClassify = () => {
function onChangeStartDate(date) { function onChangeStartDate(date) {
datePickerData.value.selectDate = date; datePickerData.value.selectDate = date;
} }
/**
* 确认账单管理弹窗
* @param data
*/
const handleBillManagementConfirm = (data) => {
console.log("确认账单管理弹窗", data)
if (data.type == 'billClassify') {
billData.value.merchantOption.billClassify = data.value
} else if (data.type == 'tagAndNote') {
billData.value.merchantOption.tag = data.tags
billData.value.merchantOption.note = data.note
}
}
</script> </script>
<style lang="less"> <style lang="less">
@ -1472,98 +1378,4 @@ page {
} }
.tag-content {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 20rpx 16rpx 0 16rpx;
margin-bottom: 24rpx;
.tag-item {
position: relative;
background-color: #E6F2FF;
color: #2788D1;
font-size: 26rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0 24rpx;
border-radius: 8rpx;
margin-right: 24rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
.delete-tag {
position: absolute;
top: -12rpx;
right: -12rpx;
width: 32rpx;
height: 32rpx;
background-color: #FE4141;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
border: 2rpx solid #ffffff;
z-index: 10;
.close-icon-img {
width: 14rpx;
height: 14rpx;
filter: brightness(0) invert(1);
}
}
}
.add-tag-box {
height: 52rpx;
line-height: 52rpx;
background-color: #F4F9FF;
border-radius: 8rpx;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 24rpx;
padding: 0 16rpx;
.add-symbol {
color: #2788D1;
font-size: 40rpx;
line-height: 52rpx;
font-weight: 300;
}
}
.input-box {
width: 160rpx;
height: 60rpx;
background-color: #F4F9FF;
border-radius: 8rpx;
margin-bottom: 24rpx;
padding: 0 16rpx;
display: flex;
align-items: center;
.tag-input-field {
width: 100%;
font-size: 26rpx;
color: #2788D1;
}
}
}
.note-content {
border-bottom: 1rpx solid #EDEDED;
margin-bottom: 24rpx;
.note-textarea {
width: 100%;
height: 160rpx;
// background-color: #F9F9F9;
font-size: 28rpx;
color: #333;
}
}
</style> </style>

View File

@ -154,7 +154,7 @@
</view> </view>
<view class="bill-classification bill-management-item"> <view class="bill-classification bill-management-item">
<text class="label">账单分类</text> <text class="label">账单分类</text>
<view class="right-box flex-align-center"> <view class="right-box flex-align-center" @click="handleBillManagementClick('billClassify')">
<text>{{ billData.merchantOption.billClassify }}</text> <text>{{ billData.merchantOption.billClassify }}</text>
<uni-icons style="margin-left: 8rpx;" type="right" size="12" color="#969696"></uni-icons> <uni-icons style="margin-left: 8rpx;" type="right" size="12" color="#969696"></uni-icons>
</view> </view>
@ -163,7 +163,8 @@
<text v-if="billData.merchantOption.note.text || billData.merchantOption.note.isImage" <text v-if="billData.merchantOption.note.text || billData.merchantOption.note.isImage"
class="label">标签</text> class="label">标签</text>
<text v-else class="label">标签和备注</text> <text v-else class="label">标签和备注</text>
<view class="tag-box right-box flex-1 flex-align-center"> <view class="tag-box right-box flex-1 flex-align-center"
@click="handleBillManagementClick('tagAndNote')">
<text v-if="billData.merchantOption.tag.length == 0 || !billData.merchantOption.tag">添加</text> <text v-if="billData.merchantOption.tag.length == 0 || !billData.merchantOption.tag">添加</text>
<view v-else v-for="(tag, index) in billData.merchantOption.tag.slice(0, 3)" :key="tag" <view v-else v-for="(tag, index) in billData.merchantOption.tag.slice(0, 3)" :key="tag"
class="tag-item"> class="tag-item">
@ -178,7 +179,8 @@
<view class="bill-management-item" <view class="bill-management-item"
v-if="billData.merchantOption.note.text || billData.merchantOption.note.isImage"> v-if="billData.merchantOption.note.text || billData.merchantOption.note.isImage">
<text class="label">备注</text> <text class="label">备注</text>
<view class="tag-box right-box flex-1 flex-align-center"> <view class="tag-box right-box flex-1 flex-align-center"
@click="handleBillManagementClick('tagAndNote')">
<view class="remark-box flex-align-center text-align-right flex-1"> <view class="remark-box flex-align-center text-align-right flex-1">
<image v-if="billData.merchantOption.note.isImage" class="remark-img" <image v-if="billData.merchantOption.note.isImage" class="remark-img"
src="/static/image/bill/bill-detail/bill-remark-img.png"> src="/static/image/bill/bill-detail/bill-remark-img.png">
@ -195,7 +197,8 @@
<view class="bill-management-item" style="margin-top: 32rpx;"> <view class="bill-management-item" style="margin-top: 32rpx;">
<text class="label">计入收支</text> <text class="label">计入收支</text>
<view class="right-box flex-align-center" style="transform:scale(0.6); width: 100rpx;"> <view class="right-box flex-align-center" style="transform:scale(0.6); width: 100rpx;">
<switch color="#1676FE" :checked="billData.merchantOption.countInAndOut" /> <switch color="#1676FE" :checked="billData.merchantOption.countInAndOut"
@change="handleCountInAndOutChange" />
</view> </view>
</view> </view>
<view class="bottom-icon-box"> <view class="bottom-icon-box">
@ -212,10 +215,14 @@
</view> </view>
</view> </view>
<billManagementPopup ref="billManagementPopupRef" @confirm="handleBillManagementConfirm"></billManagementPopup>
</template> </template>
<script setup> <script setup>
import navBar from '@/components/nav-bar/nav-bar.vue' import navBar from '@/components/nav-bar/nav-bar.vue'
import billManagementPopup from '@/components/bill-management-popup/bill-management-popup.vue'
import { import {
billBottomIconList billBottomIconList
} from '@/static/json/add-bill.json' } from '@/static/json/add-bill.json'
@ -233,16 +240,18 @@ import {
onLoad, onLoad,
onShow, onShow,
} from '@dcloudio/uni-app' } from '@dcloudio/uni-app'
import { import {
useStore useStore
} from '@/store/index.js' } from '@/store/index.js'
const { const {
getBillList, getBillList,
updateBill updateBill
} = useStore() } = useStore()
const billManagementPopupRef = ref(null)
const buttonGroup = [{ const buttonGroup = [{
name: "编辑账单", name: "编辑账单",
click: () => { click: () => {
@ -353,6 +362,47 @@ const getServiceDetailRightText = (value) => {
const item = serviceDetailRightTextList.find(item => item.value === Number(value)) const item = serviceDetailRightTextList.find(item => item.value === Number(value))
return item?.text || "" return item?.text || ""
} }
//
function handleCountInAndOutChange(e) {
billData.value.merchantOption.countInAndOut = e.detail.value
console.log(e, billData.value)
// Edit mode
updateBill(billData.value.id, billData.value)
}
//
const handleBillManagementClick = (type) => {
let initData = {}
if (type == 'billClassify') {
initData = {
classifyName: billData.value.merchantOption.billClassify
}
} else if (type == 'tagAndNote') {
initData = {
tags: billData.value.merchantOption.tag,
note: billData.value.merchantOption.note
}
}
billManagementPopupRef.value.open(type, initData)
}
/**
* 确认账单管理弹窗
* @param data
*/
const handleBillManagementConfirm = (data) => {
console.log(data)
if (data.type == 'billClassify') {
billData.value.merchantOption.billClassify = data.value
billData.value.selectId = data.id
} else if (data.type == 'tagAndNote') {
billData.value.merchantOption.tag = data.tags
billData.value.merchantOption.note = data.note
}
updateBill(billData.value.id, billData.value)
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -45,6 +45,7 @@
Number(currentMonthData.outCome).toFixed(2) }}</text></view> Number(currentMonthData.outCome).toFixed(2) }}</text></view>
<view class="item"><text>收入</text><text class="money wx-font-regular">{{ <view class="item"><text>收入</text><text class="money wx-font-regular">{{
Number(currentMonthData.inCome).toFixed(2) }}</text></view> Number(currentMonthData.inCome).toFixed(2) }}</text></view>
</view> </view>
<view class=""> <view class="">
<text>收支分析</text> <text>收支分析</text>
@ -56,28 +57,34 @@
:ref="el => cardRefs[index] = el"> :ref="el => cardRefs[index] = el">
<view class="list-title-card" <view class="list-title-card"
:class="{ 'current-month': item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear() }"> :class="{ 'current-month': item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear() }">
<view v-if="item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear()"
class="bg-right-text">
<text>贴纸</text>
<image class="right-icon" src="/static/image/common/right-blue.png"></image>
</view>
<view class="list-title"> <view class="list-title">
<view> <view>
<text class="month alipay-font">{{ item.month }}</text> <text class="month alipay-font">{{ item.month }}</text>
<text></text> <text></text>
</view>
<image class="filter-icon down " src="/static/image/bill/bill-list/down-black.png" mode=""> <image class="filter-icon down " src="/static/image/bill/bill-list/down-black.png" mode="">
</image> </image>
</view> </view>
</view>
<view class="flex-between analysis-box"> <view class="flex-between analysis-box">
<view class="income-ande-outCome"> <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"> <view class="outCome item">
<text class="title">支出</text> <text class="title">支出</text>
<text class="amount alipay-font"><text class="font-11 wx-font-regular"></text>{{ <text class="amount alipay-font"><text class="font-11 wx-font-regular"></text>{{
Number(item.outCome).toFixed(2) Number(item.outCome).toFixed(2)
}}</text> }}</text>
</view> </view>
<view class="income item">
<text class="title">收入</text>
<text class="amount alipay-font"><text class="font-11 wx-font-regular"></text>{{
Number(item.inCome).toFixed(2)
}}</text>
</view>
</view> </view>
<view v-if="!(item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear())" <view v-if="!(item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear())"
class="analysis-button"> class="analysis-button">
@ -229,6 +236,13 @@ onShow(() => {
// #ifdef APP-PLUS // #ifdef APP-PLUS
util.setAndroidSystemBarColor('#F5F5F5') util.setAndroidSystemBarColor('#F5F5F5')
uni.setNavigationBarColor({
animation: { //
duration: 100,
timingFunc: 'easeIn'
}
})
plus.navigator.setStatusBarStyle("dark");
// #endif // #endif
}) })
@ -629,7 +643,6 @@ page {
.income-ande-outCome { .income-ande-outCome {
display: flex; display: flex;
font-size: 13px; font-size: 13px;
margin-top: 6px;
.item { .item {
margin-right: 16px; margin-right: 16px;
@ -648,7 +661,7 @@ page {
border-radius: 8px; border-radius: 8px;
margin: 0 12px; margin: 0 12px;
margin-bottom: 12px; margin-bottom: 12px;
background-color: #FFFFFF; // background-color: #FFFFFF;
.list-title-card { .list-title-card {
background-color: #FFFFFF; background-color: #FFFFFF;
@ -666,7 +679,6 @@ page {
} }
.income-ande-outCome { .income-ande-outCome {
margin-top: 8px;
display: flex; display: flex;
.item { .item {
@ -712,13 +724,20 @@ page {
} }
} }
.list-title {
display: flex;
align-items: center;
}
.current-month { .current-month {
position: relative;
padding: 10px 12px 18px; padding: 10px 12px 18px;
background: url('/static/image/bill/bill-list/current-month-bill-bg.png') no-repeat center center; background: url('/static/image/bill/bill-list/current-month-bill-bg.png') no-repeat center center;
background-size: 100% 100%; background-size: 100% 100%;
background-color: transparent;
.income-ande-outCome { .income-ande-outCome {
margin-top: 8px; margin-top: 8rpx;
display: flex; display: flex;
.item { .item {
@ -747,7 +766,6 @@ page {
} }
.income-ande-outCome-analysis { .income-ande-outCome-analysis {
margin-top: 3px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -780,5 +798,26 @@ page {
} }
} }
.bg-right-text {
display: flex;
align-items: center;
color: #2B3841;
font-size: 22rpx;
position: absolute;
right: 18rpx;
top: 32rpx;
.right-icon {
width: 16rpx;
height: 16rpx;
margin-left: 2rpx;
}
}
.bill-list {
background-color: #ffffff;
}
} }
</style> </style>

View File

@ -0,0 +1,132 @@
<!-- pages/webview/webview.vue -->
<template>
<view class="webview-container">
<navBar class="nav-bar" :title="pageTitle" :bgColor="data.navBar.bgColor" isBack></navBar>
<web-view :src="url" :webview-styles="webviewStyles"></web-view>
</view>
</template>
<script setup>
//
// import ZdyNavbar from "@/components/navbar/navbar.vue"
import navBar from '@/components/nav-bar/nav-bar.vue'
import { util } from '@/utils/common.js'
import {
ref,
reactive,
watch,
nextTick,
getCurrentInstance,
onMounted,
onBeforeUnmount,
toRefs,
computed
} from "vue";
import {
onLoad,
onReady,
onShow
} from '@dcloudio/uni-app'
const data = reactive({
navBar: {
title: "",
bgColor: '#ffffff',
},
dark: "dark",
//
systemBarHeight: "0",
})
let {
systemBarHeight
} = toRefs(data)
const url = ref('')
const pageTitle = ref('')
onLoad((options) => {
uni.getSystemInfo({
success: res => {
systemBarHeight.value = res.statusBarHeight; //
}
})
if (options.url) {
url.value = decodeURIComponent(options.url)
// const videoExps = [/\.mp4$/i, /\.m3u8$/i, /\.flv$/i, /\.avi$/i, /\.mov$/i, /\.wmv$/i, /\.webm$/i,
// /\.mkv$/i
// ];
// const isVideo = videoExps.some(exp => exp.test(url.value))
// console.log(isVideo)
// if (isVideo) {
// plus.navigator.setStatusBarStyle("light");
// } else {
// plus.navigator.setStatusBarStyle("dark");
// }
}
console.log("options参数", options);
if (options.title) {
console.log("标题", options.title);
pageTitle.value = decodeURIComponent(options.title)
}
})
onShow(() => {
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#ffffff')
plus.navigator.setStatusBarStyle("dark");
// #endif
})
const goBack = () => {
uni.navigateBack()
}
const isVideo = computed(() => {
if (!url.value) return false
const videoExps = [/\.mp4$/i, /\.m3u8$/i, /\.flv$/i, /\.avi$/i, /\.mov$/i, /\.wmv$/i, /\.webm$/i,
/\.mkv$/i
];
return videoExps.some(exp => exp.test(url.value));
})
const webviewStyles = computed(() => {
return {
top: `${Number(systemBarHeight.value) + 40}px`,
bottom: '0px',
'padding-bottom': '20px'
}
})
</script>
<style scoped>
.webview-container {
flex: 1;
display: flex;
flex-direction: column;
}
.header {
padding: 20rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
}
.video-web {
position: fixed;
width: 100%;
background-color: #000;
}
</style>
<style>
page {
background-color: #ffffff;
}
</style>

View File

@ -0,0 +1,934 @@
<template>
<view class="container">
<view class="bg-container"></view>
<view class="status-nav-bar" :style="{ height: (statusBarHeight + 44) + 'px' }">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="nav-bar">
<view class="nav-bar-left" @click="goback()">
<image class="nav-bar-left-image" src="@/static/image/alipay-bill/back.png" mode=""></image>
</view>
<view class="nav-bar-right">
<image class="nav-bar-right-image" src="@/static/image/alipay-bill/music-icon.png" mode=""></image>
<image class="nav-bar-right-image" src="@/static/image/alipay-bill/share-icon.png" mode=""></image>
</view>
</view>
</view>
<image ref="bgImage" class="bg-image" src="@/static/image/alipay-bill/main-bg.png" mode="widthFix"></image>
<image class="eye-image" src="@/static/image/alipay-bill/hide.png" mode="widthFix"></image>
<image class="find-more" src="@/static/image/alipay-bill/find-more.png" mode=""></image>
<view class="total-money">
<text>年度总支出</text>
<text class="money alipay-font-bold" @click="openPopup">{{ formatMoney(totalMoney) }}</text>
<text class="unit"></text>
</view>
<view class="frist-money money-type-item">
<view class="type-input-box">
<!-- <{{ moneyFrist.type }}>支出最多 -->
<<view class="percentage-input-box">
<text class="ghost-text">{{ moneyFrist.type }}</text>
<input class="percentage-input" type="text" v-model="moneyFrist.type"
@input="inputPercentage($event, 'moneyFrist')" maxlength="4"></input>
</view>>支出最多
</view>
<view class="money">
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyFrist.percentage }}</text>
<input class="percentage-input" type="digit" v-model="moneyFrist.percentage"
@input="inputPercentage($event, 'moneyFrist')"></input>
</view>
<view>%</view>
</view>
</view>
<view class="second-money money-type-item">
<view class="type-input-box">
<!-- {{ moneySecond.type }} -->
<view class="percentage-input-box">
<text class="ghost-text">{{ moneySecond.type }}</text>
<input class="percentage-input" type="text" v-model="moneySecond.type"
@input="inputPercentage($event, 'moneySecond')"></input>
</view>
</view>
<view class="money">
<view class="percentage-input-box">
<text class="ghost-text">{{ moneySecond.percentage }}</text>
<input class="percentage-input" type="digit" v-model="moneySecond.percentage"
@input="inputPercentage($event, 'moneySecond')"></input>
</view>
<view>%</view>
</view>
</view>
<view class="third-money money-type-item">
<view class="type-input-box">
<!-- {{ moneyThird.type }} -->
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyThird.type }}</text>
<input class="percentage-input" type="text" v-model="moneyThird.type"
@input="inputPercentage($event, 'moneyThird')"></input>
</view>
</view>
<view class="money">
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyThird.percentage }}</text>
<input class="percentage-input" type="digit" v-model="moneyThird.percentage"
@input="inputPercentage($event, 'moneyThird')"></input>
</view>
<view>%</view>
</view>
</view>
<view class="fourth-money money-type-item">
<view class="type-input-box">
<!-- {{ moneyFourth.type }} -->
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyFourth.type }}</text>
<input class="percentage-input" type="text" v-model="moneyFourth.type"
@input="inputPercentage($event, 'moneyFourth')"></input>
</view>
</view>
<view class="money">
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyFourth.percentage }}</text>
<input class="percentage-input" type="digit" v-model="moneyFourth.percentage"
@input="inputPercentage($event, 'moneyFourth')"></input>
</view>
<view>%</view>
</view>
</view>
<view class="fifth-money money-type-item">
<view class="type-input-box">
<!-- {{ moneyFifth.type }} -->
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyFifth.type }}</text>
<input class="percentage-input" type="text" v-model="moneyFifth.type"
@input="inputPercentage($event, 'moneyFifth')"></input>
</view>
</view>
<view class="money">
<view class="percentage-input-box">
<text class="ghost-text">{{ moneyFifth.percentage }}</text>
<input class="percentage-input" type="digit" v-model="moneyFifth.percentage"
@input="inputPercentage($event, 'moneyFifth')"></input>
</view>
<view>%</view>
</view>
</view>
<view class="time-box" @click="openTimePopup">
数据统计截至{{ formatDate(time) }}
</view>
<image class="footer-image" src="@/static/image/alipay-bill/footer.png" mode="heightFix"></image>
<!-- 修改金额弹窗 -->
<uni-popup ref="dialogPopup" type="dialog">
<uni-popup-dialog class="popup-dialog" mode="input" title="修改年度总支出" :before-close="true"
@close="dialogPopupClose()" @confirm="confirm">
<uni-easyinput v-model="totalMoney" step="0.01" type="digit" :focus="true"
placeholder="请输入金额"></uni-easyinput>
</uni-popup-dialog>
</uni-popup>
<!-- 修改时间弹窗 -->
<uni-popup ref="timeDialogPopup" type="dialog">
<uni-popup-dialog class="popup-dialog" mode="input" title="修改时间" :before-close="true"
@close="timeDialogPopupClose()" @confirm="confirmTime">
<picker mode="date" fields="day" :value="time" @change="bindTimeChange" start="2025-12-29"
:end="formatDateISO(new Date())">
<view class="picker-view" style="padding: 10px; border-radius: 4px; text-align: center;">
{{ formatDate(time) || '请选择时间' }}
</view>
</picker>
</uni-popup-dialog>
</uni-popup>
</view>
<!-- 水印 -->
<!-- <view v-if="$isVip()">
<watermark :dark="data.dark" />
<liu-drag-button :canDocking="false" @clickBtn="$goRechargePage('watermark')">
<c-lottie ref="cLottieRef" :src='$watermark()' width="94px" height='74px' :loop="true"></c-lottie>
</liu-drag-button>
</view> -->
</template>
<script setup>
import {
ref,
reactive,
toRefs
} from 'vue';
import {
onLoad,
onShow,
onUnload
} from '@dcloudio/uni-app';
const statusBarHeight = ref(0);
const dialogPopup = ref(null);
const timeDialogPopup = ref(null);
const data = reactive({
totalMoney: 10000000,
time: "",
moneyFrist: {
type: '转账红包',
percentage: "83"
},
moneySecond: {
type: '家居家装',
percentage: "83"
},
moneyThird: {
type: '餐饮美食',
percentage: "83"
},
moneyFourth: {
type: '日用百货',
percentage: "83"
},
moneyFifth: {
type: '服饰装扮',
percentage: "83"
},
navBarHeight: 0
})
let {
totalMoney,
moneyFrist,
moneySecond,
moneyThird,
moneyFourth,
moneyFifth,
time,
navBarHeight
} = toRefs(data)
onLoad(() => {
time.value = getYesterday();
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight;
const config = uni.getStorageSync('config')
console.log("---config---", config);
const font = config.config['client.uniapp.font']
console.log("字体地址信息", font.alipay2025);
// Font loading logic
const fontUrl = font.alipay2025;
const fontName = 'AlibabaSemiBold';
const loadFont = (path) => {
uni.loadFontFace({
family: fontName,
source: `url("${path}")`,
success() {
console.log('字体加载成功');
},
fail(err) {
console.error('字体加载失败', err);
}
});
};
// #ifdef H5
// H5 URL
loadFont(fontUrl);
// #endif
// #ifndef H5
// H5 使
const savedFontPath = uni.getStorageSync('alipay_font_path');
if (savedFontPath) {
loadFont(savedFontPath);
} else {
uni.downloadFile({
url: fontUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
const savedPath = saveRes.savedFilePath;
uni.setStorageSync('alipay_font_path', savedPath);
console.log("字体保存路径", savedPath);
loadFont(savedPath);
},
fail: (err) => {
console.error('保存文件失败', err);
// Fallback:
loadFont(res.tempFilePath);
}
});
}
},
fail: (err) => {
console.error('下载字体失败', err);
}
});
}
// #endif
try {
data.totalMoney = uni.getStorageSync('totalMoney') || 1000000;
data.moneyFrist = uni.getStorageSync('moneyFrist') || {
type: '转账红包',
percentage: "66.3"
};
data.moneySecond = uni.getStorageSync('moneySecond') || {
type: '家居家装',
percentage: "16.8"
};
data.moneyThird = uni.getStorageSync('moneyThird') || {
type: '服饰装备',
percentage: "8.3"
};
data.moneyFourth = uni.getStorageSync('moneyFourth') || {
type: '日用百货',
percentage: "5.2"
};
data.moneyFifth = uni.getStorageSync('moneyFifth') || {
type: '餐饮美食',
percentage: "3.2"
};
data.time = uni.getStorageSync('alipayBillEndTime') || getYesterday();
console.log('数据同步缓存成功');
} catch (err) {
console.log('数据同步缓存失败', err);
}
})
/**
* 打开修改金额弹窗
*/
const openPopup = () => {
dialogPopup.value.open();
}
const goback = () => {
uni.navigateBack();
}
const formatMoney = (val) => {
let num = Number(val);
if (isNaN(num)) {
return '0.00';
}
let str = num.toFixed(2);
let parts = str.split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
}
const dialogPopupClose = () => {
dialogPopup.value.close();
}
const confirm = () => {
uni.setStorageSync('totalMoney', totalMoney.value);
dialogPopup.value.close();
}
/**
* 格式化时间
* @param {Object} date
*/
const formatDate = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = d.getMonth() + 1;
const day = d.getDate();
return `${y}${m}${day}`;
}
const inputPercentage = (e, type) => {
console.log('Saving', type, data[type]);
uni.setStorageSync(type, data[type]);
}
/**
* 打开修改时间弹窗
*/
const openTimePopup = () => {
timeDialogPopup.value.open();
}
const timeDialogPopupClose = () => {
timeDialogPopup.value.close();
}
const bindTimeChange = (e) => {
time.value = e.detail.value;
}
/**
* 获取昨天的时间
*/
const getYesterday = () => {
const date = new Date();
date.setDate(date.getDate() - 1);
return formatDateISO(date);
}
/**
* 格式化时间 2025-12-01
* @param {Object} date
*/
const formatDateISO = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
return `${y}-${m}-${day}`;
}
const confirmTime = () => {
uni.setStorageSync('alipayBillEndTime', time.value);
timeDialogPopup.value.close();
}
</script>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
background-color: #D3E9FE;
// background: linear-gradient(180deg, #6BB8FE 0%, #D3E9FE 100%);
position: relative;
overflow: hidden;
.nav-bar {
box-sizing: border-box;
width: 100vw;
height: 44px;
background-color: transparent;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
.nav-bar-left-image {
width: 24px;
height: 24px;
}
.nav-bar-right-image {
width: 28px;
height: 28px;
margin-left: 16px;
}
}
.bg-container {
width: 100vw;
height: 50%;
background: linear-gradient(180deg, #6BB8FE 0%, #D3E9FE 100%);
position: absolute;
top: 0;
left: 0;
}
}
.bg-image {
width: 100vw;
height: 85% !important;
position: fixed;
left: 0;
bottom: 0;
}
.total-money {
position: absolute;
top: calc(15% + 12px);
left: 10%;
color: #fff;
font-size: 30rpx;
font-weight: 500;
display: flex;
align-items: center;
.money {
font-weight: 700;
font-size: 44rpx;
margin-left: 6rpx;
}
.percentage-input-box {
margin-left: 6rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ghost-text {
font-size: 44rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 44rpx;
line-height: 44rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 44rpx;
text-align: center;
font-weight: 700;
}
.unit {
font-size: 44rpx;
}
}
.frist-money {
position: absolute;
top: calc(48%);
left: 16%;
font-size: 36rpx;
font-weight: 500;
display: flex;
align-items: center;
flex-direction: column;
transform: rotate(6deg);
font-weight: 700;
line-height: 58rpx;
.type-input-box {
.ghost-text {
font-size: 36rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 36rpx;
line-height: 36rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 36rpx;
text-align: center;
font-weight: 700;
}
}
.money {
font-weight: 700;
font-size: 44rpx;
margin-left: 6rpx;
display: flex;
.percentage-input-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ghost-text {
font-size: 44rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 44rpx;
line-height: 44rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 44rpx;
text-align: center;
font-weight: 700;
}
}
}
.second-money {
position: absolute;
top: calc(33% + 8rpx);
right: 12%;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
flex-direction: column;
transform: rotate(6deg);
font-weight: 700;
line-height: 44rpx;
.type-input-box {
height: 44rpx;
.ghost-text {
font-size: 32rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 32rpx;
line-height: 32rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 32rpx;
text-align: center;
font-weight: 700;
}
}
.money {
display: flex;
align-items: center;
font-size: 32rpx;
margin-left: 6rpx;
.percentage-input-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ghost-text {
font-size: 32rpx;
min-width: 16px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 32rpx;
line-height: 32rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 32rpx;
text-align: center;
font-weight: 700;
}
}
}
.third-money {
position: absolute;
top: calc(25% + 8rpx);
left: 23%;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
flex-direction: column;
transform: rotate(4deg);
font-weight: 700;
line-height: 42rpx;
.type-input-box {
height: 42rpx;
line-height: 42rpx;
.ghost-text {
font-size: 32rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 32rpx;
line-height: 32rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 32rpx;
text-align: center;
font-weight: bold;
}
}
.money {
font-size: 32rpx;
margin-left: 6rpx;
display: flex;
align-items: center;
.percentage-input-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ghost-text {
font-size: 32rpx;
min-width: 16px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 32rpx;
line-height: 32rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 32rpx;
text-align: center;
font-weight: 700;
}
}
}
.fourth-money {
position: absolute;
top: calc(44% - 4rpx);
right: calc(14% + 4px);
font-size: 24rpx;
font-weight: 500;
display: flex;
align-items: center;
flex-direction: column;
transform: rotate(4deg);
line-height: 28rpx;
font-weight: 700;
.type-input-box {
height: 28rpx;
line-height: 28rpx;
.ghost-text {
font-size: 24rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 24rpx;
line-height: 24rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 24rpx;
text-align: center;
font-weight: 700;
}
}
.money {
display: flex;
align-items: center;
font-size: 24rpx;
margin-left: 6rpx;
.percentage-input-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ghost-text {
font-size: 24rpx;
min-width: 16px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 24rpx;
line-height: 24rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 24rpx;
text-align: center;
font-weight: 700;
}
}
}
.fifth-money {
position: absolute;
top: calc(35% - 4rpx);
left: 7%;
font-size: 30rpx;
font-weight: 500;
display: flex;
align-items: center;
flex-direction: column;
transform: rotate(4deg);
font-weight: 700;
line-height: 42rpx;
.type-input-box {
height: 42rpx;
line-height: 42rpx;
.ghost-text {
font-size: 30rpx;
min-width: 20px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 30rpx;
line-height: 30rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 30rpx;
text-align: center;
font-weight: bold;
}
}
.money {
display: flex;
align-items: center;
font-size: 30rpx;
margin-left: 6rpx;
.percentage-input-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ghost-text {
font-size: 30rpx;
min-width: 16px;
font-weight: 700;
visibility: hidden;
opacity: 0;
white-space: pre;
height: 30rpx;
line-height: 30rpx;
}
.percentage-input {
position: absolute;
left: 0;
width: 100%;
height: 100%;
font-size: 30rpx;
text-align: center;
font-weight: 700;
}
}
}
.money-type-item {
color: #000000;
font-weight: 700;
.type-input-box {
display: flex;
align-items: center;
.percentage-input-box {
margin-left: 6rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.find-more {
width: 118rpx;
height: 50rpx;
position: absolute;
top: 28%;
right: -4%;
transform: translateX(-50%);
}
.eye-image {
width: 98rpx;
height: 40rpx;
position: absolute;
top: 17%;
right: 8%;
}
.time-box {
padding: 10rpx 56rpx;
background-color: #000000;
opacity: 0.35;
color: #FAC6CA;
font-size: 22rpx;
line-height: 22rpx;
font-weight: 500;
display: inline-block;
text-align: center;
border-radius: 286px;
position: absolute;
white-space: nowrap;
left: 50%;
transform: translateX(-50%);
bottom: 30%;
}
.footer-image {
height: 190rpx;
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 32rpx;
}
.alipay-font-bold {
font-family: 'AlibabaSemiBold';
}
</style>

View File

@ -6,7 +6,7 @@
<view class="nav-bar-box"> <view class="nav-bar-box">
<view class="status-box" :style="{ height: statusBarHeight + 'px' }"></view> <view class="status-box" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="nav-box"> <view class="nav-box">
<view class="left-box"> <view class="left-box" @click="exit">
<image style="width: 40rpx; height: 40rpx;" src="/static/image/nav-bar/back-black.png"></image> <image style="width: 40rpx; height: 40rpx;" src="/static/image/nav-bar/back-black.png"></image>
</view> </view>
<view class="title">小宝模拟器</view> <view class="title">小宝模拟器</view>
@ -14,7 +14,9 @@
</view> </view>
</view> </view>
<view class="content-box"> <view class="content-box" :style="{ height: windowHeight + 'px' }">
<view>
<view style="background-color: transparent;" :style="{ height: (44 + statusBarHeight) + 'px' }"></view>
<view class="user-box"> <view class="user-box">
<image class="user-bg" :style="{ width: (windowWidth - 32) + 'px' }" <image class="user-bg" :style="{ width: (windowWidth - 32) + 'px' }"
:src="`/static/image/index/${userInfo.vip > 1 ? (userInfo.vip == 3 ? 'lifetime-vip-bg' : 'vip-bg') : 'no-vip-bg'}.png`" :src="`/static/image/index/${userInfo.vip > 1 ? (userInfo.vip == 3 ? 'lifetime-vip-bg' : 'vip-bg') : 'no-vip-bg'}.png`"
@ -34,27 +36,42 @@
</view> </view>
<view v-if="userInfo.vip && userInfo.vip != 3" class="btn-box"> <view v-if="userInfo.vip && userInfo.vip != 3" class="btn-box">
<image class="open-vip-btn" <image class="open-vip-btn"
:src="`/static/image/index/${userInfo.vip > 1 ? 'vip-btn' : 'open-vip-btn'}.png`"></image> :src="`/static/image/index/${userInfo.vip > 1 ? 'vip-btn' : 'open-vip-btn'}.png`">
</image>
</view> </view>
</view> </view>
</view> </view>
<view class="notice-box"> <view class="notice-box" @click="clickNotice">
<view class="sound-box">
<uni-icons type="sound" size="18" color="#D8D8D8"></uni-icons> <uni-icons type="sound" size="18" color="#D8D8D8"></uni-icons>
<text class="notice-content">欢迎大家使用装样大师,使用中有问题客服为您解答</text> </view>
<view ref="noticeContainer" class="notice-content-wrapper">
<view ref="noticeInner" class="notice-inner">
<text ref="noticeBox" class="notice-content" style="margin-right: 30rpx;">{{ noticeInfo.text
}}</text>
<text class="notice-content" style="margin-right: 30rpx;">{{ noticeInfo.text }}</text>
</view>
</view>
</view> </view>
<view class="group-box"> <view class="group-box">
<image class="title-img" src="/static/image/index/shipingjiaocheng.png"></image> <image class="title-img" src="/static/image/index/shipingjiaocheng.png"></image>
<view class="video-help-box"> <view class="video-help-box">
<view class="video-help-item" v-for="item in videoHelpList" :key="item.id"
@click="clickVideoHelp(item)">
<image class="video-help-img" :src="item.icon"></image>
<text class="video-help-title">{{ item.text }}教程</text>
</view>
</view> </view>
</view> </view>
<view class="group-box"> <view class="group-box">
<image class="title-img" src="/static/image/index/monixiaobao.png"></image> <image class="title-img" src="/static/image/index/monixiaobao.png"></image>
<view class="menu-box"> <view class="menu-box">
<view class="item-box" v-for="item in menuList" :key="item.name" @click="util.goPage(item.path)"> <view class="item-box" v-for="item in menuList" :key="item.name"
@click="util.goPage(item.path)">
<view class="menu-item" :style="{ width: (windowWidth - 50) / 2 + 'px' }"> <view class="menu-item" :style="{ width: (windowWidth - 50) / 2 + 'px' }">
<!-- <text class="menu-item-name">{{ item.name }}</text> --> <!-- <text class="menu-item-name">{{ item.name }}</text> -->
<image class="name-img" :src="'/static/image/index/menu-name/' + item.icon + '.png'" <image class="name-img" :src="'/static/image/index/menu-name/' + item.icon + '.png'"
@ -70,11 +87,16 @@
<view class="activity-box"> <view class="activity-box">
<image class="alipay-year-bill" :style="{ width: (windowWidth - 32) + 'px' }" <image class="alipay-year-bill" :style="{ width: (windowWidth - 32) + 'px' }"
src="/static/image/index/alipay-year-bill.png" mode="widthFix"></image> src="/static/image/index/alipay-year-bill.png" mode="widthFix"
@click="util.goPage(`/pages/index/alipay-annual-bill/alipay-annual-bill`)"></image>
</view>
</view> </view>
<view class="footer-box">
<text class="vision-text">版本:{{ vision }}</text>
</view> </view>
</view>
</view> </view>
</template> </template>
@ -85,6 +107,9 @@ import {
import { import {
storage storage
} from '@/utils/storage.js' } from '@/utils/storage.js'
import {
get
} from '@/utils/requests.js'
import { import {
ref, ref,
reactive, reactive,
@ -128,20 +153,30 @@ const data = reactive({
statusBarHeight: 0, statusBarHeight: 0,
windowWidth: 0, windowWidth: 0,
windowHeight: 0, windowHeight: 0,
userInfo: {} userInfo: {},
videoHelpList: [],
noticeInfo: {},
vision: "1.0.0"
}) })
const { const {
statusBarHeight, statusBarHeight,
windowWidth, windowWidth,
userInfo windowHeight,
userInfo,
videoHelpList,
noticeInfo,
vision
} = toRefs(data); } = toRefs(data);
onLoad(() => { onLoad(() => {
setUserData() // 启动时获取数据
fetchUserData()
}) })
onShow(() => { onShow(() => {
// 每次显示时刷新数据
setUserData()
// 获取系统信息 // 获取系统信息
const systemInfo = uni.getSystemInfoSync(); const systemInfo = uni.getSystemInfoSync();
data.statusBarHeight = systemInfo.statusBarHeight; data.statusBarHeight = systemInfo.statusBarHeight;
@ -151,12 +186,221 @@ onShow(() => {
util.setAndroidSystemBarColor('#F0F4F9') util.setAndroidSystemBarColor('#F0F4F9')
// #endif // #endif
}) })
/** /**
* 设置用户数据 * 获取用户数据(从服务器)
*/
const fetchUserData = async () => {
try {
// 先设置默认值,避免页面显示异常
setUserData()
// 并行获取用户信息和配置
const [userResult, configResult] = await Promise.allSettled([
fetchUserInfo(),
fetchUserConfig()
])
// 处理用户信息结果
if (userResult.status === 'fulfilled') {
console.log('用户信息获取成功')
} else {
console.error('获取用户信息失败:', userResult.reason)
}
// 处理用户配置结果
if (configResult.status === 'fulfilled') {
console.log('用户配置获取成功')
} else {
console.error('获取用户配置失败:', configResult.reason)
}
// 刷新页面数据
setUserData()
} catch (error) {
console.error('获取用户数据异常:', error)
}
}
/**
* 获取用户信息
*/
const fetchUserInfo = async () => {
const data = await get('', 'api/user', {})
if (data.code === 0) {
uni.setStorageSync('userInfo', data.data)
return data.data
} else {
throw new Error(data.message || '获取用户信息失败')
}
}
/**
* 获取用户配置
*/
const fetchUserConfig = async () => {
const data = await get('', 'api/user/config', {})
if (data.code === 0) {
uni.setStorageSync('config', data.data)
return data.data
} else {
throw new Error(data.message || '获取用户配置失败')
}
}
/**
* 设置用户数据(从本地存储读取)
*/ */
const setUserData = () => { const setUserData = () => {
data.userInfo = storage.get("userInfo") // 用户信息 - 提供默认值
console.log(data.userInfo) const userInfoData = storage.get("userInfo")
data.userInfo = userInfoData || {
user_id: '加载中...',
avater: '/static/default-avatar.png',
vip: 0,
vip_expire: ''
}
// 配置信息 - 安全访问
const configData = storage.get("config")
if (configData && configData.config) {
data.noticeInfo = configData.config['client.uniapp.notice'] || { text: '加载中...', url: '' }
data.noticeInfo = configData.config['client.uniapp.notice'] || { text: '加载中...', url: '' }
// 启动走马灯
startMarquee();
data.videoHelpList = configData.config['client.uniapp.alipay.video_help'] || []
} else {
data.noticeInfo = { text: '加载中...', url: '' }
data.videoHelpList = []
}
}
/**
* 点击视频教程
* @param item
*/
const clickVideoHelp = (item) => {
const url = item.url
util.goPage(`/pages/common/webview/webview?url=${encodeURIComponent(url)}&title=${item.text}`)
}
/**
* 点击公告
*/
const clickNotice = () => {
if (!noticeInfo.value.url) return
const url = noticeInfo.value.url + `&uni_id=${userInfo.value.user_id}`
util.goPage(`/pages/common/webview/webview?url=${encodeURIComponent(url)}&title=${noticeInfo.value.title}`)
}
/**
* 退出模拟器
*/
const exit = () => {
plus.runtime.quit()
}
const noticeContainer = ref(null);
const noticeInner = ref(null);
const noticeBox = ref(null);
// #ifndef H5
const animation = uni.requireNativePlugin('animation');
const dom = uni.requireNativePlugin('dom');
// #endif
let marqueeTimer = null;
const currentMarqueeId = ref(0);
const lastMarqueeText = ref('');
/**
* 开始走马灯
*/
const startMarquee = () => {
// 避免不必要的重置:如果文本没有变化且正在运行,则忽略
if (lastMarqueeText.value === noticeInfo.value.text && currentMarqueeId.value > 0) {
return;
}
lastMarqueeText.value = noticeInfo.value.text;
// 清除待执行的启动定时器
if (marqueeTimer) {
clearTimeout(marqueeTimer);
marqueeTimer = null;
}
// 增加 ID 以使之前的动画循环失效
currentMarqueeId.value++;
const myId = currentMarqueeId.value;
// 清除旧动画状态 (重置位置)
if (noticeInner.value) {
animation.transition(noticeInner.value, {
styles: { transform: 'translateX(0)' },
duration: 0,
delay: 0
});
}
marqueeTimer = setTimeout(() => {
if (!noticeContainer.value || !noticeInner.value) return;
// 如果ID不匹配说明有新的启动请求放弃当前
if (myId !== currentMarqueeId.value) return;
// 获取容器宽度
dom.getComponentRect(noticeContainer.value, (resContainer) => {
const containerWidth = resContainer.size.width;
if (!containerWidth) return;
// 获取文本/内部容器宽度
dom.getComponentRect(noticeInner.value, (resText) => {
const textWidth = resText.size.width / 2; // 因为复制了一份
if (!textWidth) return;
// 执行滚动
runMarqueeAnimation(containerWidth, textWidth, myId);
});
});
}, 1000); // 增加延时确保渲染
}
/**
* 执行滚动动画循环
*/
const runMarqueeAnimation = (containerWidth, textWidth, myId) => {
// ID 校验如果当前ID不匹配说明已被新动画取代停止递归
if (myId !== currentMarqueeId.value) return;
if (!noticeInner.value) return;
// 动画的目标:移动 Inner 容器
const target = noticeInner.value;
const realScrollDistance = textWidth;
// 1. 重置位置到 0 (无感重置)
animation.transition(target, {
styles: { transform: `translateX(0)` },
duration: 0,
delay: 0
}, () => {
// 再次校验ID
if (myId !== currentMarqueeId.value) return;
// 计算时间
const speed = 50; // px/s
const duration = (realScrollDistance / speed) * 1000;
// 2. 向左滚动一个文本宽度的距离
animation.transition(target, {
styles: { transform: `translateX(-${realScrollDistance}px)` },
duration: duration,
timingFunction: 'linear',
delay: 0
}, () => {
// 3. 循环
runMarqueeAnimation(containerWidth, textWidth, myId);
});
});
} }
</script> </script>
<style> <style>
@ -182,11 +426,14 @@ const setUserData = () => {
.content-box { .content-box {
position: fixed; position: fixed;
top: 150rpx; top: 0rpx;
left: 0; left: 0;
right: 0; right: 0;
z-index: 999; z-index: 999;
background-color: transparent; background-color: transparent;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.status-box { .status-box {
@ -215,6 +462,7 @@ const setUserData = () => {
flex: 1; flex: 1;
height: 44px; height: 44px;
font-size: 32rpx; font-size: 32rpx;
font-weight: 500;
color: #1A1A1A; color: #1A1A1A;
font-weight: bold; font-weight: bold;
background-color: transparent; background-color: transparent;
@ -234,7 +482,7 @@ const setUserData = () => {
.user-box { .user-box {
position: relative; position: relative;
margin: 0 32rpx 0; margin: 24rpx 32rpx 0;
height: 120rpx; height: 120rpx;
z-index: 10; z-index: 10;
} }
@ -310,12 +558,33 @@ const setUserData = () => {
border-radius: 16rpx 16rpx 16rpx 16rpx; border-radius: 16rpx 16rpx 16rpx 16rpx;
padding: 0 16rpx; padding: 0 16rpx;
height: 64rpx; height: 64rpx;
overflow: hidden;
}
.sound-box {
height: 64rpx;
width: 50rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
}
.notice-content-wrapper {
flex: 1;
flex-direction: row;
overflow: hidden;
}
.notice-inner {
flex-direction: row;
align-items: center;
} }
.notice-content { .notice-content {
font-size: 24rpx; font-size: 24rpx;
color: #767676; color: #767676;
margin-left: 8rpx;
} }
.group-box { .group-box {
@ -329,12 +598,30 @@ const setUserData = () => {
} }
.video-help-box { .video-help-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #FFFFFF; background-color: #FFFFFF;
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
border-radius: 24rpx; border-radius: 24rpx;
margin-top: 16rpx; margin-top: 16rpx;
} }
.video-help-item {
text-align: center;
}
.video-help-img {
width: 96rpx;
height: 96rpx;
}
.video-help-title {
font-size: 24rpx;
color: #1A1A1A;
}
.menu-box { .menu-box {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -380,8 +667,8 @@ const setUserData = () => {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 72rpx; width: 68rpx;
height: 32rpx; height: 30rpx;
z-index: 99; z-index: 99;
} }
@ -392,4 +679,17 @@ const setUserData = () => {
.alipay-year-bill { .alipay-year-bill {
/* width: 100%; */ /* width: 100%; */
} }
.footer-box {
display: flex;
align-items: center;
justify-content: center;
margin-top: 40rpx;
margin-bottom: 20rpx;
}
.vision-text {
font-size: 24rpx;
color: #767676;
}
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -383,7 +383,6 @@ export const util = {
timingFunc: 'easeIn' timingFunc: 'easeIn'
} }
}) })
console.log("状态栏设置完毕!");
setTimeout(function () { setTimeout(function () {
uni.setNavigationBarColor({ uni.setNavigationBarColor({
frontColor: frontColor, frontColor: frontColor,