对接银联和日照银行分开页面

This commit is contained in:
zhouyanli
2026-09-21 14:07:37 +08:00
parent 4f209b8fb7
commit cf8e8263b7
9 changed files with 1357 additions and 100 deletions

View File

@@ -2,8 +2,8 @@
"name" : "花开物业", "name" : "花开物业",
"appid" : "__UNI__29C3D60", "appid" : "__UNI__29C3D60",
"description" : "智慧社区居民端面向小区业主使用,支持注册、验证码、账号密码多种登录方式,提供密码找回功能,账号使用安全便捷。支持多小区切换,首页设有轮播 banner、社区公告,集成远程开门、生活缴费、在线报修、访客邀请、社区活动常用服务。全服务板块统一涵盖门禁开门、线上缴费、报修申报、访客预约、投诉反馈、问卷调研、活动报名、社区公示、一键联系物业功能。个人中心可管理房屋、住户、车位、车辆信息,支持更换手机号、修改密码、消息通知设置、版本更新及账号退出,一站式满足业主居家生活、社区服务、房产车辆管理需求,实现社区生活数字化、便捷化。", "description" : "智慧社区居民端面向小区业主使用,支持注册、验证码、账号密码多种登录方式,提供密码找回功能,账号使用安全便捷。支持多小区切换,首页设有轮播 banner、社区公告,集成远程开门、生活缴费、在线报修、访客邀请、社区活动常用服务。全服务板块统一涵盖门禁开门、线上缴费、报修申报、访客预约、投诉反馈、问卷调研、活动报名、社区公示、一键联系物业功能。个人中心可管理房屋、住户、车位、车辆信息,支持更换手机号、修改密码、消息通知设置、版本更新及账号退出,一站式满足业主居家生活、社区服务、房产车辆管理需求,实现社区生活数字化、便捷化。",
"versionName" : "1.2.5", "versionName" : "1.2.7",
"versionCode" : 139, "versionCode" : 141,
"transformPx" : false, "transformPx" : false,
"uniCloud" : { "uniCloud" : {
"provider" : "aliyun", "provider" : "aliyun",
@@ -57,7 +57,7 @@
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>" "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
], ],
"schemes" : "", "schemes" : "myapppay",
"targetSdkVersion" : 30 "targetSdkVersion" : 30
}, },
/* ios打包配置 */ /* ios打包配置 */
@@ -120,7 +120,8 @@
"__platform__" : [ "ios", "android" ] "__platform__" : [ "ios", "android" ]
}, },
"weixin" : { "weixin" : {
"__platform__" : [ "ios", "android" ] "__platform__" : [ "ios", "android" ],
"appid" : "wx84b02b2ec80715f4"
} }
}, },
"oauth" : { "oauth" : {

View File

@@ -180,7 +180,7 @@ export const getopenIdByCode = (data) => {
// 银联支付下单 // 银联支付下单
export const getUnionPayTn = (data) => { export const getUnionPayTn = (data) => {
return post('/api/topUp/unionPay/getTn', data) return post('/api/unionPay/getTn', data)
} }
// 获取日照银行小程序拉起参数 // 获取日照银行小程序拉起参数
export const getBankMiniProgramParams = (data) => { export const getBankMiniProgramParams = (data) => {

View File

@@ -2,8 +2,8 @@ import { navigateToLogin } from '@/common/checkLogin.js'
let baseUrl = ''; let baseUrl = '';
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
baseUrl = 'http://192.168.1.222:8080'; // 开发环境 baseUrl = 'http://192.168.0.222:8080'; // 开发环境
// baseUrl = "https://during-immovable-eats.ngrok-free.dev"; // baseUrl = 'https://wuyeapi.ckdzkj.com';
} else { } else {
baseUrl = 'https://wuyeapi.ckdzkj.com'; // 生产环境 baseUrl = 'https://wuyeapi.ckdzkj.com'; // 生产环境
// baseUrl = 'https://during-immovable-eats.ngrok-free.dev'; // baseUrl = 'https://during-immovable-eats.ngrok-free.dev';

View File

@@ -0,0 +1,700 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="switchStatus(paymentItem.type) || '缴费'" left-icon="left" @clickLeft="goBack"
backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 1. 缴费详情 -->
<view v-if="currentStep === 1" class="pay-page">
<!-- 物业费缴费 -->
<!-- 金额展示 -->
<view class="amount-section" v-if="outstandingPayment">
<text class="amount-text"
:class="{ 'outstandingPaymentClass':outstandingPayment }">{{outstandingPayment?'-¥'+payAmount:'¥'+payAmount}}</text>
</view>
<view class="amount-section" v-else>
<text class="amount-text paymentClass">{{'¥'+payAmount}}</text>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<!-- 缴费信息 -->
<view class="info-section" style="">
<view>
<view class="info-item">
<text class="info-label">户号</text>
<text class="info-value">{{ paymentItem.deviceCode}}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name}}</text>
</view>
<view class="info-item">
<text class="info-label">地址</text>
<text class="info-value">{{ paymentItem.fullAddress}}</text>
</view>
<!-- <view style="border-bottom: 2rpx solid #c7c7c7;padding-bottom:20rpx;"></view> -->
<!-- 缴费金额输入 -->
<!-- <view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<view class="input-section" style="display: flex;">
<text class="amount-input" style="width: auto;margin: 0rpx 20rpx;">¥</text>
<input
type="digit"
:value="payNum"
@input="(e) => payNum = e.detail.value"
class="amount-input"
placeholder="点击输入缴费金额"
/>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view> -->
<view class="arrears-row" v-if="outstandingPayment==true">
<text class="arrears-text">当前欠费金额¥{{ arrearsAmount }}元</text>
</view>
<!-- <view class="arrears-row" v-if="outstandingPayment==false">
<text class="arrears-text">暂未查询到欠费</text>
</view> -->
</view>
</view>
</view>
<!-- 2. 缴费成功页 -->
<view class="container" v-if="currentStep==2">
<!-- 成功图标 -->
<view class="success-icon">
<text class="icon-check">✓</text>
</view>
<!-- 主标题 -->
<view class="title">缴费成功</view>
<!-- 提示文案 -->
<view class="desc">
成功缴纳{{ switchStatus(paymentItem.type) || '' }}费用 ¥{{ arrearsAmount || payAmount }}元
</view>
</view>
</view>
</scroll-view>
<!-- 立即缴费按钮 -->
<view class="btn-section" v-if="currentStep === 1">
<button class="pay-btn" @click="handlePay">立即缴费</button>
</view>
<!-- 返回按钮 -->
<view class="btn-section" v-if="currentStep === 2">
<button class="pay-btn" @click="handleBackToList">返回生活缴费</button>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive
} from 'vue'
import {
onShow,
onLoad
} from '@dcloudio/uni-app'
import {getSelectByDeviceCode} from '@/pageSubPack/api/apiSub.js'
// 响应式数据
const outstandingPayment = ref(true)
const storageValue = uni.getStorageSync('arrearsPayment')
const paymentItem = reactive(typeof storageValue === 'object' && storageValue !== null ? storageValue : {})
const currentStep = ref(1)
const payAmount = ref('0.00')
const arrearsAmount = ref('0.00')
// 页面加载时初始化数据
onLoad(() => {
initPaymentData()
})
onShow(() => {
getBalance()
})
// 初始化缴费数据
const initPaymentData = () => {
if (paymentItem) {
arrearsAmount.value = Math.abs(paymentItem.balance || 0).toFixed(2)
payAmount.value = Math.abs(paymentItem.balance || 0)
outstandingPayment.value = paymentItem.balance < 0
}
}
// 查询单条余额(水电费通用),从小程序返回后刷新
const getBalance = () => {
if (!paymentItem.deviceCode) return
const deviceTypeMap = { '电费': '1', '水费': '2' }
getSelectByDeviceCode({
deviceCode: paymentItem.deviceCode,
type: deviceTypeMap[paymentItem.type]
}).then(res => {
const data = res.data
if (data && data.balance !== undefined && data.balance !== null) {
arrearsAmount.value = Math.abs(data.balance || 0).toFixed(2)
payAmount.value = Math.abs(data.balance || 0)
outstandingPayment.value = data.balance < 0
}
}).catch(err => {
console.log('查询余额失败', err)
})
}
// 返回
const goBack = () => {
uni.navigateBack();
}
// 立即缴费 — 通过微信拉起日照银行小程序进行支付
const handlePay = () => {
const amount = arrearsAmount.value
try {
uni.showLoading({ title: '加载中...' })
// 设备类型映射:电费→1(电表),水费→2(水表)
const deviceTypeMap = { '电费': '1', '水费': '2' }
const mappedDeviceType = deviceTypeMap[paymentItem.type] || paymentItem.deviceType || ''
// 直接从本地参数拼 path 查询字符串传给支付小程序,不走接口
const token = uni.getStorageSync('token') || ''
const params = {
acct: uni.getStorageSync('userId'),
orderType: '20',
deviceCode: paymentItem.deviceCode || '',
deviceType: mappedDeviceType,
billId: paymentItem.billId || '',
residentUserId: uni.getStorageSync('userId'),
// goodName: paymentItem.name || switchStatus(paymentItem.type) || '',
// remark: '服务费',
token: token
}
const queryString = Object.keys(params).map(k =>
encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
).join('&')
uni.hideLoading()
console.log('传入中间小程序的path参数:', queryString)
// #ifdef APP-PLUS
// 原生App:通过微信SDK拉起小程序
plus.share.getServices((services) => {
const weixin = services.find(s => s.id === 'weixin')
if (!weixin) {
uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
return
}
if (!weixin.launchMiniProgram) {
uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
return
}
weixin.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 2, // 0 正式版 / 1开发版 / 2体验版
path: 'pages/arrearsPayment/arrearsPayment?' + queryString,
}, (res) => {
console.log('回调---回调结果(App)', JSON.stringify(res))
}, (err) => {
console.error('err---拉起失败(App)', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
} catch (err) {
uni.hideLoading()
console.error('拉起支付小程序异常', err)
uni.showToast({ title: '请求异常,请重试', icon: 'none' })
}
}
// 缴费成功推送通知
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
getPushSingle({
userId: userId,
channel: 'resident',
title: '缴费成功',
content: `您已成功缴纳${switchStatus(paymentItem.type) || ''}费用 ¥${arrearsAmount.value || payAmount.value}元`
}).catch(err => {
console.log('推送发送失败', err)
})
}
}
// 返回缴费列表
const handleBackToList = () => {
uni.navigateBack();
}
// 类型码转中文名展示
const switchStatus = (type) => {
const typeMap = {
'0': '物业费',
'1': '车位费',
'2': '垃圾处理费'
}
return typeMap[type] || type
}
</script>
<style lang="scss">
page {
background-color: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff */
;
}
/* 1. 物业费详情页 */
.pay-page {
width: 100%;
/* min-height: 100vh; */
/* background-color: #fff; */
}
.amount-section {
padding: 60rpx 0;
text-align: center;
}
.amount-text {
font-size: 56rpx;
font-weight: 600;
}
.outstandingPaymentClass {
color: #E34D59;
}
.paymentClass {
color: #00aaff;
}
.info-section {
padding-top: 20rpx;
/* margin-bottom: 40rpx; */
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 80rpx;
}
.info-label {
font-size: 30rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
color: #333;
font-weight: 600;
}
.info-payAmountItem {
height: 100rpx;
line-height: 100rpx;
margin-bottom: 60rpx;
}
.info-payAmount {
font-size: 50rpx;
color: #333;
font-weight: 600;
}
.input-section {
/* padding: 030rpx; */
}
.amount-input {
width: 100%;
height: 100rpx;
font-size: 32rpx;
line-height: 100rpx;
border: none;
outline: none;
margin-right: 10rpx;
}
.btn-section {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
}
.pay-btn {
width: 100%;
height: 100rpx;
line-height: 100rpx;
background-color: #007aff;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
outline: none;
}
/* 2. 支付密码弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
.password-modal {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: 40rpx;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.close-icon {
font-size: 44rpx;
width: 80rpx;
height: 100rpx;
display: flex;
align-items: center;
}
.modal-title {
font-size: 32rpx;
font-weight: 500;
color: #000;
}
.header-right {
width: 80rpx;
}
.pay-info {
padding: 40rpx 30rpx;
text-align: center;
}
.merchant-name {
display: block;
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.pay-amount {
font-size: 64rpx;
font-weight: 500;
color: #000;
}
.pay-way {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.way-label {
font-size: 30rpx;
color: #666;
}
.way-content {
display: flex;
align-items: center;
}
.bank-icon {
width: 32rpx;
height: 32rpx;
margin-right: 6rpx;
}
.bank-name {
font-size: 30rpx;
color: #333;
margin-right: 16rpx;
}
.arrow-icon {
font-size: 28rpx;
color: #999;
}
/* 银行卡列表 */
.bank-list {
padding: 030rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.bank-item {
display: flex;
align-items: center;
height: 100rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.bank-item:last-child {
border-bottom: none;
}
.bank-item-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.bank-item-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
.check-icon {
font-size: 36rpx;
color: #007aff;
}
/* 密码输入框 */
.password-input {
display: flex;
justify-content: space-between;
padding: 40rpx 30rpx;
}
.password-item {
width: 100rpx;
height: 100rpx;
/* border: 2rpx solid #dcdcdc; */
background-color: #efefef;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.password-dot {
width: 24rpx;
height: 24rpx;
background-color: #000;
border-radius: 50%;
}
/* 数字键盘 */
.keyboard {
width: 100%;
}
.keyboard-row {
display: flex;
}
.keyboard-item {
flex: 1;
height: 54px;
line-height: 54px;
text-align: center;
font-size: 48rpx;
color: #000;
border: 2rpx solid #f0f0f0;
background-color: #fff;
}
.keyboard-item:active {
background-color: #e5e5e5;
}
.keyboard-item.empty {
background-color: #f5f5f5;
border: none;
}
.keyboard-item.delete {
background-color: #f5f5f5;
border: none;
}
/* 3. 缴费成功页 */
/* 全局容器 */
.container {
display: flex;
flex-direction: column;
align-items: center;
}
/* 成功图标 */
.success-icon {
width: 220rpx;
height: 220rpx;
border-radius: 50%;
background-color: #1677ff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 200rpx;
margin-bottom: 60rpx;
}
.icon-check {
color: #fff;
font-size: 130rpx;
font-weight: bold;
line-height: 1;
}
/* 主标题 */
.title {
font-size: 40rpx;
font-weight: bold;
color: #000;
margin-bottom: 20rpx;
}
/* 提示文案 */
.desc {
font-size: 32rpx;
color: #666;
line-height: 1.6;
text-align: center;
margin-bottom: 150rpx;
}
/* 支付方式选择 */
.pay-method-section {
margin-top: 30rpx;
padding: 0 20rpx;
}
.pay-method-title {
font-size: 30rpx;
color: #333;
font-weight: 600;
margin-bottom: 20rpx;
display: block;
}
.pay-method-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.pay-method-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background: #fff;
border-radius: 12rpx;
border: 2rpx solid #ededed;
}
.pay-method-item.active {
border-color: #1677ff;
background: #f0f7ff;
}
.pay-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.pay-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
.pay-check {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #1677ff;
position: relative;
}
.pay-check::after {
content: '';
position: absolute;
left: 10rpx;
top: 6rpx;
width: 8rpx;
height: 14rpx;
border: 2rpx solid #fff;
border-left: none;
border-top: none;
transform: rotate(45deg);
}
/* 核心:欠费金额行样式 */
.arrears-row {
display: flex;
align-items: center;
justify-content: space-between;
/* padding:30rpx; */
margin-bottom: 60rpx;
}
.arrears-text {
font-size: 28rpx;
color: #999;
}
.auto-fill-btn {
font-size: 28rpx;
color: #007aff;
/* 蓝色,匹配设计图 */
font-weight: 500;
padding: 10rpx 0;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,464 @@
<template>
<view class="wrap">
<scroll-view class="page" scroll-y @scrolltolower="onLoadMore">
<view class="empty-state" v-if="!loading && billList.length === 0">
<text class="empty-text">暂无账单数据</text>
</view>
<up-collapse v-if="billList.length" :border="false">
<up-collapse-item
v-for="(item, index) in billList"
:key="index"
:name="String(index)"
>
<!-- 标题:勾选框 + 月份 -->
<template #title>
<view class="collapse-header">
<view class="check-circle" :class="{ checked: item.selected }" @click.stop="toggleSelect(index)">
<text v-if="item.selected" class="check-icon">✓</text>
</view>
<text class="month-text">{{ item.month }}</text>
</view>
</template>
<!-- 右侧金额 -->
<template #value>
<text class="price-text">¥{{ item.totalAmount }}</text>
</template>
<!-- 展开内容:每条明细 -->
<view class="collapse-body">
<view class="detail-row fee-row" v-for="(detail, di) in item.details" :key="di">
<text class="detail-label">{{ detail.name }}</text>
<text class="detail-value">¥{{ detail.amount }}</text>
</view>
<view class="detail-row meta-row" v-if="item.unitNo || item.date">
<text class="detail-label">{{ item.unitNo }}</text>
<text class="detail-value">{{ item.date }}</text>
</view>
<view class="detail-row meta-row">
<text class="detail-label">原金额:¥{{ item.totalAmount }}</text>
<text class="detail-value">优惠:¥{{ formatNum(item.discount) }}</text>
</view>
</view>
</up-collapse-item>
</up-collapse>
<!-- 加载状态 -->
<view class="load-more-status" v-if="loadingMore">
<text class="loading-text">加载中...</text>
</view>
<view class="load-more-status" v-else-if="!hasMore && billList.length >= pageSize">
<text class="no-more-text">— 没有更多了 —</text>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="billList.length">
<view class="select-all" @click="toggleSelectAll">
<view class="check-circle" :class="{ checked: isAllSelected }">
<text v-if="isAllSelected" class="check-icon">✓</text>
</view>
<text class="select-all-text">全选</text>
</view>
<view class="total-box">
<text class="total-label">合计:</text>
<text class="total-price">¥{{ selectedTotal.toFixed(2) }}</text>
</view>
<button class="pay-btn" :disabled="!canPay" @click="handleBatchPay">立即缴费</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import { getTopUpDetailsByMonth } from '/pageSubPack/api/apiSub.js'
const loading = ref(false)
const billList = ref([])
const pageNum = ref(1)
const pageSize = ref(10)
const total = ref(0)
const hasMore = ref(true)
const loadingMore = ref(false)
// 从 paymentIndex 传来的数据中取 billResidentId
const cacheItem = uni.getStorageSync('arrearsPayment') || {}
const billResidentId = ref(cacheItem.billResidentId || cacheItem.id || '')
const isFirstShow = ref(true)
onLoad(() => {
fetchBills()
})
onShow(() => {
// 首次进入由 onLoad 拉取,避免重复请求
if (isFirstShow.value) {
isFirstShow.value = false
return
}
fetchBills()
})
onPullDownRefresh(() => {
fetchBills()
})
// 拉取按月汇总账单
const fetchBills = async (isLoadMore = false) => {
if (!billResidentId.value) return
if (isLoadMore) {
if (!hasMore.value || loadingMore.value) return
pageNum.value++
loadingMore.value = true
} else {
pageNum.value = 1
hasMore.value = true
loading.value = true
}
try {
const res = await getTopUpDetailsByMonth({
billResidentId: billResidentId.value,
villageId: uni.getStorageSync('currentVillageId') || '',
pageNum: pageNum.value,
pageSize: pageSize.value
})
const rows = res.rows || []
const typeNameMap = { '0': '物业管理服务费', '1': '车位管理费', '2': '生活垃圾处理费' }
const mapped = rows.map(r => {
const firstDetail = (r.details && r.details.length) ? r.details[0] : {}
const restDetails = (r.details || []).slice(1)
return {
month: r.month || '',
totalAmount: formatNum(r.totalAmount),
discount: 0,
selected: false,
periodDisplay: firstDetail.periodDisplay || '',
unitNo: cacheItem.unitNo || cacheItem.fullAddress || '',
date: r.month || '',
details: restDetails.map(d => ({
name: typeNameMap[d.type] || d.name || '缴费费用',
amount: formatNum(d.amount),
id: d.id,
payStatus: d.payStatus || '0'
})),
billIds: (r.details || []).map(d => d.id).filter(Boolean)
}
})
if (isLoadMore) {
billList.value = [...billList.value, ...mapped]
} else {
billList.value = mapped
}
total.value = res.total || 0
hasMore.value = billList.value.length < total.value
} catch (e) {
if (isLoadMore) pageNum.value--
console.error('获取账单失败', e)
} finally {
loading.value = false
loadingMore.value = false
uni.stopPullDownRefresh()
}
}
const onLoadMore = () => {
fetchBills(true)
}
const formatNum = (val) => {
return (Number(val || 0)).toFixed(2)
}
const toggleSelect = (index) => {
const current = billList.value[index]
if (!current.selected) {
// 选中:检查前面所有项是否都已选中,不允许跳过
for (let i = 0; i < index; i++) {
if (!billList.value[i].selected) {
uni.showToast({ title: '请按顺序从第一个月开始选择', icon: 'none' })
return
}
}
billList.value[index].selected = true
} else {
// 取消选中:同时取消该项及后面所有已选中的
for (let i = index; i < billList.value.length; i++) {
billList.value[i].selected = false
}
}
}
const isAllSelected = computed(() =>
billList.value.length > 0 && billList.value.every(m => m.selected)
)
const toggleSelectAll = () => {
const val = !isAllSelected.value
billList.value.forEach(m => { m.selected = val })
}
const selectedTotal = computed(() =>
billList.value.filter(m => m.selected).reduce((sum, m) => sum + Number(m.totalAmount || 0), 0)
)
const canPay = computed(() => {
const selected = billList.value.filter(m => m.selected)
if (!selected.length) return false
// 必须从第一个开始且连续
return billList.value[0].selected && selected.length > 0
})
// 批量缴费
const handleBatchPay = () => {
const selected = billList.value.filter(m => m.selected)
if (!selected.length) return
const allIds = selected.flatMap(m => m.billIds)
const token = uni.getStorageSync('token') || ''
const userId = uni.getStorageSync('userId') || ''
const query = [
['acct', userId],
['orderType', '10'],
['billIds', allIds.join(',')],
// ['deviceType', '3'],
['residentUserId', userId],
['token', token]
].map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&')
console.log('往小程序传递的参数',query)
// #ifdef APP-PLUS
uni.showLoading({ title: '加载中...' })
plus.share.getServices((services) => {
const wx = services.find(s => s.id === 'weixin')
uni.hideLoading()
if (!wx) return uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
if (!wx.launchMiniProgram) return uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
wx.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 0,//0正式版,1开发版,2体验版
path: 'pages/arrearsPayment/arrearsPayment?' + query,
}, (res) => {
console.log('小程序回调:', JSON.stringify(res))
}, (err) => {
console.error('拉起失败:', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
}
</script>
<style lang="scss">
page {
background-color: #f5f5f5;
}
</style>
<style scoped>
.wrap {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
}
.page {
flex: 1;
overflow-y: auto;
padding: 20rpx 30rpx;
box-sizing: border-box;
padding-bottom: 140rpx;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
}
.empty-text {
font-size: 30rpx;
color: #ccc;
}
.load-more-status {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.loading-text {
font-size: 26rpx;
color: #999;
}
.no-more-text {
font-size: 26rpx;
color: #ccc;
}
/* ---- 折叠面板 ---- */
::v-deep .u-collapse-item {
margin-bottom: 20rpx;
border-radius: 8rpx;
overflow: hidden;
background: #fff;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
::v-deep .u-collapse-item__content {
background: #F5FAFF;
padding: 0;
}
::v-deep .u-cell {
padding: 20rpx 18rpx !important;
}
/* 标题区 */
.collapse-header {
display: flex;
align-items: center;
gap: 16rpx;
}
.check-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 3rpx solid #ddd;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.check-circle.checked {
background-color: #1677ff;
border-color: #1677ff;
}
.check-icon {
color: #fff;
font-size: 24rpx;
font-weight: bold;
line-height: 1;
}
.month-text {
font-size: 32rpx;
font-weight: 600;
color: #222;
}
/* 金额 */
.price-text {
font-size: 32rpx;
font-weight: 700;
color: #FF8F40;
}
/* 展开内容 */
.collapse-body {
padding: 20rpx 24rpx 30rpx;
margin: 0 24rpx;
background: #F5FAFF;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14rpx 0;
}
.detail-label {
font-size: 28rpx;
}
.detail-value {
font-size: 28rpx;
}
/* 明细费用名 - 深色 */
.fee-row .detail-label,
.fee-row .detail-value {
color: #666666;
}
/* 日期/原金额/优惠 - 浅色 */
.meta-row .detail-label,
.meta-row .detail-value {
color: #999999;
}
/* ---- 底部操作栏 ---- */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.06);
z-index: 10;
}
.select-all {
display: flex;
align-items: center;
gap: 10rpx;
flex-shrink: 0;
margin-right: 16rpx;
}
.select-all-text {
font-size: 26rpx;
color: #666;
}
.total-box {
flex: 1;
display: flex;
align-items: baseline;
justify-content: center;
}
.total-label {
font-size: 26rpx;
color: #666;
}
.total-price {
font-size: 36rpx;
font-weight: 700;
color: #FF8F40;
}
.pay-btn {
flex-shrink: 0;
width: 180rpx;
height: 72rpx;
line-height: 72rpx;
background-color: #1677ff;
color: #fff;
font-size: 28rpx;
font-weight: 500;
border-radius: 8rpx;
border: none;
padding: 0;
}
.pay-btn[disabled] {
background-color: #ccc;
opacity: 0.7;
}
</style>

View File

@@ -210,44 +210,31 @@ const canPay = computed(() => {
return billList.value[0].selected && selected.length > 0 return billList.value[0].selected && selected.length > 0
}) })
// 批量缴费 // 批量缴费:跳转银联收银台选择支付方式
const handleBatchPay = () => { const handleBatchPay = () => {
const selected = billList.value.filter(m => m.selected) const selected = billList.value.filter(m => m.selected)
if (!selected.length) return if (!selected.length) return
const allIds = selected.flatMap(m => m.billIds) const allIds = selected.flatMap(m => m.billIds).filter(Boolean)
const token = uni.getStorageSync('token') || '' if (!allIds.length) {
uni.showToast({ title: '账单信息异常,请稍后重试', icon: 'none' })
return
}
const userId = uni.getStorageSync('userId') || '' const userId = uni.getStorageSync('userId') || ''
const query = [ // 收银台页面(yinlianPayTn)从 payData 中取参数
['acct', userId], uni.setStorageSync('payData', {
['orderType', '10'], ...cacheItem,
['billIds', allIds.join(',')], amount: selectedTotal.value.toFixed(2),
// ['deviceType', '3'], billIds: allIds,
['residentUserId', userId], type: cacheItem.type || cacheItem.name || '物业费',
['token', token] residentUserId: userId
].map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&') })
console.log('往小程序传递的参数',query)
uni.navigateTo({
// #ifdef APP-PLUS url: '/pageSubPack/payment-list/yinlianPayTn'
uni.showLoading({ title: '加载中...' })
plus.share.getServices((services) => {
const wx = services.find(s => s.id === 'weixin')
uni.hideLoading()
if (!wx) return uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
if (!wx.launchMiniProgram) return uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
wx.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 0,//0正式版,1开发版,2体验版
path: 'pages/arrearsPayment/arrearsPayment?' + query,
}, (res) => {
console.log('小程序回调:', JSON.stringify(res))
}, (err) => {
console.error('拉起失败:', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
}) })
// #endif
} }
</script> </script>

View File

@@ -228,7 +228,8 @@ const goToBill = (item) => {
}) })
} else { } else {
uni.navigateTo({ uni.navigateTo({
url: '/pageSubPack/payment-list/billPayment' url: '/pageSubPack/payment-list/billPay-yidang'
// url: '/pageSubPack/payment-list/billPayment'
}) })
} }
} }

View File

@@ -50,11 +50,16 @@
<script setup> <script setup>
import { ref, onUnmounted } from 'vue' import { ref, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import { getUnionPayTn } from '/pageSubPack/api/apiSub.js' import { getUnionPayTn } from '/pageSubPack/api/apiSub.js'
import { getPushSingle } from '/pageSubPack/api/api.js' import { getPushSingle } from '/pageSubPack/api/api.js'
// 支付宝和微信支付 // 银联商务全民付为 App 原生 UTS 插件,不支持小程序/H5,必须条件编译引入,否则其他端构建失败
// #ifdef APP-PLUS
import { upPayWithChannel, upPayRegistry, setPayEnviroment } from '@/uni_modules/xh-uppay'
// #endif
// 银联商务全民付收银台:微信 / 支付宝
const amount = ref('0.00') const amount = ref('0.00')
const countdownTime = ref('10:00') const countdownTime = ref('10:00')
@@ -92,7 +97,6 @@
} }
const selectedType = ref('wx') const selectedType = ref('wx')
const goBack = () => uni.navigateBack()
const selectPayType = (type) => { const selectPayType = (type) => {
selectedType.value = type selectedType.value = type
@@ -101,28 +105,69 @@
// #endif // #endif
} }
// 银联商务渠道号:01 微信原生支付 / 02 支付宝支付 / 03 支付宝小程序 / 11 支付宝2.0
// 渠道号必须与后端下单的产品对应(响应里的 targetSys),对不上会静默失败:
// 微信 targetSys = WXPay → 01
// 支付宝 targetSys = Alipay 2.0 → 11(插件 Android 端 '11' 分支是空壳,未实现)
// 支付宝 targetSys = 支付宝小程序 → 03(需后端改下这个小程序产品才会生效)
const CHANNEL_MAP = { wx: '01', alipay: '03' }
// 传给后端的支付方式,沿用原有枚举 0=微信 / 1=支付宝
const PAY_TYPE_MAP = { wx: '0', alipay: '1' }
// 银联环境:0 正式 / 1 测试。必须与后端下单时所用的银联环境一致,
// 这是后端的配置项、不是前端构建环境;联调报「交易不存在/签名错误」多半是这里对不上。
// 注意:这里只对 iOS 生效,Android 要改插件 utssdk/app-android/AndroidManifest.xml 的 CurrentEnvironment(PROD/TEST)。
const UNIONPAY_ENV = process.env.NODE_ENV === 'development' ? 1 : 0
// 以下常量只在 iOS 生效(Android 的 upPayRegistry 是空实现),且每一项都必须与
// xh-uppay/utssdk/app-ios/info.plist 里 CFBundleURLTypes 的取值完全一致,
// 否则 iOS 上支付完成后回不到本应用。
const WX_APPID = 'wx84b02b2ec80715f4' // 需同步改 info.plist 的 weixin scheme(现在是 demo 值)
// 存疑:暂用项目里微信分享的 Universal Link,但银联商务支付链路可能要求用他们备案的域名,需与其确认
const WX_UNIVERSAL_LINK = 'https://verification.ckdzkj.com/uni-universallinks/__UNI__29C3D60/'
const SCHEME_CLOUD = 'unifyPayDemo' // info.plist 中 ucloud(云闪付回调,暂不用云闪付)
const SCHEME_ALI_MINI = 'qmfpppay' // info.plist 中 aliminipay(支付宝小程序回调)
const SCHEME_ALI = 'umsunifyalisafepay' // info.plist 中 alisafepay(支付宝回调)
const handlePayClick = () => { const handlePayClick = () => {
handlePay() handlePay()
} }
// 账单明细 id:接口要求 string,多个账单逗号拼接,兼容单个 billId 的旧场景
const toBillIds = (billIds, billId) => {
if (Array.isArray(billIds) && billIds.length) {
return billIds.filter(Boolean).join(',')
}
if (typeof billIds === 'string' && billIds) {
return billIds
}
return billId ? String(billId) : ''
}
const handlePay = async () => { const handlePay = async () => {
try { try {
uni.showLoading({ title: '发起支付中' }) uni.showLoading({ title: '发起支付中' })
const payTypeMap = { wx: '0', alipay: '1' }
const backendPayType = payTypeMap[selectedType.value] || '1'
const res = await getUnionPayTn({ const billIds = toBillIds(payData.value.billIds, payData.value.billId)
amount: amount.value, if (!billIds) {
uni.hideLoading()
uni.showToast({ title: '账单信息异常,请返回重新选择', icon: 'none' })
return
}
// deviceCode/deviceType 仅水电表充值传;无值即物业费/停车费/垃圾处理费
const payload = {
amount: Number(amount.value),
orderDesc: (payData.value.type || '缴费') + '缴费', orderDesc: (payData.value.type || '缴费') + '缴费',
residentUserId: uni.getStorageSync('userId'), residentUserId: payData.value.residentUserId || uni.getStorageSync('userId'),
payType: backendPayType, payType: PAY_TYPE_MAP[selectedType.value],
deviceCode: payData.value.deviceCode || '', billIds
deviceType: '2', }
chargeTypeId: payData.value.chargeTypeId, if (payData.value.deviceCode) payload.deviceCode = payData.value.deviceCode
chargeItemId: payData.value.chargeItemId, if (payData.value.deviceType) payload.deviceType = payData.value.deviceType
billId: payData.value.billId,
houseId: payData.value.houseId const res = await getUnionPayTn(payload)
})
console.log('支付接口返回:', JSON.stringify(res.data)) console.log('支付接口返回:', JSON.stringify(res.data))
uni.hideLoading() uni.hideLoading()
@@ -131,15 +176,20 @@
return return
} }
const payType = selectedType.value // 后端下发银联商务的 appPayRequest,整串 JSON 原样交给 SDK,不要拆开解析
const data = res.data const data = res.data
const payDataStr = typeof data === 'string'
? data
: (data.appPayRequest || data.appRequest || '')
if (payType === 'wx') { if (!payDataStr || typeof payDataStr !== 'string' || !payDataStr.trim().startsWith('{')) {
invokeWxPay(data.wx || data.wxPayInfo || data) console.error('未获取到有效的 appPayRequest,后端返回:', JSON.stringify(data))
} else if (payType === 'alipay') { uni.showToast({ title: '支付参数异常,请联系客服', icon: 'none' })
invokeAlipay(data.alipay || data.aliPayInfo || data) return
} }
invokeUnionPay(payDataStr, CHANNEL_MAP[selectedType.value])
} catch (err) { } catch (err) {
uni.hideLoading() uni.hideLoading()
console.log('支付异常', err) console.log('支付异常', err)
@@ -147,49 +197,96 @@
} }
} }
// 微信支付 // 银联商务综合支付:拉起微信 / 支付宝完成付款
const invokeWxPay = (data) => { const invokeUnionPay = (payData, channel) => {
const orderInfo = { // #ifdef APP-PLUS
appid: data.appId, ensureRegistered()
partnerid: data.partnerId || data.partnerid || '', paying = true
prepayid: data.prepayId || data.prepayid || (data.package || '').replace('prepay_id=', ''), upPayWithChannel({ payData, channel }, (code, info) => {
package: 'Sign=WXPay', handlePayResult(String(code), info)
noncestr: data.nonceStr, })
timestamp: data.timeStamp, // #endif
sign: data.paySign
// #ifndef APP-PLUS
uni.showToast({ title: '当前端暂不支持银联支付', icon: 'none' })
// #endif
}
// 注册和支付环境设置都只对 iOS 生效;Android 调 setPayEnviroment 只会刷警告,
// Android 的环境要改插件 utssdk/app-android/AndroidManifest.xml 里的 CurrentEnvironment。
let registered = false
const ensureRegistered = () => {
if (registered) return
registered = true
// #ifdef APP-PLUS
if (uni.getSystemInfoSync().platform === 'ios') {
setPayEnviroment(UNIONPAY_ENV)
upPayRegistry({
appId: WX_APPID,
link: WX_UNIVERSAL_LINK,
cloudUrl: SCHEME_CLOUD,
aliminipayUrl: SCHEME_ALI_MINI,
aliUrl: SCHEME_ALI
})
} }
// #endif
uni.requestPayment({
provider: 'wxpay',
orderInfo: orderInfo,
success: (res) => {
console.log('微信支付成功', res)
paySuccess()
},
fail: (err) => {
console.error('微信支付失败或取消', err)
uni.showToast({ title: '支付取消或失败', icon: 'none' })
}
})
} }
// 支付宝支付 let paying = false // 支付请求是否已发出
const invokeAlipay = (data) => { let payDone = false // 是否已给出结果提示,避免重复
const orderInfo = data.orderInfo || data
uni.requestPayment({ // Android 端插件不触发回调,用户从微信/支付宝切回来时靠 onShow 兜底;
provider: 'alipay', // iOS 有回调,交给 handlePayResult 处理,这里不插手。
orderInfo: orderInfo, onShow(() => {
success: (res) => { if (!paying) return
console.log('支付宝支付成功', res) if (uni.getSystemInfoSync().platform === 'android') {
paySuccess() paying = false
}, notifyPayPending()
fail: (err) => { }
console.error('支付宝支付失败或取消', err) })
uni.showToast({ title: '支付取消或失败', icon: 'none' })
} // 结果码见 xh-uppay readme:0000 只表示「支付请求发送成功」,不代表付款成功,
}) // 真实结果以银联商务异步通知到后端的结果为准。所以这里不做成功判定,
// 统一引导用户回账单列表看实际状态。
const handlePayResult = (code, info) => {
console.log('银联支付回调', code, JSON.stringify(info))
paying = false
switch (code) {
case '0000':
case '2001': // 订单处理中,结果未知(有可能已支付成功)
notifyPayPending()
break
case '1000':
payDone = true
uni.showToast({ title: '支付已取消', icon: 'none' })
break
case '1001':
payDone = true
uni.showToast({ title: '支付参数错误', icon: 'none' })
break
case '1002':
payDone = true
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
break
case '1003':
payDone = true
uni.showToast({ title: '未安装对应的支付客户端', icon: 'none' })
break
default:
payDone = true
uni.showToast({ title: (info && info.resultMsg) || '支付失败', icon: 'none' })
}
} }
const notifyPayPending = () => {
if (payDone) return
payDone = true
uni.showToast({ title: '支付结果确认中,请稍后在账单中查看', icon: 'none' })
setTimeout(() => uni.navigateBack(), 2000)
}
// 支付成功流转:等后端提供「订单查询」接口后,查单确认已支付时再调用这里。
// 目前 SDK 的结果码不足以判定付款成功,所以暂时没有调用点。
const paySuccess = () => { const paySuccess = () => {
sendPaySuccessPush() sendPaySuccessPush()
uni.showToast({ title: '支付成功', icon: 'success' }) uni.showToast({ title: '支付成功', icon: 'success' })

View File

@@ -461,6 +461,13 @@
"navigationBarTitleText": "缴费账单", "navigationBarTitleText": "缴费账单",
"enablePullDownRefresh": true "enablePullDownRefresh": true
} }
},
{
"path": "billPay-yidang",
"style": {
"navigationBarTitleText": "缴费账单",
"enablePullDownRefresh": true
}
} }
] ]