619 lines
17 KiB
Vue
619 lines
17 KiB
Vue
<template>
|
||
<div class="payment-page">
|
||
<h1 class="page-title">在线缴费</h1>
|
||
|
||
<!-- 缴费人信息与记录按钮 -->
|
||
<div class="user-info-bar bg-white p-3 flex justify-between align-center">
|
||
<div>
|
||
<span>缴费人: </span>
|
||
<span class="text-primary">{{ payerName }}</span>
|
||
</div>
|
||
<van-button icon="records" size="small" type="info" plain @click="goToRecords">缴费记录</van-button>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- 账单列表 -->
|
||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||
<van-list
|
||
v-model="loading"
|
||
:finished="finished"
|
||
finished-text="没有更多了"
|
||
@load="onLoad"
|
||
>
|
||
<!-- 没有账单数据时显示的内容 -->
|
||
<div v-if="buildingGroups.length === 0 && !loading" class="empty-data">
|
||
<van-empty image="search" description="暂无账单数据" />
|
||
</div>
|
||
|
||
<!-- 账单列表 (楼宇分组) -->
|
||
<div v-for="building in buildingGroups" :key="building.buildingId" class="bill-group">
|
||
<!-- 楼宇名称 (一级) -->
|
||
<div class="bill-group-header">
|
||
<!-- <div class="checkbox-wrapper" @click.stop="toggleBuildingCheck(building)">
|
||
<van-checkbox
|
||
:value="isBuildingChecked(building)"
|
||
/>
|
||
</div> -->
|
||
<div class="header-content" @click="toggleBuilding(building.buildingId)">
|
||
<span class="building-name">{{ building.buildingName }}</span>
|
||
<span class="building-info">
|
||
{{ building.totalCount }}项 | ¥{{ formatAmount(building.totalAmount) }}
|
||
</span>
|
||
<van-icon :name="buildingExpanded[building.buildingId] ? 'arrow-up' : 'arrow-down'" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-show="buildingExpanded[building.buildingId]">
|
||
<!-- 费用类型分组 (二级) -->
|
||
<div
|
||
v-for="feeType in building.feeTypeGroups"
|
||
:key="`${building.buildingId}_${feeType.feTypeName}`"
|
||
class="fee-type-group"
|
||
>
|
||
<div class="fee-type-header ml-4">
|
||
<!-- <div class="checkbox-wrapper ml-4" @click.stop="toggleFeeTypeCheck(building, feeType)">
|
||
<van-checkbox
|
||
:value="isFeeTypeChecked(building.buildingId, feeType)"
|
||
/>
|
||
</div> -->
|
||
<div class="header-content" @click="toggleFeeType(building.buildingId, feeType.feTypeName)">
|
||
<span class="fee-type-name">{{ feeType.feTypeName }}</span>
|
||
<span class="fee-type-info">
|
||
{{ feeType.totalCount }}项 | ¥{{ formatAmount(feeType.totalAmount) }}
|
||
</span>
|
||
<van-icon :name="feeTypeExpanded[`${building.buildingId}_${feeType.feTypeName}`] ? 'arrow-up' : 'arrow-down'" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-show="feeTypeExpanded[`${building.buildingId}_${feeType.feTypeName}`]">
|
||
<!-- 账单列表 (三级) -->
|
||
<div
|
||
v-for="bill in feeType.bills"
|
||
:key="bill.billId"
|
||
class="bill-item"
|
||
>
|
||
<div class="checkbox-wrapper ml-5" @click.stop="toggleCheck(bill)">
|
||
<van-checkbox
|
||
:value="isChecked(bill)"
|
||
/>
|
||
</div>
|
||
<div class="bill-content" @click="viewBillDetail(bill.billId)">
|
||
<div class="bill-info">
|
||
<div class="bill-remark">{{ bill.remark }}</div>
|
||
<div class="bill-date">应收日期: {{ bill.pybDt }}</div>
|
||
</div>
|
||
<div class="bill-amount">¥{{ formatAmount(bill.totalNeedAmount) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</van-list>
|
||
</van-pull-refresh>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<div class="footer-action-bar">
|
||
<div class="summary-info">
|
||
<div class="flex align-center">
|
||
<span class="ml-3">已选: {{ selectedBillsCount }}项</span>
|
||
</div>
|
||
<div class="total-amount">合计: <span class="text-danger">¥{{ formatAmount(selectedBillsTotalAmount) }}</span></div>
|
||
</div>
|
||
<div class="action-buttons">
|
||
<van-button
|
||
type="default"
|
||
size="normal"
|
||
class="mr-2"
|
||
@click="goToPrepay"
|
||
>预缴费</van-button>
|
||
<van-button
|
||
type="primary"
|
||
size="normal"
|
||
:disabled="selectedBillsCount === 0"
|
||
@click="showPaymentTypeDialog"
|
||
>去缴费</van-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支付方式选择 -->
|
||
<van-dialog
|
||
v-model="paymentTypeVisible"
|
||
title="选择支付方式"
|
||
:show-confirm-button="false"
|
||
:close-on-click-overlay="true"
|
||
>
|
||
<div class="payment-type-options">
|
||
<div class="payment-type-item">
|
||
<div class="radio-wrapper" @click.stop="selectPaymentType('personal')">
|
||
<van-radio :name="'personal'" v-model="paymentType">个对公缴费</van-radio>
|
||
</div>
|
||
<div class="payment-type-desc">个人对公司支付,通过微信/支付宝等方式</div>
|
||
</div>
|
||
<div class="payment-type-item">
|
||
<div class="radio-wrapper" @click.stop="selectPaymentType('enterprise')">
|
||
<van-radio :name="'enterprise'" v-model="paymentType">公对公缴费</van-radio>
|
||
</div>
|
||
<div class="payment-type-desc">公司对公司支付,通过银行转账方式</div>
|
||
</div>
|
||
</div>
|
||
<div class="dialog-footer">
|
||
<van-button type="default" block @click="cancelPaymentType">取消</van-button>
|
||
<van-button type="primary" block @click="confirmPaymentType">确定</van-button>
|
||
</div>
|
||
</van-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
|
||
import api from '@/api/payment';
|
||
|
||
export default {
|
||
name: 'PaymentIndex',
|
||
data() {
|
||
return {
|
||
refreshing: false,
|
||
loading: false,
|
||
finished: false,
|
||
pageNum: 1,
|
||
pageSize: 20,
|
||
|
||
buildingGroups: [], // 本地保存楼宇分组数据
|
||
totalAmount: 0, // 总金额
|
||
totalCount: 0, // 总数量
|
||
payerName: '', // 缴费人姓名
|
||
|
||
buildingExpanded: {}, // 楼宇展开状态
|
||
feeTypeExpanded: {}, // 费用类型展开状态
|
||
|
||
paymentTypeVisible: false, // 支付方式选择弹窗
|
||
paymentType: 'personal', // 支付方式
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters('payment', [
|
||
'selectedBillsCount',
|
||
'selectedBillsTotalAmount',
|
||
]),
|
||
...mapState('payment', ['selectedBills']),
|
||
},
|
||
watch: {
|
||
// 监听选中账单数量变化
|
||
selectedBillsCount(newVal) {
|
||
// 监听但不需要执行全选相关逻辑
|
||
},
|
||
},
|
||
created() {
|
||
this.loadData();
|
||
},
|
||
methods: {
|
||
...mapMutations('payment', [
|
||
'ADD_SELECTED_BILL',
|
||
'REMOVE_SELECTED_BILL',
|
||
'CLEAR_SELECTED_BILLS',
|
||
'SET_PAY_TYPE',
|
||
]),
|
||
...mapActions('payment', ['createPayOrder']),
|
||
|
||
// 格式化金额
|
||
formatAmount(amount) {
|
||
return parseFloat(amount).toFixed(2);
|
||
},
|
||
|
||
// 加载数据
|
||
async loadData() {
|
||
try {
|
||
this.loading = true;
|
||
const params = {
|
||
pageNum: this.pageNum,
|
||
pageSize: this.pageSize,
|
||
payerId: '1', // 直接使用默认业户ID
|
||
clearStatus: '4', // 待收款
|
||
};
|
||
|
||
const response = await api.getBills(params);
|
||
|
||
if (response.code === '0000000000000000') {
|
||
const data = response.data;
|
||
|
||
// 保存数据到本地
|
||
this.buildingGroups = data.buildingGroups || [];
|
||
this.totalAmount = data.totalAmount || 0;
|
||
this.totalCount = data.totalCount || 0;
|
||
this.payerName = data.payerName || '默认业户';
|
||
|
||
this.pageNum++;
|
||
this.finished = true; // 一次性加载所有数据
|
||
|
||
// 默认展开所有楼宇
|
||
this.buildingGroups.forEach(building => {
|
||
this.$set(this.buildingExpanded, building.buildingId, true);
|
||
});
|
||
} else {
|
||
this.$toast.fail(response.message || '获取账单列表失败');
|
||
}
|
||
} catch (error) {
|
||
this.$toast.fail('获取账单列表失败');
|
||
} finally {
|
||
this.loading = false;
|
||
this.refreshing = false;
|
||
}
|
||
},
|
||
|
||
// 下拉刷新
|
||
onRefresh() {
|
||
this.resetData();
|
||
this.loadData();
|
||
},
|
||
|
||
// 上拉加载更多
|
||
onLoad() {
|
||
this.loadData();
|
||
},
|
||
|
||
// 重置数据
|
||
resetData() {
|
||
this.pageNum = 1;
|
||
this.finished = false;
|
||
this.CLEAR_SELECTED_BILLS();
|
||
},
|
||
|
||
// 切换楼宇展开状态
|
||
toggleBuilding(buildingId) {
|
||
this.$set(this.buildingExpanded, buildingId, !this.buildingExpanded[buildingId]);
|
||
},
|
||
|
||
// 切换费用类型展开状态
|
||
toggleFeeType(buildingId, feTypeName) {
|
||
const key = `${buildingId}_${feTypeName}`;
|
||
this.$set(this.feeTypeExpanded, key, !this.feeTypeExpanded[key]);
|
||
},
|
||
|
||
// 判断账单是否被选中
|
||
isChecked(bill) {
|
||
return this.selectedBills.some(item => item.billId === bill.billId);
|
||
},
|
||
|
||
// 判断费用类型是否被选中(全部子账单被选中)
|
||
isFeeTypeChecked(buildingId, feeType) {
|
||
if (feeType.bills.length === 0) return false;
|
||
return feeType.bills.every(bill => this.isChecked(bill));
|
||
},
|
||
|
||
// 判断楼宇是否被选中(全部费用类型被选中)
|
||
isBuildingChecked(building) {
|
||
if (building.feeTypeGroups.length === 0) return false;
|
||
return building.feeTypeGroups.every(feeType =>
|
||
this.isFeeTypeChecked(building.buildingId, feeType)
|
||
);
|
||
},
|
||
|
||
// 切换费用类型选中状态
|
||
toggleFeeTypeCheck(building, feeType) {
|
||
const checked = this.isFeeTypeChecked(building.buildingId, feeType);
|
||
|
||
if (checked) {
|
||
// 如果已选中,则移除该费用类型下的所有账单
|
||
const billIdsToRemove = feeType.bills.map(bill => bill.billId);
|
||
billIdsToRemove.forEach(billId => {
|
||
this.REMOVE_SELECTED_BILL(billId);
|
||
});
|
||
} else {
|
||
// 如果未选中,则选择该费用类型下的第一条账单
|
||
if (feeType.bills.length > 0) {
|
||
const bill = feeType.bills[0];
|
||
// 确保账单对象包含费用类型名称和楼宇ID信息
|
||
bill.feeTypeName = feeType.feTypeName;
|
||
bill.buildingId = building.buildingId;
|
||
this.ADD_SELECTED_BILL(bill);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 切换楼宇选中状态
|
||
toggleBuildingCheck(building) {
|
||
const checked = this.isBuildingChecked(building);
|
||
|
||
if (checked) {
|
||
// 如果已选中,则移除该楼宇下的所有账单
|
||
building.feeTypeGroups.forEach(feeType => {
|
||
const billIdsToRemove = feeType.bills.map(bill => bill.billId);
|
||
billIdsToRemove.forEach(billId => {
|
||
this.REMOVE_SELECTED_BILL(billId);
|
||
});
|
||
});
|
||
} else {
|
||
// 如果未选中,则选择每个费用类型下的第一条账单
|
||
building.feeTypeGroups.forEach(feeType => {
|
||
if (feeType.bills.length > 0) {
|
||
const bill = feeType.bills[0];
|
||
// 确保账单对象包含费用类型名称和楼宇ID信息
|
||
bill.feeTypeName = feeType.feTypeName;
|
||
bill.buildingId = building.buildingId;
|
||
this.ADD_SELECTED_BILL(bill);
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
// 切换账单选中状态
|
||
toggleCheck(bill) {
|
||
// 确保账单对象包含所需信息
|
||
const feeType = this.findFeeTypeByBill(bill);
|
||
const building = this.findBuildingByBill(bill);
|
||
|
||
if (feeType && building) {
|
||
bill.feeTypeName = feeType.feTypeName;
|
||
bill.buildingId = building.buildingId;
|
||
}
|
||
|
||
if (this.isChecked(bill)) {
|
||
this.REMOVE_SELECTED_BILL(bill.billId);
|
||
} else {
|
||
this.ADD_SELECTED_BILL(bill);
|
||
}
|
||
},
|
||
|
||
// 查找账单所属的费用类型
|
||
findFeeTypeByBill(bill) {
|
||
for (const building of this.buildingGroups) {
|
||
for (const feeType of building.feeTypeGroups) {
|
||
if (feeType.bills.some(b => b.billId === bill.billId)) {
|
||
return feeType;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
|
||
// 查找账单所属的楼宇
|
||
findBuildingByBill(bill) {
|
||
for (const building of this.buildingGroups) {
|
||
for (const feeType of building.feeTypeGroups) {
|
||
if (feeType.bills.some(b => b.billId === bill.billId)) {
|
||
return building;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
|
||
// 查看账单详情
|
||
viewBillDetail(billId) {
|
||
this.$router.push(`/payment/bill-detail/${billId}`);
|
||
},
|
||
|
||
// 跳转到缴费记录页面
|
||
goToRecords() {
|
||
this.$router.push('/payment/records');
|
||
},
|
||
|
||
// 跳转到预缴费页面
|
||
goToPrepay() {
|
||
this.$router.push('/payment/prepay');
|
||
},
|
||
|
||
// 显示支付方式选择弹窗
|
||
showPaymentTypeDialog() {
|
||
this.paymentTypeVisible = true;
|
||
},
|
||
|
||
// 选择支付方式
|
||
selectPaymentType(type) {
|
||
this.paymentType = type;
|
||
},
|
||
|
||
// 取消支付方式选择
|
||
cancelPaymentType() {
|
||
this.paymentTypeVisible = false;
|
||
},
|
||
|
||
// 确认支付方式
|
||
confirmPaymentType() {
|
||
this.SET_PAY_TYPE(this.paymentType);
|
||
this.paymentTypeVisible = false;
|
||
this.goPay();
|
||
},
|
||
|
||
// 去支付
|
||
async goPay() {
|
||
if (this.selectedBillsCount === 0) {
|
||
this.$toast('请选择需要缴费的账单');
|
||
return;
|
||
}
|
||
|
||
// 创建支付订单
|
||
const orderData = await this.createPayOrder();
|
||
if (!orderData) return;
|
||
|
||
// 根据支付类型处理
|
||
if (this.paymentType === 'personal') {
|
||
// 个对公支付,唤起支付
|
||
// 这里应该跳转到第三方支付页面,根据实际情况实现
|
||
window.location.href = orderData.payUrl;
|
||
} else {
|
||
// 公对公支付,展示支付链接
|
||
this.$router.push({
|
||
path: '/payment/enterprise-pay',
|
||
query: {
|
||
orderNo: orderData.orderNo,
|
||
amount: orderData.amount
|
||
}
|
||
});
|
||
}
|
||
},
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.payment-page {
|
||
padding-bottom: 60px;
|
||
}
|
||
|
||
.user-info-bar {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.bill-group {
|
||
margin-bottom: 8px;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.bill-group-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
background-color: #f8f8f8;
|
||
|
||
.header-content {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
}
|
||
|
||
.building-name {
|
||
flex: 1;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.building-info {
|
||
margin-right: 8px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.fee-type-group {
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.fee-type-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 16px;
|
||
font-size: 15px;
|
||
|
||
.header-content {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
}
|
||
|
||
.fee-type-name {
|
||
flex: 1;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.fee-type-info {
|
||
margin-right: 8px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.bill-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
|
||
.bill-content {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
}
|
||
|
||
.bill-info {
|
||
flex: 1;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.bill-remark {
|
||
font-size: 14px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.bill-date {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.bill-amount {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #ee0a24;
|
||
}
|
||
}
|
||
|
||
.empty-data {
|
||
padding: 40px 0;
|
||
}
|
||
|
||
.footer-action-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.summary-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.total-amount {
|
||
margin-top: 4px;
|
||
font-size: 15px;
|
||
|
||
span {
|
||
font-weight: 500;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
min-width: 120px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
.payment-type-options {
|
||
padding: 16px;
|
||
|
||
.payment-type-item {
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #eee;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.payment-type-desc {
|
||
margin-top: 6px;
|
||
margin-left: 24px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
padding: 16px;
|
||
|
||
.van-button {
|
||
flex: 1;
|
||
margin: 0 4px;
|
||
}
|
||
}
|
||
|
||
.checkbox-wrapper, .radio-wrapper {
|
||
padding: 6px;
|
||
margin-top: -2px;
|
||
margin-bottom: -2px;
|
||
z-index: 10;
|
||
}
|
||
</style> |