初始化项目 #1

Open
zengqiyang wants to merge 21 commits from zqy-mst into main
33 changed files with 4348 additions and 122 deletions
Showing only changes of commit c29be6d13e - Show all commits

102
h5/README.md Normal file
View File

@ -0,0 +1,102 @@
# H5在线缴费系统
基于Vue.js和Vant UI的移动端在线缴费系统提供账单管理、缴费支付、对账等功能。
## 主要功能
- 缴费账单列表查询:查询当前业户待缴费的账单
- 预缴账单列表查询:查询当前业户未到期的预缴账单
- 缴费支付:支持个对公和公对公两种支付方式
- 缴费记录查询:查询历史缴费记录
- 支付结果查询:查询支付状态
- 其他辅助功能
## 技术栈
- Vue 2.6.x
- Vuex
- Vue Router
- Vant UI
- Axios
- Sass
- ES6+
## 项目结构
```
├── public/ # 静态资源目录
├── src/ # 源代码目录
│ ├── api/ # API接口定义
│ ├── assets/ # 静态资源文件
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ ├── store/ # Vuex状态管理
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
├── babel.config.js # Babel配置
├── postcss.config.js # PostCSS配置
└── vue.config.js # Vue CLI配置
```
## 快速开始
### 安装依赖
```
npm install
```
### 开发模式
```
npm run dev
```
### 生产构建
```
npm run build
```
### 代码格式化
```
npm run lint
```
## 组件说明
### 主要页面组件
- `Index.vue`: 在线缴费首页,显示待缴费账单列表
- `PrepayBills.vue`: 预缴账单页面,显示未到期的预缴账单
- `Records.vue`: 缴费记录页面,显示历史缴费记录
- `BillDetail.vue`: 账单详情页,显示账单详细信息
- `RecordDetail.vue`: 缴费记录详情页,显示缴费记录详细信息
- `EnterprisePay.vue`: 公对公支付页面,提供公对公支付链接
- `PayResult.vue`: 支付结果页面,显示支付结果信息
### 公共组件
- `PayerSelector.vue`: 业户选择器组件,用于选择当前操作的业户
## 业务规则
### 缴费流程
1. 用户在账单列表页选择需要缴费的账单
2. 选择支付方式(个对公/公对公)
3. 系统生成支付订单
4. 用户完成支付
5. 系统更新账单状态和缴费记录
### 公对公支付
1. 用户选择公对公支付方式后,系统生成支付链接
2. 支付链接有效期为5分钟
3. 用户复制链接后在浏览器中打开完成支付
4. 系统自动更新支付状态
## API文档
详细的API接口文档请参考后端开发文档。

5
h5/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
};

81
h5/package.json Normal file
View File

@ -0,0 +1,81 @@
{
"name": "U-H5",
"version": "1.0.0",
"private": true,
"author": {
"name": "zzz"
},
"scripts": {
"dev": "vue-cli-service serve --mode development",
"build": "vue-cli-service build --mode production",
"lint": "vue-cli-service lint",
"lint-fix": "vue-cli-service lint ./src --ext .vue,.js",
"prepare": "husky install"
},
"dependencies": {
"@vant/area-data": "^1.2.1",
"axios": "^0.27.2",
"clipboard": "^2.0.11",
"echarts": "^5.5.1",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"qrcode": "^1.5.3",
"qs": "^6.10.3",
"tui-image-editor": "^3.15.3",
"vant": "^2.13.1",
"vue": "^2.6.14",
"vue-draggable-resizable": "^2.3.0",
"vue-router": "^3.5.4",
"vue-wechat-title": "^2.0.7",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"weixin-js-sdk": "^1.4.0-test"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-airbnb": "^5.0.2",
"autoprefixer": "^10.4.7",
"babel-plugin-import": "^1.13.3",
"compression-webpack-plugin": "^6.1.1",
"core-js": "^3.23.2",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"eslint-plugin-vuejs-accessibility": "^1.1.0",
"husky": "^7.0.2",
"lint-staged": "^11.1.2",
"postcss-px-to-viewport": "^1.1.1",
"prettier": "^2.7.1",
"sass": "^1.53.0",
"sass-loader": "^13.0.0",
"style-resources-loader": "^1.5.0",
"vue-template-compiler": "^2.6.14",
"webpack-bundle-analyzer": "^4.5.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

16
h5/postcss.config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
viewportHeight: 667, // 视窗的高度根据750设备的宽度来指定一般指定1334
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数
viewportUnit: 'vw', // 指定需要转换成的视窗单位建议使用vw
selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false // 允许在媒体查询中转换`px`
},
'autoprefixer': {
browsers: ['Android >= 4.0', 'iOS >= 8']
}
}
};

36
h5/public/index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- 移动端适配 -->
<script>
(function(win, doc) {
if (!win.addEventListener) return;
function setFont() {
var html = document.documentElement;
var width = html.clientWidth;
var fontSize = (width / 375) * 16; // 以iPhone 6/7/8为基准
html.style.fontSize = fontSize + 'px';
}
setFont();
setTimeout(function() {
setFont();
}, 300);
doc.addEventListener('DOMContentLoaded', setFont, false);
win.addEventListener('resize', setFont, false);
win.addEventListener('load', setFont, false);
})(window, document);
</script>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

23
h5/src/App.vue Normal file
View File

@ -0,0 +1,23 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
<style lang="scss">
#app {
font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100%;
height: 100%;
background-color: #f5f5f5;
color: #333;
}
</style>

50
h5/src/api/payment.js Normal file
View File

@ -0,0 +1,50 @@
import { request } from '@/utils/request';
const api = {
// 查询缴费账单列表
getBills(params) {
return request.post('/payment/bills', params);
},
// 查询预缴账单列表
getPrepayBills(params) {
return request.post('/payment/prepay-bills', params);
},
// 查询账单详情
getBillDetail(billId, config = { showLoading: false }) {
return request.get(`/payment/bill-detail/${billId}`, null, config);
},
// 查询缴费记录
getPaymentRecords(params) {
return request.post('/payment/records', params);
},
// 查询缴费记录详情
getRecordDetail(billId, config) {
return request.get(`/payment/record-detail/${billId}`, null, config);
},
// 创建支付订单
createPayOrder(data) {
return request.post('/payment/create-order', data);
},
// 查询支付结果
queryPayResult(orderNo) {
return request.get(`/payment/order-result/${orderNo}`);
},
// 取消支付订单
cancelOrder(orderNo) {
return request.post(`/payment/cancel-order/${orderNo}`);
},
// 获取公对公支付链接
getEnterprisePayUrl(orderNo) {
return request.get(`/payment/enterprise-pay-url/${orderNo}`);
},
};
export default api;

15
h5/src/api/user.js Normal file
View File

@ -0,0 +1,15 @@
import { request } from '@/utils/request';
const api = {
// 获取用户信息
getUserInfo() {
return request.get('/user/info');
},
// 获取业户列表
getPayerList() {
return request.get('/user/payer-list');
},
};
export default api;

View File

@ -0,0 +1,268 @@
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-size: 16px;
font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
color: #333;
}
* {
box-sizing: border-box;
}
/* 常用颜色 */
:root {
--primary-color: #1989fa;
--success-color: #07c160;
--warning-color: #ff976a;
--danger-color: #ee0a24;
--info-color: #909399;
--background-color: #f5f5f5;
--text-color: #323233;
--text-color-light: #969799;
--border-color: #ebedf0;
}
/* 常用间距 */
.m-0 { margin: 0; }
.m-1 { margin: 4px; }
.m-2 { margin: 8px; }
.m-3 { margin: 12px; }
.m-4 { margin: 16px; }
.m-5 { margin: 20px; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 4px; }
.mt-2 { margin-top: 8px; }
.mt-3 { margin-top: 12px; }
.mt-4 { margin-top: 16px; }
.mt-5 { margin-top: 20px; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 4px; }
.mb-2 { margin-bottom: 8px; }
.mb-3 { margin-bottom: 12px; }
.mb-4 { margin-bottom: 16px; }
.mb-5 { margin-bottom: 20px; }
.ml-0 { margin-left: 0; }
.ml-1 { margin-left: 4px; }
.ml-2 { margin-left: 8px; }
.ml-3 { margin-left: 12px; }
.ml-4 { margin-left: 16px; }
.ml-5 { margin-left: 20px; }
.mr-0 { margin-right: 0; }
.mr-1 { margin-right: 4px; }
.mr-2 { margin-right: 8px; }
.mr-3 { margin-right: 12px; }
.mr-4 { margin-right: 16px; }
.mr-5 { margin-right: 20px; }
.p-0 { padding: 0; }
.p-1 { padding: 4px; }
.p-2 { padding: 8px; }
.p-3 { padding: 12px; }
.p-4 { padding: 16px; }
.p-5 { padding: 20px; }
.pt-0 { padding-top: 0; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.pt-3 { padding-top: 12px; }
.pt-4 { padding-top: 16px; }
.pt-5 { padding-top: 20px; }
.pb-0 { padding-bottom: 0; }
.pb-1 { padding-bottom: 4px; }
.pb-2 { padding-bottom: 8px; }
.pb-3 { padding-bottom: 12px; }
.pb-4 { padding-bottom: 16px; }
.pb-5 { padding-bottom: 20px; }
.pl-0 { padding-left: 0; }
.pl-1 { padding-left: 4px; }
.pl-2 { padding-left: 8px; }
.pl-3 { padding-left: 12px; }
.pl-4 { padding-left: 16px; }
.pl-5 { padding-left: 20px; }
.pr-0 { padding-right: 0; }
.pr-1 { padding-right: 4px; }
.pr-2 { padding-right: 8px; }
.pr-3 { padding-right: 12px; }
.pr-4 { padding-right: 16px; }
.pr-5 { padding-right: 20px; }
/* 常用布局类 */
.flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.align-start {
align-items: flex-start;
}
.align-center {
align-items: center;
}
.align-end {
align-items: flex-end;
}
.flex-1 {
flex: 1;
}
.flex-grow-0 {
flex-grow: 0;
}
.flex-grow-1 {
flex-grow: 1;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-primary {
color: var(--primary-color);
}
.text-success {
color: var(--success-color);
}
.text-warning {
color: var(--warning-color);
}
.text-danger {
color: var(--danger-color);
}
.text-info {
color: var(--info-color);
}
.bg-white {
background-color: #fff;
}
.rounded {
border-radius: 4px;
}
.rounded-circle {
border-radius: 50%;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.position-relative {
position: relative;
}
.position-absolute {
position: absolute;
}
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
.divider {
height: 8px;
background-color: var(--background-color);
}
/* 标题样式 */
.page-title {
font-size: 18px;
font-weight: 500;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
background-color: #fff;
margin: 0;
}
/* 自定义卡片样式 */
.card {
background-color: #fff;
border-radius: 8px;
margin: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(100, 101, 102, 0.08);
}
.card-title {
font-size: 16px;
font-weight: 500;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
/* 底部操作栏 */
.footer-action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #fff;
padding: 10px 16px;
border-top: 1px solid var(--border-color);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}

View File

@ -0,0 +1,101 @@
// 主题颜色
$primary-color: #1989fa;
$success-color: #07c160;
$warning-color: #ff976a;
$danger-color: #ee0a24;
$info-color: #909399;
// 背景颜色
$background-color: #f5f5f5;
$background-color-light: #fafafa;
// 文字颜色
$text-color: #323233;
$text-color-light: #969799;
$text-color-disabled: #c8c9cc;
// 边框颜色
$border-color: #ebedf0;
$border-color-dark: #dcdee0;
// 字体大小
$font-size-xs: 10px;
$font-size-sm: 12px;
$font-size-md: 14px;
$font-size-lg: 16px;
$font-size-xl: 18px;
// 边距
$padding-xs: 4px;
$padding-sm: 8px;
$padding-md: 12px;
$padding-lg: 16px;
$padding-xl: 20px;
// 圆角
$border-radius-sm: 2px;
$border-radius-md: 4px;
$border-radius-lg: 8px;
$border-radius-max: 999px;
// 动画
$animation-duration-base: 0.3s;
$animation-duration-fast: 0.2s;
$animation-timing-function-enter: ease-out;
$animation-timing-function-leave: ease-in;
// 阴影
$shadow-1: 0 2px 4px rgba(0, 0, 0, 0.1);
$shadow-2: 0 4px 8px rgba(0, 0, 0, 0.1);
$shadow-3: 0 8px 16px rgba(0, 0, 0, 0.1);
// 混合器
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin multi-ellipsis($lines) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
@mixin hairline-bottom($color: $border-color) {
position: relative;
&::after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
border-bottom: 1px solid $color;
transform: scaleY(0.5);
}
}
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
@mixin clearfix {
&::after {
content: '';
display: table;
clear: both;
}
}

View File

@ -0,0 +1,81 @@
<template>
<div class="payer-selector">
<van-field
v-model="payerName"
readonly
label="当前业户"
placeholder="请选择业户"
right-icon="arrow-down"
@click="showPopup"
/>
<van-popup v-model="showPicker" round position="bottom">
<van-picker
show-toolbar
title="选择业户"
:columns="payerOptions"
@confirm="onConfirm"
@cancel="onCancel"
/>
</van-popup>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'PayerSelector',
data() {
return {
showPicker: false,
};
},
computed: {
...mapState('user', ['currentPayer', 'payerList']),
payerName() {
return this.currentPayer ? this.currentPayer.name : '';
},
payerOptions() {
return this.payerList.map(payer => ({
text: payer.name,
value: payer.id,
}));
},
},
created() {
//
if (this.payerList.length === 0) {
this.getPayerList();
}
},
methods: {
...mapActions('user', ['getPayerList', 'selectPayer']),
showPopup() {
this.showPicker = true;
},
onConfirm(value) {
const payerId = value.value;
const payer = this.payerList.find(p => p.id === payerId);
if (payer) {
this.selectPayer(payer);
}
this.showPicker = false;
},
onCancel() {
this.showPicker = false;
},
},
};
</script>
<style lang="scss" scoped>
.payer-selector {
margin-bottom: 10px;
}
</style>

10
h5/src/env.development.js Normal file
View File

@ -0,0 +1,10 @@
// 开发环境配置
export default {
NODE_ENV: 'development',
// API 基础路径
BASE_API: '/api',
// 页面标题
TITLE: '在线缴费系统'
};

10
h5/src/env.production.js Normal file
View File

@ -0,0 +1,10 @@
// 生产环境配置
export default {
NODE_ENV: 'production',
// API 基础路径
BASE_API: '/api',
// 页面标题
TITLE: '在线缴费系统'
};

25
h5/src/main.js Normal file
View File

@ -0,0 +1,25 @@
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 完整引入Vant及其样式
import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);
// 导入全局样式
import './assets/styles/index.scss';
// 全局API请求
import { request } from './utils/request';
Vue.prototype.$http = request;
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

75
h5/src/router/index.js Normal file
View File

@ -0,0 +1,75 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueWechatTitle from 'vue-wechat-title';
Vue.use(VueRouter);
Vue.use(VueWechatTitle);
// 路由懒加载
const routes = [
{
path: '/',
redirect: '/payment',
},
{
path: '/payment',
name: 'Payment',
component: () => import('../views/payment/Index.vue'),
meta: { title: '在线缴费' },
},
{
path: '/payment/prepay',
name: 'PrepayBills',
component: () => import('../views/payment/PrepayBills.vue'),
meta: { title: '预缴账单' },
},
{
path: '/payment/records',
name: 'PaymentRecords',
component: () => import('../views/payment/Records.vue'),
meta: { title: '缴费记录' },
},
{
path: '/payment/bill-detail/:billId',
name: 'BillDetail',
component: () => import('../views/payment/BillDetail.vue'),
meta: { title: '账单详情' },
},
{
path: '/payment/record-detail/:billId',
name: 'RecordDetail',
component: () => import('../views/payment/RecordDetail.vue'),
meta: { title: '缴费详情' },
},
{
path: '/payment/pay-result',
name: 'PayResult',
component: () => import('../views/payment/PayResult.vue'),
meta: { title: '支付结果' },
},
{
path: '/payment/enterprise-pay',
name: 'EnterprisePay',
component: () => import('../views/payment/EnterprisePay.vue'),
meta: { title: '公对公支付' },
},
{
path: '*',
redirect: '/payment',
},
];
const router = new VueRouter({
routes,
});
// 全局路由守卫
router.beforeEach((to, from, next) => {
// 更新页面标题
if (to.meta.title) {
document.title = to.meta.title;
}
next();
});
export default router;

26
h5/src/store/index.js Normal file
View File

@ -0,0 +1,26 @@
import Vue from 'vue';
import Vuex from 'vuex';
import payment from './modules/payment';
import user from './modules/user';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
loading: false,
},
mutations: {
SET_LOADING(state, loading) {
state.loading = loading;
},
},
actions: {
setLoading({ commit }, loading) {
commit('SET_LOADING', loading);
},
},
modules: {
payment,
user,
},
});

View File

@ -0,0 +1,217 @@
import { Toast } from 'vant';
import api from '@/api/payment';
const state = {
bills: [], // 缴费账单列表
prepayBills: [], // 预缴账单列表
paymentRecords: [], // 缴费记录
currentBill: null, // 当前查看的账单详情
selectedBills: [], // 已选择的账单
totalAmount: 0, // 总金额
totalCount: 0, // 总数量
payerName: '', // 缴费人姓名
buildingGroups: [], // 楼宇分组
payType: 'personal', // 支付类型personal-个对公enterprise-公对公
};
const mutations = {
SET_BILLS(state, data) {
state.bills = data.bills || [];
state.totalAmount = data.totalAmount || 0;
state.totalCount = data.totalCount || 0;
state.payerName = data.payerName || '';
state.buildingGroups = data.buildingGroups || [];
},
SET_PREPAY_BILLS(state, data) {
state.prepayBills = data.bills || [];
state.totalAmount = data.totalAmount || 0;
state.totalCount = data.totalCount || 0;
state.payerName = data.payerName || '';
state.buildingGroups = data.buildingGroups || [];
},
SET_PAYMENT_RECORDS(state, records) {
state.paymentRecords = records;
},
SET_CURRENT_BILL(state, bill) {
state.currentBill = bill;
},
ADD_SELECTED_BILL(state, bill) {
// 保留已选择的其他费用类型账单,但替换同一费用类型下的账单
const existingBillsFromOtherTypes = state.selectedBills.filter(item =>
item.feeTypeName !== bill.feeTypeName ||
item.buildingId !== bill.buildingId
);
// 将当前账单加入到已选择的账单列表中
state.selectedBills = [...existingBillsFromOtherTypes, bill];
},
REMOVE_SELECTED_BILL(state, billId) {
state.selectedBills = state.selectedBills.filter(bill => bill.billId !== billId);
},
CLEAR_SELECTED_BILLS(state) {
state.selectedBills = [];
},
SET_PAY_TYPE(state, type) {
state.payType = type;
},
};
const actions = {
// 获取缴费账单列表
async getBills({ commit }, params) {
try {
const response = await api.getBills(params);
if (response.code === '0000000000000000') {
commit('SET_BILLS', response.data);
return response.data;
} else {
Toast.fail(response.message || '获取账单列表失败');
return null;
}
} catch (error) {
Toast.fail('获取账单列表失败');
return null;
}
},
// 获取预缴账单列表
async getPrepayBills({ commit }, params) {
try {
const response = await api.getPrepayBills(params);
if (response.code === '0000000000000000') {
commit('SET_PREPAY_BILLS', response.data);
return response.data;
} else {
Toast.fail(response.message || '获取预缴账单列表失败');
return null;
}
} catch (error) {
Toast.fail('获取预缴账单列表失败');
return null;
}
},
// 获取缴费记录
async getPaymentRecords({ commit }, params) {
try {
const response = await api.getPaymentRecords(params);
if (response.code === '0000000000000000') {
commit('SET_PAYMENT_RECORDS', response.data.list || []);
return response.data;
} else {
Toast.fail(response.message || '获取缴费记录失败');
return null;
}
} catch (error) {
Toast.fail('获取缴费记录失败');
return null;
}
},
// 获取账单详情
async getBillDetail({ commit }, billId) {
try {
const response = await api.getBillDetail(billId);
if (response.code === '0000000000000000') {
commit('SET_CURRENT_BILL', response.data);
return response.data;
} else {
Toast.fail(response.message || '获取账单详情失败');
return null;
}
} catch (error) {
Toast.fail('获取账单详情失败');
return null;
}
},
// 获取缴费记录详情
async getRecordDetail({ commit }, billId) {
try {
const response = await api.getRecordDetail(billId);
if (response.code === '0000000000000000') {
commit('SET_CURRENT_BILL', response.data);
return response.data;
} else {
Toast.fail(response.message || '获取缴费记录详情失败');
return null;
}
} catch (error) {
Toast.fail('获取缴费记录详情失败');
return null;
}
},
// 创建支付订单
async createPayOrder({ state }) {
if (state.selectedBills.length === 0) {
Toast.fail('请选择需要缴费的账单');
return null;
}
try {
const billIds = state.selectedBills.map(bill => bill.billId);
const payType = state.payType;
const response = await api.createPayOrder({
billIds,
payType,
});
if (response.code === '0000000000000000') {
return response.data;
} else {
Toast.fail(response.message || '创建支付订单失败');
return null;
}
} catch (error) {
Toast.fail('创建支付订单失败');
return null;
}
},
// 查询支付结果
async queryPayResult({ commit }, orderNo) {
try {
const response = await api.queryPayResult(orderNo);
return response.data;
} catch (error) {
Toast.fail('查询支付结果失败');
return null;
}
},
// 取消订单
async cancelOrder(_, orderNo) {
try {
const response = await api.cancelOrder(orderNo);
if (response.code === '0000000000000000') {
Toast.success('取消订单成功');
return true;
} else {
Toast.fail(response.message || '取消订单失败');
return false;
}
} catch (error) {
Toast.fail('取消订单失败');
return false;
}
},
};
const getters = {
selectedBillsTotalAmount(state) {
return state.selectedBills.reduce((total, bill) => total + bill.totalNeedAmount, 0);
},
selectedBillsCount(state) {
return state.selectedBills.length;
},
};
export default {
namespaced: true,
state,
mutations,
actions,
getters,
};

View File

@ -0,0 +1,107 @@
import { Toast } from 'vant';
import api from '@/api/user';
const state = {
userInfo: null, // 用户信息
currentPayer: null, // 当前选择的业户
payerList: [], // 业户列表
};
const mutations = {
SET_USER_INFO(state, userInfo) {
state.userInfo = userInfo;
},
SET_CURRENT_PAYER(state, payer) {
state.currentPayer = payer;
// 将当前选择的业户ID保存到localStorage
if (payer) {
localStorage.setItem('currentPayerId', payer.id);
} else {
localStorage.removeItem('currentPayerId');
}
},
SET_PAYER_LIST(state, list) {
state.payerList = list;
},
};
const actions = {
// 获取用户信息
// async getUserInfo({ commit }) {
// try {
// const response = await api.getUserInfo();
// if (response.code === '0000000000000000') {
// commit('SET_USER_INFO', response.data);
// return response.data;
// } else {
// Toast.fail(response.message || '获取用户信息失败');
// return null;
// }
// } catch (error) {
// Toast.fail('获取用户信息失败');
// return null;
// }
// },
// // 获取业户列表
// async getPayerList({ commit }) {
// try {
// const response = await api.getPayerList();
// if (response.code === '0000000000000000') {
// const payerList = response.data || [];
// commit('SET_PAYER_LIST', payerList);
// // 如果localStorage中有保存的业户ID则自动选择
// const savedPayerId = localStorage.getItem('currentPayerId');
// if (savedPayerId && payerList.length > 0) {
// const savedPayer = payerList.find(payer => payer.id === Number(savedPayerId));
// if (savedPayer) {
// commit('SET_CURRENT_PAYER', savedPayer);
// } else {
// // 如果没找到保存的业户,则选择第一个
// commit('SET_CURRENT_PAYER', payerList[0]);
// }
// } else if (payerList.length > 0) {
// // 默认选择第一个业户
// commit('SET_CURRENT_PAYER', payerList[0]);
// }
// return payerList;
// } else {
// Toast.fail(response.message || '获取业户列表失败');
// return [];
// }
// } catch (error) {
// Toast.fail('获取业户列表失败');
// return [];
// }
// },
// // 选择业户
// selectPayer({ commit }, payer) {
// commit('SET_CURRENT_PAYER', payer);
// },
};
const getters = {
// 当前选择的业户ID
currentPayerId(state) {
return state.currentPayer ? state.currentPayer.id : null;
},
// 当前选择的业户名称
currentPayerName(state) {
return state.currentPayer ? state.currentPayer.name : '';
},
// 是否已选择业户
hasSelectedPayer(state) {
return !!state.currentPayer;
},
};
export default {
namespaced: true,
state,
mutations,
actions,
getters,
};

164
h5/src/utils/request.js Normal file
View File

@ -0,0 +1,164 @@
import axios from 'axios';
import { Toast } from 'vant';
import qs from 'qs';
import store from '@/store';
// 创建axios实例
const service = axios.create({
baseURL: 'http://192.168.137.45:8080',
timeout: 30000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 显示loading除非请求配置中指定了不显示
if (config.showLoading !== false) {
store.dispatch('setLoading', true);
}
// 获取token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 如果有选择业户则在请求头中带上业户ID
const currentPayerId = store.getters['user/currentPayerId'];
if (currentPayerId) {
config.headers['X-Payer-Id'] = currentPayerId;
}
// 处理GET请求参数
if (config.method === 'get' && config.params) {
config.paramsSerializer = (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' });
};
}
return config;
},
(error) => {
// 隐藏loading
store.dispatch('setLoading', false);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
// 隐藏loading
store.dispatch('setLoading', false);
// 处理二进制数据
if (response.config.responseType === 'blob') {
return response;
}
const res = response.data;
// 判断是否成功
if (res.code === '0000000000000000') {
return res;
}
// 处理业务错误
if (res.code === 'UNAUTHORIZED') {
// 未登录,跳转到登录页
Toast.fail('用户未登录或登录已过期');
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/login';
}, 1500);
} else {
// 其他业务错误
Toast.fail(res.message || '请求失败');
}
return res;
},
(error) => {
// 隐藏loading
store.dispatch('setLoading', false);
let message = '请求失败';
if (error.response) {
switch (error.response.status) {
case 401:
message = '用户未登录或登录已过期';
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/login';
}, 1500);
break;
case 403:
message = '拒绝访问';
break;
case 404:
message = '请求地址错误';
break;
case 500:
message = '服务器内部错误';
break;
default:
message = `请求失败(${error.response.status})`;
}
} else if (error.message.includes('timeout')) {
message = '请求超时';
}
Toast.fail(message);
return Promise.reject(error);
}
);
// 封装请求方法
export const request = {
get(url, params, config = {}) {
return service({
url,
method: 'get',
params,
...config
});
},
post(url, data, config = {}) {
return service({
url,
method: 'post',
data,
...config
});
},
put(url, data, config = {}) {
return service({
url,
method: 'put',
data,
...config
});
},
delete(url, params, config = {}) {
return service({
url,
method: 'delete',
params,
...config
});
},
download(url, params, config = {}) {
return service({
url,
method: 'get',
params,
responseType: 'blob',
...config
});
},
};
export default service;

View File

@ -0,0 +1,236 @@
<template>
<div class="bill-detail-page">
<van-nav-bar
title="账单详情"
left-arrow
@click-left="goBack"
/>
<div class="no-data" v-if="!currentBill">
<van-loading v-if="loading" type="spinner" color="#1989fa" />
<van-empty v-else description="暂无数据" />
</div>
<template v-if="currentBill">
<!-- 基本信息 -->
<div class="card">
<div class="card-title">基本信息</div>
<van-cell-group :border="false">
<van-cell title="费用类型" :value="currentBill && currentBill.feTypeName" />
<van-cell title="账单编号" :value="currentBill && currentBill.billNo" />
<van-cell title="账单状态" :value="currentBill && currentBill.billStatus" />
<van-cell title="结清状态" :value="currentBill && currentBill.clearStatus" />
<van-cell title="计费周期" :value="currentBill && currentBill.feePeriod" />
<van-cell title="应收金额" :value="currentBill && `¥${formatAmount(currentBill.receivableAmount)}`" />
<van-cell title="实收金额" :value="currentBill && `¥${formatAmount(currentBill.receivedAmount)}`" />
<van-cell title="需收金额" :value="currentBill && `¥${formatAmount(currentBill.needAmount)}`" />
<van-cell title="应收日期" :value="currentBill && currentBill.receivableDate" />
<van-cell title="关联合同" :value="currentBill && currentBill.contractNo" />
<van-cell title="跟进人" :value="currentBill && currentBill.follower" />
<van-cell title="关联楼宇" :value="currentBill && currentBill.buildingName" />
<van-cell title="关联房间" :value="currentBill && currentBill.roomNumber" />
<van-cell title="创建时间" :value="currentBill && currentBill.createTime" />
<van-cell title="账单备注" :value="currentBill && currentBill.remark" />
</van-cell-group>
</div>
<!-- 滞纳金信息 -->
<div class="card">
<div class="card-title">滞纳金信息</div>
<van-cell-group :border="false">
<van-cell title="需收滞纳金" :value="`¥${formatAmount(currentBill.needOvdueAmt || 0)}`" />
<van-cell title="已收滞纳金" :value="`¥${formatAmount(currentBill.receivedOvdueAmt || 0)}`" />
<van-cell title="起算天数" :value="`${currentBill.ovdueStartDays || 0}天`" />
<van-cell title="滞纳金比例" :value="`${parseFloat((currentBill.ovdueIntRate || 0) * 100).toFixed(3)}%/天`" />
<van-cell title="滞纳金上限" :value="`${parseFloat((currentBill.ovdueLimitRate || 0) * 100).toFixed(0)}%`" />
</van-cell-group>
</div>
<!-- 账单明细 -->
<div class="card" v-if="currentBill.billDetails && currentBill.billDetails.length > 0">
<div class="card-title">账单明细</div>
<div class="bill-detail-list">
<div
v-for="detail in currentBill.billDetails"
:key="detail.billDetailId"
class="bill-detail-item"
@click="showBillDetailInfo(detail)"
>
<div class="bill-detail-type">
<van-tag :type="detail.billDetailType == '0' ? 'primary' : 'warning'">
{{ detail.billDetailType == '0' ? '原账单' : '滞纳金' }}
</van-tag>
</div>
<div class="bill-detail-name">{{ detail.feTypeName || '未知费用类型' }}</div>
<div class="bill-detail-amount">¥{{ formatAmount(detail.billAmount || 0) }}</div>
<van-icon name="arrow" />
</div>
</div>
</div>
</template>
<!-- 账单明细详情弹窗 -->
<van-popup v-model="showDetailPopup" round position="bottom" style="height: 70%">
<div class="popup-header">
<div class="popup-title">明细详情</div>
<van-icon name="cross" @click="showDetailPopup = false" />
</div>
<div class="popup-content" v-if="selectedDetail">
<van-cell-group :border="false">
<van-cell title="费用类型" :value="selectedDetail.feTypeName || '未知'" />
<van-cell title="账单编号" :value="selectedDetail.totBillNo || '未知'" />
<van-cell title="计费周期" :value="selectedDetail.chggBgnDt + '至' + selectedDetail.chggEndDt" />
<van-cell title="账单金额" :value="`¥${formatAmount(selectedDetail.accblAmt || 0)}`" />
<van-cell title="关联合同" :value="selectedDetail.contractNo || '未知'" />
<van-cell title="创建时间" :value="selectedDetail.createTime || '未知'" />
<van-cell title="账单备注" :value="selectedDetail.remark || '无'" />
</van-cell-group>
</div>
</van-popup>
</div>
</template>
<script>
import { Toast } from 'vant';
import api from '@/api/payment';
export default {
name: 'BillDetail',
data() {
return {
loading: false,
currentBill: null, //
showDetailPopup: false,
selectedDetail: null,
};
},
computed: {
billId() {
return this.$route.params.billId;
},
},
created() {
this.fetchBillDetail();
},
methods: {
//
async fetchBillDetail() {
this.loading = true;
try {
// showLoading false loading
const response = await api.getBillDetail(this.billId, { showLoading: false });
if (response.code === '0000000000000000') {
//
if (!response.data) {
Toast.fail('账单数据格式不正确');
return;
}
this.currentBill = response.data;
} else {
Toast.fail(response.message || '获取账单详情失败');
}
} catch (error) {
console.error('获取账单详情出错:', error);
Toast.fail('获取账单详情失败');
} finally {
// loading
this.loading = false;
}
},
//
goBack() {
this.$router.go(-1);
},
//
formatAmount(amount) {
if (!amount || isNaN(amount)) {
return '0.00';
}
return parseFloat(amount).toFixed(2);
},
//
showBillDetailInfo(detail) {
if (!detail) {
Toast.fail('明细数据不完整');
return;
}
this.selectedDetail = detail;
this.showDetailPopup = true;
},
},
};
</script>
<style lang="scss" scoped>
.bill-detail-page {
padding-bottom: 20px;
background-color: #f5f5f5;
}
.no-data {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
}
.bill-detail-list {
padding: 0 10px;
}
.bill-detail-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.bill-detail-type {
margin-right: 10px;
}
.bill-detail-name {
flex: 1;
font-size: 14px;
}
.bill-detail-amount {
font-size: 15px;
font-weight: 500;
margin-right: 10px;
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
.popup-title {
font-size: 16px;
font-weight: 500;
}
.van-icon {
font-size: 20px;
color: #999;
}
}
.popup-content {
padding: 10px 0;
overflow-y: auto;
max-height: calc(70vh - 56px);
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div class="enterprise-pay-page">
<van-nav-bar
title="公对公支付"
left-arrow
@click-left="goBack"
/>
<div class="card payment-info">
<div class="payment-header">
<div class="payment-title">公对公支付</div>
<div class="payment-subtitle">请复制链接至浏览器完成支付</div>
</div>
<div class="payment-amount">¥{{ formatAmount(amount) }}</div>
<div class="countdown">
<span>链接有效期</span>
<van-count-down :time="timeRemaining" format="mm:ss" />
</div>
</div>
<div class="card payment-link">
<div class="card-title">支付链接</div>
<div class="link-box">
<div class="link-content">{{ payLink }}</div>
<van-button
type="primary"
size="small"
:disabled="!payLink"
@click="copyLink"
>复制链接</van-button>
</div>
<div class="tips">
<p>温馨提示</p>
<p>1. 该链接5分钟内有效请尽快完成支付</p>
<p>2. 复制链接后可粘贴至浏览器地址栏打开</p>
<p>3. 支付成功后系统将自动处理订单</p>
</div>
</div>
<div class="buttons">
<van-button type="default" block @click="goBack">返回</van-button>
<van-button type="primary" block @click="checkPayStatus">查询支付结果</van-button>
</div>
</div>
</template>
<script>
import { Toast } from 'vant';
import clipboard from 'clipboard';
import api from '@/api/payment';
export default {
name: 'EnterprisePay',
data() {
return {
orderNo: '',
amount: 0,
payLink: '',
timeRemaining: 5 * 60 * 1000, // 5
timer: null,
loadingStatus: false,
clipboardInstance: null,
};
},
computed: {
//
isLinkExpired() {
return this.timeRemaining <= 0;
},
},
created() {
//
this.orderNo = this.$route.query.orderNo;
this.amount = this.$route.query.amount || 0;
if (!this.orderNo) {
Toast('订单信息不完整,请返回重试');
return;
}
//
this.getEnterprisePayLink();
//
this.initClipboard();
},
beforeDestroy() {
//
if (this.timer) {
clearInterval(this.timer);
}
//
if (this.clipboardInstance) {
this.clipboardInstance.destroy();
}
},
methods: {
//
initClipboard() {
this.$nextTick(() => {
// clipboard.js
const button = document.createElement('button');
button.id = 'copy-btn';
button.style.display = 'none';
document.body.appendChild(button);
this.clipboardInstance = new clipboard('#copy-btn', {
text: () => this.payLink
});
this.clipboardInstance.on('success', () => {
Toast.success('链接已复制');
});
this.clipboardInstance.on('error', () => {
Toast.fail('复制失败,请手动复制');
});
});
},
//
copyLink() {
if (!this.payLink) {
Toast('暂无可复制的链接');
return;
}
//
document.getElementById('copy-btn').click();
},
//
async getEnterprisePayLink() {
try {
// API
const response = await api.getEnterprisePayUrl(this.orderNo);
if (response.code === '0000000000000000') {
this.payLink = response.data.payUrl;
//
this.startCountdown();
} else {
Toast.fail(response.message || '获取支付链接失败');
}
} catch (error) {
Toast.fail('获取支付链接失败');
}
},
//
startCountdown() {
this.timer = setInterval(() => {
this.timeRemaining -= 1000;
//
if (this.timeRemaining <= 0) {
clearInterval(this.timer);
this.payLink = '链接已过期,请返回重新获取';
Toast('支付链接已过期');
}
}, 1000);
},
//
formatAmount(amount) {
return parseFloat(amount).toFixed(2);
},
//
goBack() {
this.$router.go(-1);
},
//
async checkPayStatus() {
if (this.loadingStatus) return;
this.loadingStatus = true;
Toast.loading({
message: '查询支付结果中...',
forbidClick: true,
});
try {
const response = await api.queryPayResult(this.orderNo);
Toast.clear();
this.loadingStatus = false;
if (response.code === '0000000000000000') {
const result = response.data;
//
this.$router.replace({
path: '/payment/pay-result',
query: {
orderNo: this.orderNo,
status: result.status,
amount: this.amount,
},
});
} else {
Toast.fail(response.message || '查询支付结果失败');
}
} catch (error) {
Toast.clear();
this.loadingStatus = false;
Toast.fail('查询支付结果失败,请稍后再试');
}
},
},
};
</script>
<style lang="scss" scoped>
.enterprise-pay-page {
padding-bottom: 20px;
background-color: #f5f5f5;
}
.payment-info {
text-align: center;
padding: 20px 0;
.payment-header {
margin-bottom: 16px;
.payment-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 8px;
}
.payment-subtitle {
font-size: 14px;
color: #666;
}
}
.payment-amount {
font-size: 28px;
font-weight: bold;
margin-bottom: 16px;
}
.countdown {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #666;
.van-count-down {
color: var(--danger-color);
}
}
}
.payment-link {
.link-box {
display: flex;
align-items: center;
margin-bottom: 16px;
.link-content {
flex: 1;
background-color: #f8f8f8;
padding: 12px;
border-radius: 4px;
font-size: 14px;
word-break: break-all;
margin-right: 10px;
color: #666;
}
}
.tips {
font-size: 12px;
color: #999;
line-height: 1.6;
p {
margin: 4px 0;
}
}
}
.buttons {
margin: 20px 12px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
</style>

View File

@ -0,0 +1,619 @@
<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>

View File

@ -0,0 +1,295 @@
<template>
<div class="pay-result-page">
<div class="result-content">
<!-- 支付成功 -->
<div v-if="payStatus === 'success'" class="result-success">
<van-icon name="success" />
<div class="result-title">支付成功</div>
<div class="result-amount">¥{{ formatAmount(amount) }}</div>
<div class="result-info">
<p>订单编号{{ orderNo }}</p>
<p>支付时间{{ formatDateTime() }}</p>
</div>
</div>
<!-- 支付处理中 -->
<div v-else-if="payStatus === 'processing'" class="result-processing">
<van-loading type="spinner" color="#1989fa" size="50px" />
<div class="result-title">支付处理中</div>
<div class="result-amount">¥{{ formatAmount(amount) }}</div>
<div class="result-info">
<p>订单编号{{ orderNo }}</p>
<p>请耐心等待支付结果处理中...</p>
</div>
</div>
<!-- 支付失败 -->
<div v-else-if="payStatus === 'failed'" class="result-failed">
<van-icon name="cross" />
<div class="result-title">支付失败</div>
<div class="result-amount">¥{{ formatAmount(amount) }}</div>
<div class="result-info">
<p>订单编号{{ orderNo }}</p>
<p>支付未完成请重新支付</p>
</div>
</div>
<!-- 支付已关闭 -->
<div v-else-if="payStatus === 'closed'" class="result-closed">
<van-icon name="warning-o" />
<div class="result-title">支付已关闭</div>
<div class="result-amount">¥{{ formatAmount(amount) }}</div>
<div class="result-info">
<p>订单编号{{ orderNo }}</p>
<p>该订单已关闭请重新发起支付</p>
</div>
</div>
<!-- 其他状态 -->
<div v-else class="result-unknown">
<van-icon name="question-o" />
<div class="result-title">状态未知</div>
<div class="result-amount">¥{{ formatAmount(amount) }}</div>
<div class="result-info">
<p>订单编号{{ orderNo }}</p>
<p>无法获取支付状态请稍后查询</p>
</div>
</div>
</div>
<!-- 按钮区域 -->
<div class="button-group">
<van-button
v-if="payStatus === 'processing'"
type="primary"
block
@click="checkPayStatus"
>刷新支付结果</van-button>
<van-button
v-if="payStatus === 'failed' || payStatus === 'closed'"
type="primary"
block
@click="goToPaymentPage"
>重新支付</van-button>
<van-button type="default" block @click="goToPaymentPage">返回缴费页面</van-button>
<van-button
v-if="payStatus === 'success'"
type="primary"
block
@click="goToRecords"
>查看缴费记录</van-button>
</div>
</div>
</template>
<script>
import { Toast } from 'vant';
import api from '@/api/payment';
export default {
name: 'PayResult',
data() {
return {
orderNo: '',
payStatus: '',
amount: 0,
loadingStatus: false,
timer: null,
};
},
created() {
//
this.orderNo = this.$route.query.orderNo;
this.payStatus = this.$route.query.status;
this.amount = this.$route.query.amount || 0;
//
this.payStatus = this.formatPayStatus(this.payStatus);
//
if (this.payStatus === 'processing') {
this.autoCheckStatus();
}
},
beforeDestroy() {
if (this.timer) {
clearTimeout(this.timer);
}
},
methods: {
//
formatPayStatus(status) {
switch (status) {
case 'success':
case '1':
return 'success';
case 'processing':
case '0':
return 'processing';
case 'failed':
case '2':
return 'failed';
case 'closed':
case '3':
return 'closed';
default:
return 'unknown';
}
},
//
autoCheckStatus() {
this.timer = setTimeout(() => {
this.checkPayStatus();
}, 5000); // 5
},
//
async checkPayStatus() {
if (this.loadingStatus) return;
this.loadingStatus = true;
Toast.loading({
message: '查询支付结果中...',
forbidClick: true,
});
try {
const response = await api.queryPayResult(this.orderNo);
Toast.clear();
this.loadingStatus = false;
if (response.code === '0000000000000000') {
const result = response.data;
this.payStatus = this.formatPayStatus(result.status);
//
if (this.payStatus === 'processing') {
this.autoCheckStatus();
}
} else {
Toast.fail(response.message || '查询支付结果失败');
}
} catch (error) {
Toast.clear();
this.loadingStatus = false;
Toast.fail('查询支付结果失败,请稍后再试');
}
},
//
formatAmount(amount) {
return parseFloat(amount).toFixed(2);
},
//
formatDateTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
//
goToPaymentPage() {
this.$router.replace('/payment');
},
//
goToRecords() {
this.$router.replace('/payment/records');
},
},
};
</script>
<style lang="scss" scoped>
.pay-result-page {
min-height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.result-content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
text-align: center;
.van-icon {
font-size: 60px;
margin-bottom: 20px;
}
.van-loading {
margin-bottom: 20px;
}
.result-title {
font-size: 20px;
font-weight: 500;
margin-bottom: 16px;
}
.result-amount {
font-size: 32px;
font-weight: bold;
margin-bottom: 30px;
}
.result-info {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.result-success {
.van-icon {
color: var(--success-color);
}
}
.result-failed {
.van-icon {
color: var(--danger-color);
}
}
.result-closed {
.van-icon {
color: var(--warning-color);
}
}
.result-unknown {
.van-icon {
color: var(--info-color);
}
}
}
.button-group {
padding: 20px;
.van-button {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,602 @@
<template>
<div class="prepay-bills-page">
<van-nav-bar
title="预缴账单"
left-arrow
@click-left="goBack"
/>
<!-- 缴费人信息 -->
<div class="user-info-bar bg-white p-3">
<span>缴费人: </span>
<span class="text-primary">{{ payerName }}</span>
</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>已选: {{ selectedBillsCount }}</span>
</div>
<div class="total-amount">合计: <span class="text-danger">¥{{ formatAmount(selectedBillsTotalAmount) }}</span></div>
</div>
<div class="action-buttons">
<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: 'PrepayBills',
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']),
},
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: '2', // 使ID
};
const response = await api.getPrepayBills(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}`);
},
//
goBack() {
this.$router.go(-1);
},
//
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>
.prepay-bills-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>

View File

@ -0,0 +1,286 @@
<template>
<div class="record-detail-page">
<van-nav-bar
title="缴费详情"
left-arrow
@click-left="goBack"
/>
<div class="no-data" v-if="!currentBill">
<van-loading v-if="loading" type="spinner" color="#1989fa" />
<van-empty v-else description="暂无数据" />
</div>
<template v-if="currentBill">
<!-- 支付信息 -->
<div class="card payment-info">
<div class="payment-status">
<van-icon name="success" />
<span>支付成功</span>
</div>
<div class="payment-amount">¥{{ formatAmount(getPaymentAmount()) }}</div>
</div>
<!-- 基本信息 -->
<div class="card">
<div class="card-title">基本信息</div>
<van-cell-group :border="false">
<van-cell title="费用类型" :value="currentBill.basicInfo && currentBill.basicInfo.feTypeName" />
<van-cell title="账单编号" :value="currentBill.basicInfo && currentBill.basicInfo.billNo" />
<van-cell title="账单状态" :value="currentBill.basicInfo && currentBill.basicInfo.billStatus" />
<van-cell title="结清状态" :value="currentBill.basicInfo && currentBill.basicInfo.clearStatus" />
<van-cell title="计费周期" :value="currentBill.basicInfo && currentBill.basicInfo.feePeriod" />
<van-cell title="应收金额" :value="currentBill.basicInfo && `¥${formatAmount(currentBill.basicInfo.receivableAmount)}`" />
<van-cell title="实收金额" :value="currentBill.basicInfo && `¥${formatAmount(currentBill.basicInfo.receivedAmount)}`" />
<van-cell title="需收金额" :value="currentBill.basicInfo && `¥${formatAmount(currentBill.basicInfo.needAmount)}`" />
<van-cell title="应收日期" :value="currentBill.basicInfo && currentBill.basicInfo.receivableDate" />
<van-cell title="关联合同" :value="currentBill.basicInfo && currentBill.basicInfo.contractNo" />
<van-cell title="跟进人" :value="currentBill.basicInfo && currentBill.basicInfo.follower" />
<van-cell title="关联楼宇" :value="currentBill.basicInfo && currentBill.basicInfo.buildingName" />
<van-cell title="关联房间" :value="currentBill.basicInfo && currentBill.basicInfo.roomNumber" />
<van-cell title="创建时间" :value="currentBill.basicInfo && currentBill.basicInfo.createTime" />
<van-cell title="账单备注" :value="currentBill.basicInfo && currentBill.basicInfo.remark" />
</van-cell-group>
</div>
<!-- 滞纳金信息 -->
<div class="card" v-if="currentBill.overdueInfo">
<div class="card-title">滞纳金信息</div>
<van-cell-group :border="false">
<van-cell title="需收滞纳金" :value="`¥${formatAmount(currentBill.overdueInfo.needOverdueAmount || 0)}`" />
<van-cell title="已收滞纳金" :value="`¥${formatAmount(currentBill.overdueInfo.receivedOverdueAmount || 0)}`" />
<van-cell title="起算天数" :value="`${currentBill.overdueInfo.startDays || 0}天`" />
<van-cell title="滞纳金比例" :value="`${parseFloat((currentBill.overdueInfo.overdueRate || 0) * 100).toFixed(3)}%/天`" />
<van-cell title="滞纳金上限" :value="`${parseFloat((currentBill.overdueInfo.overdueLimit || 0) * 100).toFixed(0)}%`" />
</van-cell-group>
</div>
<!-- 账单明细 -->
<div class="card" v-if="currentBill.billDetails && currentBill.billDetails.length > 0">
<div class="card-title">账单明细</div>
<div class="bill-detail-list">
<div
v-for="detail in currentBill.billDetails"
:key="detail.billDetailId"
class="bill-detail-item"
@click="showBillDetailInfo(detail)"
>
<div class="bill-detail-type">
<van-tag :type="detail.billDetailType === '0' ? 'primary' : 'warning'">
{{ detail.billDetailType === '0' ? '原账单' : '滞纳金' }}
</van-tag>
</div>
<div class="bill-detail-name">{{ detail.feTypeName || '未知费用类型' }}</div>
<div class="bill-detail-amount">¥{{ formatAmount(detail.billAmount || 0) }}</div>
<van-icon name="arrow" />
</div>
</div>
</div>
</template>
<!-- 账单明细详情弹窗 -->
<van-popup v-model="showDetailPopup" round position="bottom" style="height: 70%">
<div class="popup-header">
<div class="popup-title">明细详情</div>
<van-icon name="cross" @click="showDetailPopup = false" />
</div>
<div class="popup-content" v-if="selectedDetail">
<van-cell-group :border="false">
<van-cell title="费用类型" :value="selectedDetail.feTypeName || '未知'" />
<van-cell title="账单编号" :value="selectedDetail.billNo || '未知'" />
<van-cell title="计费周期" :value="selectedDetail.feePeriod || '未知'" />
<van-cell title="账单金额" :value="`¥${formatAmount(selectedDetail.billAmount || 0)}`" />
<van-cell title="关联合同" :value="selectedDetail.contractNo || '未知'" />
<van-cell title="创建时间" :value="selectedDetail.createTime || '未知'" />
<van-cell title="账单备注" :value="selectedDetail.remark || '无'" />
</van-cell-group>
</div>
</van-popup>
</div>
</template>
<script>
import { Toast } from 'vant';
import api from '@/api/payment';
export default {
name: 'RecordDetail',
data() {
return {
loading: false,
currentBill: null, //
showDetailPopup: false,
selectedDetail: null,
};
},
computed: {
billId() {
return this.$route.params.billId;
},
},
created() {
this.fetchRecordDetail();
},
methods: {
//
async fetchRecordDetail() {
this.loading = true;
try {
const response = await api.getRecordDetail(this.billId, { showLoading: false });
if (response.code === '0000000000000000') {
//
if (!response.data || !response.data.basicInfo) {
Toast.fail('账单数据格式不正确');
this.loading = false;
return;
}
this.currentBill = response.data;
} else {
Toast.fail(response.message || '获取缴费记录详情失败');
}
} catch (error) {
console.error('获取缴费记录详情出错:', error);
Toast.fail('获取缴费记录详情失败');
} finally {
this.loading = false;
}
},
//
goBack() {
this.$router.go(-1);
},
//
formatAmount(amount) {
if (!amount || isNaN(amount)) {
return '0.00';
}
return parseFloat(amount).toFixed(2);
},
//
getPaymentAmount() {
if (!this.currentBill || !this.currentBill.basicInfo) return 0;
const basicAmount = this.currentBill.basicInfo.receivedAmount || 0;
const overdueAmount = this.currentBill.overdueInfo
? (this.currentBill.overdueInfo.receivedOverdueAmount || 0)
: 0;
return basicAmount + overdueAmount;
},
//
showBillDetailInfo(detail) {
if (!detail) {
Toast.fail('明细数据不完整');
return;
}
this.selectedDetail = detail;
this.showDetailPopup = true;
},
},
};
</script>
<style lang="scss" scoped>
.record-detail-page {
padding-bottom: 20px;
background-color: #f5f5f5;
}
.no-data {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
}
.payment-info {
text-align: center;
padding: 20px 0;
background-color: #fff;
.payment-status {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
.van-icon {
color: var(--success-color);
font-size: 20px;
margin-right: 8px;
}
span {
font-size: 16px;
font-weight: 500;
}
}
.payment-amount {
font-size: 24px;
font-weight: bold;
color: #333;
}
}
.bill-detail-list {
padding: 0 10px;
}
.bill-detail-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.bill-detail-type {
margin-right: 10px;
}
.bill-detail-name {
flex: 1;
font-size: 14px;
}
.bill-detail-amount {
font-size: 15px;
font-weight: 500;
margin-right: 10px;
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
.popup-title {
font-size: 16px;
font-weight: 500;
}
.van-icon {
font-size: 20px;
color: #999;
}
}
.popup-content {
padding: 10px 0;
overflow-y: auto;
max-height: calc(70vh - 56px);
}
</style>

View File

@ -0,0 +1,378 @@
<template>
<div class="payment-records-page">
<van-nav-bar
title="缴费记录"
left-arrow
@click-left="goBack"
/>
<!-- 筛选条件 -->
<div class="filter-bar bg-white">
<van-dropdown-menu>
<van-dropdown-item v-model="feeTypeId" :options="feeTypeOptions" />
<van-dropdown-item title="日期范围" ref="dateFilter">
<div class="date-filter">
<van-cell title="开始日期" :value="startDate" @click="showStartDatePicker = true" />
<van-cell title="结束日期" :value="endDate" @click="showEndDatePicker = true" />
<div class="filter-buttons">
<van-button type="default" size="small" @click="resetDateFilter">重置</van-button>
<van-button type="primary" size="small" @click="confirmDateFilter">确定</van-button>
</div>
</div>
</van-dropdown-item>
</van-dropdown-menu>
</div>
<!-- 日期选择器 -->
<van-popup v-model="showStartDatePicker" position="bottom">
<van-datetime-picker
type="date"
title="选择开始日期"
:value="startDateObj"
:max-date="new Date()"
@confirm="onStartDateConfirm"
@cancel="showStartDatePicker = false"
/>
</van-popup>
<van-popup v-model="showEndDatePicker" position="bottom">
<van-datetime-picker
type="date"
title="选择结束日期"
:value="endDateObj"
:min-date="startDateObj"
:max-date="new Date()"
@confirm="onEndDateConfirm"
@cancel="showEndDatePicker = false"
/>
</van-popup>
<!-- 缴费记录列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div v-if="paymentRecords.length === 0 && !loading" class="empty-data">
<van-empty description="暂无缴费记录" />
</div>
<div class="record-list">
<div
v-for="record in paymentRecords"
:key="record.billId"
class="record-item"
@click="viewRecordDetail(record.billId)"
>
<div class="record-header">
<div class="record-type">{{ record.feTypeName }}</div>
<div :class="['record-status', getStatusClass(record.clearStatus)]">
{{ getStatusText(record.clearStatus) }}
</div>
</div>
<div class="record-content">
<div class="record-info">
<div class="record-date">应收日期: {{ record.pybDt }}</div>
<div class="record-remark truncate">{{ record.remark }}</div>
</div>
<div class="record-amount">¥{{ formatAmount(record.paymentAmt) }}</div>
</div>
<div class="record-footer">
<div class="record-pay-time">支付时间: {{ record.payTime }}</div>
<div class="record-pay-method">{{ record.payModeName }}</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
import { Toast } from 'vant';
import api from '@/api/payment';
export default {
name: 'PaymentRecords',
data() {
return {
refreshing: false,
loading: false,
finished: false,
pageNum: 1,
pageSize: 10,
paymentRecords: [], //
feeTypeId: 0,
feeTypeOptions: [
{ text: '全部费用类型', value: 0 },
{ text: '水费', value: 1 },
{ text: '电费', value: 2 },
{ text: '物业费', value: 3 },
{ text: '停车费', value: 4 },
],
startDate: '',
endDate: '',
startDateObj: new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1),
endDateObj: new Date(),
showStartDatePicker: false,
showEndDatePicker: false,
};
},
created() {
//
this.formatDates();
this.loadData();
},
methods: {
//
formatDates() {
this.startDate = this.formatDateString(this.startDateObj);
this.endDate = this.formatDateString(this.endDateObj);
},
// yyyy-MM-dd
formatDateString(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
//
onStartDateConfirm(date) {
this.startDateObj = date;
this.startDate = this.formatDateString(date);
this.showStartDatePicker = false;
},
//
onEndDateConfirm(date) {
this.endDateObj = date;
this.endDate = this.formatDateString(date);
this.showEndDatePicker = false;
},
//
confirmDateFilter() {
this.$refs.dateFilter.toggle();
this.resetList();
this.loadData();
},
//
resetDateFilter() {
this.startDateObj = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1);
this.endDateObj = new Date();
this.formatDates();
},
//
resetList() {
this.pageNum = 1;
this.finished = false;
this.paymentRecords = [];
},
//
resetData() {
this.resetList();
this.feeTypeId = 0;
this.resetDateFilter();
},
//
async loadData() {
try {
this.loading = true;
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
payerId: '1', // 使ID
feeTypeId: this.feeTypeId || undefined,
startDate: this.startDate,
endDate: this.endDate,
};
const response = await api.getPaymentRecords(params);
if (response.code === '0000000000000000') {
const data = response.data;
const records = data.list || [];
//
this.paymentRecords = [...this.paymentRecords, ...records];
//
if (this.pageNum * this.pageSize >= data.total) {
this.finished = true;
} else {
this.pageNum++;
}
} else {
Toast.fail(response.message || '获取缴费记录失败');
this.finished = true;
}
} catch (error) {
Toast.fail('获取缴费记录失败');
this.finished = true;
} finally {
this.loading = false;
this.refreshing = false;
}
},
//
onRefresh() {
this.resetList();
this.loadData();
},
//
onLoad() {
this.loadData();
},
//
goBack() {
this.$router.go(-1);
},
//
viewRecordDetail(billId) {
this.$router.push(`/payment/record-detail/${billId}`);
},
//
formatAmount(amount) {
return parseFloat(amount).toFixed(2);
},
//
getStatusText(status) {
switch (status) {
case '1': return '已结清';
case '2': return '部分结清';
default: return '未知状态';
}
},
//
getStatusClass(status) {
switch (status) {
case '1': return 'status-success';
case '2': return 'status-warning';
default: return '';
}
},
},
};
</script>
<style lang="scss" scoped>
.payment-records-page {
padding-bottom: 20px;
background-color: #f5f5f5;
}
.filter-bar {
position: sticky;
top: 0;
z-index: 10;
}
.date-filter {
padding: 12px 16px;
.filter-buttons {
display: flex;
justify-content: space-between;
margin-top: 16px;
.van-button {
flex: 1;
margin: 0 4px;
}
}
}
.empty-data {
padding: 40px 0;
}
.record-list {
margin-top: 10px;
}
.record-item {
margin: 0 12px 12px;
padding: 12px 16px;
background-color: #fff;
border-radius: 8px;
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.record-type {
font-size: 16px;
font-weight: 500;
}
.record-status {
font-size: 14px;
&.status-success {
color: var(--success-color);
}
&.status-warning {
color: var(--warning-color);
}
}
}
.record-content {
display: flex;
margin-bottom: 8px;
.record-info {
flex: 1;
.record-date {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.record-remark {
font-size: 13px;
color: #999;
max-width: 220px;
}
}
.record-amount {
font-size: 16px;
font-weight: 500;
color: #ee0a24;
}
}
.record-footer {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
}
</style>

71
h5/vue.config.js Normal file
View File

@ -0,0 +1,71 @@
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
// 部署应用包时的基本 URL
publicPath: process.env.NODE_ENV === 'production' ? '/' : '/',
// 当运行 vue-cli-service build 时生成的生产环境构建文件的目录
outputDir: 'dist',
// 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录
assetsDir: 'static',
// 指定生成的 index.html 的输出路径 (相对于 outputDir)
indexPath: 'index.html',
// 默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存
filenameHashing: true,
// 是否在开发环境下通过 eslint-loader 在每次保存时 lint 代码
lintOnSave: process.env.NODE_ENV !== 'production',
// 如果你不需要生产环境的 source map可以将其设置为 false 以加速生产环境构建
productionSourceMap: false,
// 开发服务器配置
devServer: {
host: '0.0.0.0',
port: 8080,
open: true,
// proxy: {
// // 配置跨域
// '/': {
// target: 'http://192.168.137.45:8080',
// changeOrigin: true,
// pathRewrite: {
// '^/api': ''
// }
// }
// }
},
// CSS相关配置
css: {
// 是否使用css分离插件 ExtractTextPlugin
extract: process.env.NODE_ENV === 'production',
// 开启 CSS source maps?
sourceMap: false,
// css预设器配置项
loaderOptions: {
sass: {
// 全局引入变量和 mixin
additionalData: `
@import "@/assets/styles/variables.scss";
`
}
}
},
// webpack配置
configureWebpack: {
// 配置 webpack 压缩
optimization: {
minimize: process.env.NODE_ENV === 'production'
}
},
// 第三方插件配置
pluginOptions: {
// ...
}
});

View File

@ -37,6 +37,7 @@
<script> <script>
import { getLabelConfig } from '@/api/asset/inventory' import { getLabelConfig } from '@/api/asset/inventory'
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue'
import QRCode from 'qrcode' // qrcode
import { API_SUCCESS_CODE } from '@/utils/constants' import { API_SUCCESS_CODE } from '@/utils/constants'
export default { export default {
@ -259,126 +260,153 @@ export default {
} }
` `
// HTML // URL
printWindow.document.write('<html><head><title>资产标签打印</title>') const qrPromises = assetsToUse.map(asset => {
printWindow.document.write('<style>' + styleContent + '</style>')
printWindow.document.write('<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"><\/script>')
printWindow.document.write('</head><body>')
//
printWindow.document.write('<div class="print-header">' + this.currentDate + ' ' + this.title + '</div>')
printWindow.document.write('<div class="label-grid">')
//
assetsToUse.forEach(asset => {
const assetId = asset.id || asset.assetId const assetId = asset.id || asset.assetId
return QRCode.toDataURL(assetId.toString(), {
errorCorrectionLevel: 'H',
margin: 1,
width: 80,
color: {
dark: '#000',
light: '#fff'
}
}).then(url => {
return { id: assetId, dataUrl: url }
}).catch(err => {
console.error('二维码生成错误:', err)
return { id: assetId, dataUrl: null }
})
})
//
Promise.all(qrPromises).then(qrCodes => {
// 便
const qrCodeMap = {}
qrCodes.forEach(qr => {
qrCodeMap[qr.id] = qr.dataUrl
})
printWindow.document.write('<div class="label-item">') // HTML
printWindow.document.write('<div class="label-content">') printWindow.document.write('<html><head><title>资产标签打印</title>')
printWindow.document.write('<div class="qrcode-section">') printWindow.document.write('<style>' + styleContent + '</style>')
// printWindow.document.write('<div class="asset-icon"></div>') printWindow.document.write('</head><body>')
printWindow.document.write('<div class="qrcode" id="qrcode-' + assetId + '"></div>')
// printWindow.document.write('<div class="asset-code">: ' + (asset.assetCode || asset.code) + '</div>')
printWindow.document.write('</div>')
printWindow.document.write('<div class="info-section">') //
printWindow.document.write('<div class="print-header">' + this.currentDate + ' ' + this.title + '</div>')
printWindow.document.write('<div class="label-grid">')
// //
this.displayFields.forEach(field => { assetsToUse.forEach(asset => {
// const assetId = asset.id || asset.assetId
console.log('Checking field:', field.key, 'in asset:', asset) const qrDataUrl = qrCodeMap[assetId]
// printWindow.document.write('<div class="label-item">')
let value = asset[field.key]; printWindow.document.write('<div class="label-content">')
printWindow.document.write('<div class="qrcode-section">')
// printWindow.document.write('<div class="asset-icon"></div>')
// 使 if (qrDataUrl) {
if (!value && field.possibleKeys) { // 使
for (const possibleKey of field.possibleKeys) { printWindow.document.write('<div class="qrcode"><img src="' + qrDataUrl + '" alt="资产二维码" width="80" height="80"/></div>')
if (asset[possibleKey] !== undefined && asset[possibleKey] !== null) { } else {
value = asset[possibleKey]; //
console.log('Found value with alternative key:', possibleKey, value); printWindow.document.write('<div class="qrcode"><div style="width:80px;height:80px;display:flex;align-items:center;justify-content:center;border:1px solid #ddd;font-size:12px;text-align:center;">ID: ' + assetId + '</div></div>')
break;
}
}
} }
console.log('Final value:', value); // printWindow.document.write('<div class="asset-code">: ' + (asset.assetCode || asset.code) + '</div>')
printWindow.document.write('</div>')
// 使 printWindow.document.write('<div class="info-section">')
printWindow.document.write('<div class="info-item">')
printWindow.document.write('<span>' + field.label + ': ' + (value || '-') + '</span>') //
this.displayFields.forEach(field => {
//
console.log('Checking field:', field.key, 'in asset:', asset)
//
let value = asset[field.key];
// 使
if (!value && field.possibleKeys) {
for (const possibleKey of field.possibleKeys) {
if (asset[possibleKey] !== undefined && asset[possibleKey] !== null) {
value = asset[possibleKey];
console.log('Found value with alternative key:', possibleKey, value);
break;
}
}
}
console.log('Final value:', value);
// 使
printWindow.document.write('<div class="info-item">')
printWindow.document.write('<span>' + field.label + ': ' + (value || '-') + '</span>')
printWindow.document.write('</div>')
})
printWindow.document.write('</div>')
printWindow.document.write('</div>')
printWindow.document.write('</div>') printWindow.document.write('</div>')
}) })
printWindow.document.write('</div>') printWindow.document.write('</div>')
printWindow.document.write('</div>')
printWindow.document.write('</div>') //
}) printWindow.document.write('<script>')
printWindow.document.write('</div>') //
printWindow.document.write('window.onload = function() {')
//
printWindow.document.write('<script>') //
printWindow.document.write('window.onload = function() {')
//
assetsToUse.forEach(asset => {
const assetId = asset.id || asset.assetId
printWindow.document.write(` printWindow.document.write(`
(function() { //
var typeNumber = 0; setTimeout(function() {
var errorCorrectionLevel = "H"; //
var qr = qrcode(typeNumber, errorCorrectionLevel); var mediaQueryList = window.matchMedia('print');
qr.addData("${assetId}");
qr.make(); //
document.getElementById("qrcode-${assetId}").innerHTML = qr.createImgTag(3); mediaQueryList.addListener(function(mql) {
})(); if (!mql.matches) {
//
setTimeout(function() {
window.close();
}, 100);
}
});
//
window.print();
//
var cancelBtn = document.createElement('button');
cancelBtn.innerText = '关闭窗口';
cancelBtn.style.display = 'block';
cancelBtn.style.margin = '20px auto';
cancelBtn.style.padding = '10px 20px';
cancelBtn.style.backgroundColor = '#409EFF';
cancelBtn.style.color = 'white';
cancelBtn.style.border = 'none';
cancelBtn.style.borderRadius = '4px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.onclick = function() {
window.close();
};
document.body.appendChild(cancelBtn);
}, 500);
`) `)
printWindow.document.write('}')
printWindow.document.write('<\/script>')
printWindow.document.write('</body></html>')
printWindow.document.close()
}).catch(err => {
console.error('生成二维码时出错:', err)
printWindow.close()
this.$message.error('生成二维码时出错,请重试')
}) })
//
printWindow.document.write(`
//
setTimeout(function() {
//
var mediaQueryList = window.matchMedia('print');
//
mediaQueryList.addListener(function(mql) {
if (!mql.matches) {
//
setTimeout(function() {
window.close();
}, 100);
}
});
//
window.print();
//
var cancelBtn = document.createElement('button');
cancelBtn.innerText = '关闭窗口';
cancelBtn.style.display = 'block';
cancelBtn.style.margin = '20px auto';
cancelBtn.style.padding = '10px 20px';
cancelBtn.style.backgroundColor = '#409EFF';
cancelBtn.style.color = 'white';
cancelBtn.style.border = 'none';
cancelBtn.style.borderRadius = '4px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.onclick = function() {
window.close();
};
document.body.appendChild(cancelBtn);
}, 500);
`)
printWindow.document.write('}')
printWindow.document.write('<\/script>')
printWindow.document.write('</body></html>')
printWindow.document.close()
}, },
/** 取消按钮 */ /** 取消按钮 */

View File

@ -641,17 +641,19 @@ export default {
handleDownloadTemplate(row) { handleDownloadTemplate(row) {
downloadReceiptTemplate(row.id).then(response => { downloadReceiptTemplate(row.id).then(response => {
// //
const blob = new Blob([response.data], { this.download(response, `${row.templateName}.docx`)
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}) // const blob = new Blob([response.data], {
const url = URL.createObjectURL(blob) // type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
const link = document.createElement('a') // })
link.href = url // const url = URL.createObjectURL(blob)
link.setAttribute('download', `${row.templateName}.docx`) // const link = document.createElement('a')
document.body.appendChild(link) // link.href = url
link.click() // link.setAttribute('download', `${row.templateName}.docx`)
document.body.removeChild(link) // document.body.appendChild(link)
URL.revokeObjectURL(url) // link.click()
// document.body.removeChild(link)
// URL.revokeObjectURL(url)
}).catch(error => { }).catch(error => {
console.error('下载模板失败', error) console.error('下载模板失败', error)
this.$message.error('下载模板失败') this.$message.error('下载模板失败')
@ -662,13 +664,14 @@ export default {
handleDownloadExample() { handleDownloadExample() {
downloadExampleTemplate().then(response => { downloadExampleTemplate().then(response => {
// //
const url = window.URL.createObjectURL(new Blob([response.data])) this.download(response, '收据模板样例.docx')
const link = document.createElement('a') // const url = window.URL.createObjectURL(new Blob([response.data]))
link.href = url // const link = document.createElement('a')
link.setAttribute('download', '收据模板样例.docx') // link.href = url
document.body.appendChild(link) // link.setAttribute('download', '.docx')
link.click() // document.body.appendChild(link)
document.body.removeChild(link) // link.click()
// document.body.removeChild(link)
}).catch(error => { }).catch(error => {
console.error('下载样例模板失败', error) console.error('下载样例模板失败', error)
this.$message.error('下载样例模板失败') this.$message.error('下载样例模板失败')

BIN
园区前端.zip Normal file

Binary file not shown.

BIN
园区前端/h5.zip Normal file

Binary file not shown.

BIN
园区前端/pc.zip Normal file

Binary file not shown.