alipay-emulator/pages/ant-credit-pay/index.vue

895 lines
21 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 水印 -->
<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>
<view class="page-container">
<view class="main-container">
<NavBar v-if="!selectedImage" title="花呗" :bgColor="data.navBar.bgColor" tipLayerType="huabei-tip" isTipLayer
tipLayerText="修改花呗信息" :buttonGroup="buttonGroup" @button-click="clickTitlePopupButton">
<!-- 使用作用域插槽自定义按钮渲染特别是switch的checked绑定 -->
<template #button="{ button }">
<view class="button flex-align-center flex-justify-center">
{{ button.name }}
<view @tap.stop>
<switch v-if="button.isSwitch" :checked="data.huabeiInfo.isOverdue" @change="button.click"
style="transform: scale(0.7);"></switch>
</view>
</view>
</template>
<view class="nav-bar flex-between w100" :class="{ 'ios-nav-bar': $system == 'iOS' }">
<view class="flex-align-center flex-1">
<view class="left flex-align-center" @click.stop="goBack">
<image class=" back-icon" src="/static/image/nav-bar/back-white.png" mode="">
</image>
</view>
<view class="title flex-align-center">
花呗
</view>
</view>
<view class="right flex-align-center">
<image class="icon" src="/static/image/ant-credit-pay/service.png"></image>
<image class="icon" src="/static/image/ant-credit-pay/setting.png"></image>
</view>
</view>
</NavBar>
<NavBar v-else title="拼图" bgColor="#EFEFEF" noBack @back="closeImage" isRightButton
@right-click="confirmImage">
</NavBar>
<view v-if="huabeiInfo.styleType == 1" class="current-month">{{ huabeiInfo.mouth }}月应还(元)</view>
<view v-else class="current-month">{{ huabeiInfo.mouth }}月账单累计中(元)</view>
<view class="money-box flex-align-center">
<text class="money alipay-font">{{ numberUtil.formatMoneyWithThousand(huabeiInfo.money) }}</text>
<uni-icons type="right" size="18" color="#B9D6FF"></uni-icons>
</view>
<!-- 样式一 按钮样式 -->
<view v-if="huabeiInfo.styleType == 1 || !huabeiInfo.styleType" class="style-1 button-group">
<view class="button-item second-button" :class="{ 'ios-button': $system == 'iOS' }">立即还款</view>
<view v-if="!huabeiInfo.isInstallment" class="button-item primary-button"
:class="{ 'ios-button': $system == 'iOS' }">
分期还款
<view v-if="huabeiInfo.installmentBadgeText" class="badge">{{ huabeiInfo.installmentBadgeText }}
</view>
</view>
</view>
<!-- 样式二 纯气泡样式 -->
<view v-if="huabeiInfo.styleType == 2" class="style-2 bubble-container">
<view class="bubble-box">
<view class="arrow"></view>
<text class="text">{{ huabeiInfo.descText }}</text>
</view>
</view>
<!-- 样式三 气泡带箭头样式 -->
<view v-if="huabeiInfo.styleType == 3" class="style-3 bubble-container">
<view class="bubble-box">
<view class="arrow"></view>
<text class="text flex-align-center">{{ huabeiInfo.descText }}
<uni-icons type="right" size="18" color="#B9D6FF"></uni-icons>
</text>
</view>
</view>
<view class="total-info-box flex-align-center">
<view class="info-item">
<view class="label">总计账单</view>
<view class="value">还款日每月{{ huabeiInfo.dueDate }}日</view>
</view>
<view class="info-item">
<view class="label">总计额度</view>
<view class="value">{{
numberUtil.formatMoneyWithThousand(Number(huabeiInfo.totalAmount) - Number(huabeiInfo.money))
}}可用
</view>
</view>
</view>
</view>
<view v-if="!selectedImage" class="image-box flex-1 flex-align-center flex-column flex-justify-center"
@touchstart="handleTouchStart" @touchend="handleTouchEnd">
<view v-if="!huabeiInfo.image" class="flex-align-center flex-column">
<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 v-else class="w100 h100">
<image class="w100 h100" :src="huabeiInfo.image" mode="widthFix"></image>
</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>
<!-- 编辑弹窗 -->
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="popup-content">
<view class="popup-title">编辑花呗数据</view>
<view class="form-item">
<text class="label">还款月份</text>
<picker :range="monthRange" :value="editHuabeiInfo.mouth - 1" @change="onMonthChange"
style="flex:1">
<view class="input">{{ editHuabeiInfo.mouth ? editHuabeiInfo.mouth + '月' : '请选择月份' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">本月应还</text>
<input class="input" type="digit" v-model="editHuabeiInfo.money" placeholder="请输入金额" />
</view>
<view class="form-item">
<text class="label">还款日</text>
<input class="input" type="number" v-model="editHuabeiInfo.dueDate" placeholder="请输入日期" />
</view>
<view class="form-item">
<text class="label">总计额度</text>
<input class="input" type="digit" v-model="editHuabeiInfo.totalAmount" placeholder="请输入总计额度" />
</view>
<view v-if="huabeiInfo.styleType != 1" class="form-item">
<text class="label">气泡文本</text>
<input class="input" type="text" v-model="editHuabeiInfo.descText" placeholder="请输入描述文本" />
</view>
<view v-if="huabeiInfo.styleType == 1 && !editHuabeiInfo.isInstallment" class="form-item">
<text class="label">分期气泡</text>
<input class="input" type="text" v-model="editHuabeiInfo.installmentBadgeText"
placeholder="请输入分期还款气泡文案" />
</view>
<view v-if="huabeiInfo.styleType == 1" class="form-item flex-between">
<text class="label">是否分期</text>
<switch :checked="editHuabeiInfo.isInstallment" style="transform: scale(0.8);"
@change="e => editHuabeiInfo.isInstallment = e.detail.value" />
</view>
<view class="popup-btns">
<view class="btn cancel" @click="closeDialog">取消</view>
<view class="btn confirm" @click="confirmDialog">确定</view>
</view>
</view>
</uni-popup>
<!-- 样式选择弹窗 -->
<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="huabeiInfo.styleType == 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>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar'
import {
numberUtil,
util
} from '@/utils/common.js'
import {
ref,
toRefs,
getCurrentInstance,
reactive
} from 'vue';
import {
onLoad,
onShow
} from '@dcloudio/uni-app';
const instance = getCurrentInstance();
const { proxy } = instance;
const buttonGroup = [{
name: "编辑花呗数据",
click: () => {
openDialog()
}
}, {
name: "切换展示样式",
click: () => {
openStyleDialog()
}
}, {
name: "删除当前底部图片",
click: () => {
data.huabeiInfo.image = ""
uni.setStorageSync(data.huabeiInfoStorageKey, data.huabeiInfo)
}
}, {
name: "花呗逾期",
isSwitch: true,
click: () => {
data.huabeiInfo.isOverdue = !data.huabeiInfo.isOverdue
uni.setStorageSync(data.huabeiInfoStorageKey, data.huabeiInfo)
if (data.huabeiInfo.isOverdue) {
uni.redirectTo({
url: '/pages/ant-credit-pay/overdue-payment/overdue-payment'
})
}
}
}]
const data = reactive({
navBar: {
bgColor: "#1777FF",
textColor: "#fff"
},
huabeiInfoStorageKey: 'huabei_info_storage',
huabeiInfo: {
mouth: 1,
money: 100,
dueDate: 15,
totalAmount: 200000,
descText: "当前账单进度已超出预期,花超了",
isInstallment: false,
styleType: 1,
installmentBadgeText: '4折起',
image: "",
isOverdue: false,
daysPastDue: 1,
isOverdueDeactivate: false,
},
selectedImage: '',
showMask: false
})
let {
huabeiInfo,
selectedImage,
showMask
} = toRefs(data)
// 编辑表单数据
const editHuabeiInfo = ref({})
const popup = ref(null)
const stylePopup = ref(null)
const scrollTop = ref(0)
const onImageScroll = (e) => {
scrollTop.value = e.detail.scrollTop
}
const styleList = [{
label: '样式 1 (默认)',
value: 1
},
{
label: '样式 2 (纯气泡)',
value: 2
},
{
label: '样式 3 (带箭头气泡)',
value: 3
}
]
const monthRange = Array.from({
length: 12
}, (_, i) => i + 1)
const onMonthChange = (e) => {
editHuabeiInfo.value.mouth = monthRange[e.detail.value]
}
onLoad((option) => {
// 进入花呗页面埋点
proxy.$apiUserEvent('all', {
type: 'click',
key: 'huabei',
value: "花呗"
})
console.log(option)
// 读取缓存
let savedInfo = uni.getStorageSync(data.huabeiInfoStorageKey)
// savedInfo.image = ""
// uni.setStorageSync(data.huabeiInfoStorageKey, savedInfo)
console.log("savedInfo====", savedInfo)
if (savedInfo) {
// 合并默认值,防止旧数据缺少新字段
data.huabeiInfo = {
...data.huabeiInfo,
...savedInfo
}
}
})
onShow(() => {
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#ffffff')
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 500)
// #endif
})
// 打开弹窗
const openDialog = () => {
// 深拷贝当前数据到编辑表单
editHuabeiInfo.value = JSON.parse(JSON.stringify(data.huabeiInfo))
popup.value.open()
}
// 关闭弹窗
const closeDialog = () => {
popup.value.close()
}
// 确认修改
const confirmDialog = () => {
// 校验数据: 本月应还不能大于总计额度
if (Number(editHuabeiInfo.value.money) > Number(editHuabeiInfo.value.totalAmount)) {
uni.showToast({
title: '本月应还不能大于总计额度',
icon: 'none'
})
return
}
data.huabeiInfo = JSON.parse(JSON.stringify(editHuabeiInfo.value))
// 保存到缓存
uni.setStorageSync(data.huabeiInfoStorageKey, data.huabeiInfo)
popup.value.close()
uni.showToast({
title: '保存成功',
icon: 'success'
})
}
// 打开样式选择弹窗
const openStyleDialog = () => {
stylePopup.value.open()
}
// 关闭样式选择弹窗
const closeStyleDialog = () => {
stylePopup.value.close()
}
// 确认图片裁剪
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)
data.huabeiInfo.image = saveRes.savedFilePath
selectedImage.value = '' // 隐藏编辑模式
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 200)
// 保存到缓存
uni.setStorageSync(data.huabeiInfoStorageKey, data.huabeiInfo)
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 confirmStyleDialog = (type) => {
data.huabeiInfo.styleType = type
// 保存到缓存
uni.setStorageSync(data.huabeiInfoStorageKey, data.huabeiInfo)
stylePopup.value.close()
}
const clickTitlePopupButton = (button) => {
button.click()
}
// 选择图片
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)
}
})
}
// 长按事件定时器
let longPressTimer = null
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)
longPressTimer = null
}
}
const closeImage = () => {
selectedImage.value = ''
data.showMask = false
plus.navigator.setStatusBarStyle("light");
return false
}
const closeMask = () => {
data.showMask = false
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style>
@import "/common/main.css";
</style>
<style lang="less" scoped>
.page-container {
position: relative;
display: flex;
flex-direction: column;
background-color: #ffffff;
height: 100vh;
overflow: hidden;
.nav-bar {
height: 100%;
display: flex;
align-items: center;
padding: 0 20rpx;
.left {
display: flex;
align-items: center;
}
.title {
color: #ffffff;
font-size: 17px;
font-weight: 500;
text-align: center;
display: flex;
align-items: center;
}
.right {
width: 80px;
}
.icon {
margin-left: 18rpx;
margin-right: 10rpx;
width: 24px;
height: 24px;
}
}
.ios-nav-bar {
.left {
width: 80px;
}
.title {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.right {
width: 80px;
}
}
.main-container {
background-color: #1777FF;
padding-bottom: 32rpx;
// position: absolute;
// top: 0;
// left: 0;
// right: 0;
// z-index: 99;
.current-month {
margin-top: 12rpx;
color: #B9D6FF;
font-size: 24rpx;
text-align: center;
}
.money-box {
text-align: center;
justify-content: center;
.money {
font-size: 64rpx;
color: #ffffff;
}
}
.button-group {
margin-top: 18rpx;
display: flex;
justify-content: center;
justify-content: center;
.button-item {
height: 64rpx;
line-height: 64rpx;
border-radius: 8rpx;
color: #ffffff;
font-size: 30rpx;
padding: 0 24rpx;
}
.second-button {
border: 2rpx solid #66B2FD;
}
.primary-button {
background-color: #66B2FD;
margin: 0 22rpx;
position: relative;
.badge {
position: absolute;
top: -16rpx;
right: -10rpx;
background-color: #F34624;
color: #fff;
font-size: 20rpx;
padding: 0 10rpx;
height: 32rpx;
line-height: 32rpx;
border-radius: 16rpx 16rpx 16rpx 0;
z-index: 1;
}
}
.ios-button {
border-radius: 33rpx !important;
}
}
.style-1 {
margin-top: 18rpx;
display: flex;
justify-content: center;
justify-content: center;
}
.bubble-container {
margin-top: 10rpx;
display: flex;
justify-content: center;
.bubble-box {
position: relative;
border: 1px solid #75bcff;
border-radius: 30rpx;
padding: 12rpx 24rpx;
display: flex;
align-items: center;
justify-content: center;
.text {
color: #ffffff;
font-size: 26rpx;
line-height: 1.2;
}
.arrow {
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 8px;
height: 8px;
background-color: #1777FF;
border-left: 1px solid #75bcff;
border-top: 1px solid #75bcff;
z-index: 1;
}
}
}
.total-info-box {
margin-top: 48rpx;
.info-item {
width: 50%;
text-align: center;
.label {
color: #ffffff;
font-size: 30rpx;
line-height: 42rpx;
}
.value {
color: #ffffff;
font-size: 26rpx;
line-height: 36rpx;
}
}
}
}
.image-box {
width: 100%;
overflow: hidden; // scroll-view 需要
}
.scroll-image-box {
width: 100%;
min-height: 0; // 修复 flex一出问题
// overflow: hidden; // scroll-view 需要
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;
}
}
}
.back-icon {
width: 24px;
height: 24px;
}
</style>
<style lang="scss" scoped>
.popup-content {
background-color: #fff;
width: 600rpx;
border-radius: 16rpx;
padding: 30rpx;
.popup-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.form-item {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.label {
width: 140rpx;
font-size: 28rpx;
color: #333;
}
.input {
flex: 1;
height: 72rpx;
line-height: 72rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
}
.flex-between {
justify-content: space-between;
}
.popup-btns {
display: flex;
margin-top: 40rpx;
.btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 12rpx;
font-size: 30rpx;
&.cancel {
background-color: #f5f5f5;
color: #666;
margin-right: 20rpx;
}
&.confirm {
background-color: #1777FF;
color: #fff;
}
}
}
}
.style-list {
.style-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
border-bottom: 1px solid #f5f5f5;
font-size: 30rpx;
color: #333;
&:last-child {
border-bottom: none;
}
}
}
</style>