初始化项目 #1
102
h5/README.md
Normal file
102
h5/README.md
Normal 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
5
h5/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
};
|
81
h5/package.json
Normal file
81
h5/package.json
Normal 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
16
h5/postcss.config.js
Normal 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
36
h5/public/index.html
Normal 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
23
h5/src/App.vue
Normal 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
50
h5/src/api/payment.js
Normal 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
15
h5/src/api/user.js
Normal 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;
|
268
h5/src/assets/styles/index.scss
Normal file
268
h5/src/assets/styles/index.scss
Normal 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);
|
||||
}
|
101
h5/src/assets/styles/variables.scss
Normal file
101
h5/src/assets/styles/variables.scss
Normal 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;
|
||||
}
|
||||
}
|
81
h5/src/components/PayerSelector.vue
Normal file
81
h5/src/components/PayerSelector.vue
Normal 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
10
h5/src/env.development.js
Normal file
@ -0,0 +1,10 @@
|
||||
// 开发环境配置
|
||||
export default {
|
||||
NODE_ENV: 'development',
|
||||
|
||||
// API 基础路径
|
||||
BASE_API: '/api',
|
||||
|
||||
// 页面标题
|
||||
TITLE: '在线缴费系统'
|
||||
};
|
10
h5/src/env.production.js
Normal file
10
h5/src/env.production.js
Normal file
@ -0,0 +1,10 @@
|
||||
// 生产环境配置
|
||||
export default {
|
||||
NODE_ENV: 'production',
|
||||
|
||||
// API 基础路径
|
||||
BASE_API: '/api',
|
||||
|
||||
// 页面标题
|
||||
TITLE: '在线缴费系统'
|
||||
};
|
25
h5/src/main.js
Normal file
25
h5/src/main.js
Normal 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
75
h5/src/router/index.js
Normal 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
26
h5/src/store/index.js
Normal 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,
|
||||
},
|
||||
});
|
217
h5/src/store/modules/payment.js
Normal file
217
h5/src/store/modules/payment.js
Normal 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,
|
||||
};
|
107
h5/src/store/modules/user.js
Normal file
107
h5/src/store/modules/user.js
Normal 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
164
h5/src/utils/request.js
Normal 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;
|
236
h5/src/views/payment/BillDetail.vue
Normal file
236
h5/src/views/payment/BillDetail.vue
Normal 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>
|
296
h5/src/views/payment/EnterprisePay.vue
Normal file
296
h5/src/views/payment/EnterprisePay.vue
Normal 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>
|
619
h5/src/views/payment/Index.vue
Normal file
619
h5/src/views/payment/Index.vue
Normal 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>
|
295
h5/src/views/payment/PayResult.vue
Normal file
295
h5/src/views/payment/PayResult.vue
Normal 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>
|
602
h5/src/views/payment/PrepayBills.vue
Normal file
602
h5/src/views/payment/PrepayBills.vue
Normal 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>
|
286
h5/src/views/payment/RecordDetail.vue
Normal file
286
h5/src/views/payment/RecordDetail.vue
Normal 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>
|
378
h5/src/views/payment/Records.vue
Normal file
378
h5/src/views/payment/Records.vue
Normal 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
71
h5/vue.config.js
Normal 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: {
|
||||
// ...
|
||||
}
|
||||
});
|
@ -37,6 +37,7 @@
|
||||
<script>
|
||||
import { getLabelConfig } from '@/api/asset/inventory'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import QRCode from 'qrcode' // 导入 qrcode 包
|
||||
import { API_SUCCESS_CODE } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
@ -259,126 +260,153 @@ export default {
|
||||
}
|
||||
`
|
||||
|
||||
// 生成HTML内容
|
||||
printWindow.document.write('<html><head><title>资产标签打印</title>')
|
||||
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 => {
|
||||
// 预先生成所有的二维码数据URL
|
||||
const qrPromises = assetsToUse.map(asset => {
|
||||
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 }
|
||||
})
|
||||
})
|
||||
|
||||
printWindow.document.write('<div class="label-item">')
|
||||
printWindow.document.write('<div class="label-content">')
|
||||
printWindow.document.write('<div class="qrcode-section">')
|
||||
// printWindow.document.write('<div class="asset-icon">资产</div>')
|
||||
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>')
|
||||
// 等待所有二维码生成完成
|
||||
Promise.all(qrPromises).then(qrCodes => {
|
||||
// 将二维码转换为键值对对象以便快速查找
|
||||
const qrCodeMap = {}
|
||||
qrCodes.forEach(qr => {
|
||||
qrCodeMap[qr.id] = qr.dataUrl
|
||||
})
|
||||
|
||||
printWindow.document.write('<div class="info-section">')
|
||||
// 生成HTML内容
|
||||
printWindow.document.write('<html><head><title>资产标签打印</title>')
|
||||
printWindow.document.write('<style>' + styleContent + '</style>')
|
||||
printWindow.document.write('</head><body>')
|
||||
|
||||
// 添加显示字段
|
||||
this.displayFields.forEach(field => {
|
||||
// 添加调试输出
|
||||
console.log('Checking field:', field.key, 'in asset:', asset)
|
||||
// 添加标题
|
||||
printWindow.document.write('<div class="print-header">' + this.currentDate + ' ' + this.title + '</div>')
|
||||
printWindow.document.write('<div class="label-grid">')
|
||||
|
||||
// 尝试从资产对象中获取字段值
|
||||
let value = asset[field.key];
|
||||
// 添加每个资产的标签
|
||||
assetsToUse.forEach(asset => {
|
||||
const assetId = asset.id || asset.assetId
|
||||
const qrDataUrl = qrCodeMap[assetId]
|
||||
|
||||
// 如果没有找到值,尝试使用可能的键名列表
|
||||
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;
|
||||
}
|
||||
}
|
||||
printWindow.document.write('<div class="label-item">')
|
||||
printWindow.document.write('<div class="label-content">')
|
||||
printWindow.document.write('<div class="qrcode-section">')
|
||||
// printWindow.document.write('<div class="asset-icon">资产</div>')
|
||||
|
||||
if (qrDataUrl) {
|
||||
// 使用预生成的二维码图像
|
||||
printWindow.document.write('<div class="qrcode"><img src="' + qrDataUrl + '" alt="资产二维码" width="80" height="80"/></div>')
|
||||
} else {
|
||||
// 如果生成失败,显示后备内容
|
||||
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>')
|
||||
}
|
||||
|
||||
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-item">')
|
||||
printWindow.document.write('<span>' + field.label + ': ' + (value || '-') + '</span>')
|
||||
printWindow.document.write('<div class="info-section">')
|
||||
|
||||
// 添加显示字段
|
||||
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('<script>')
|
||||
|
||||
// 添加生成二维码和打印的脚本
|
||||
printWindow.document.write('<script>')
|
||||
printWindow.document.write('window.onload = function() {')
|
||||
// 当页面完全加载后执行
|
||||
printWindow.document.write('window.onload = function() {')
|
||||
|
||||
// 生成二维码
|
||||
assetsToUse.forEach(asset => {
|
||||
const assetId = asset.id || asset.assetId
|
||||
// 添加自动打印代码并在打印完成后关闭窗口
|
||||
printWindow.document.write(`
|
||||
(function() {
|
||||
var typeNumber = 0;
|
||||
var errorCorrectionLevel = "H";
|
||||
var qr = qrcode(typeNumber, errorCorrectionLevel);
|
||||
qr.addData("${assetId}");
|
||||
qr.make();
|
||||
document.getElementById("qrcode-${assetId}").innerHTML = qr.createImgTag(3);
|
||||
})();
|
||||
// 给页面一点时间渲染
|
||||
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()
|
||||
}).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()
|
||||
},
|
||||
|
||||
/** 取消按钮 */
|
||||
|
BIN
pc/src/views/asset/inventory/components/AssetLabelPrint.zip
Normal file
BIN
pc/src/views/asset/inventory/components/AssetLabelPrint.zip
Normal file
Binary file not shown.
@ -641,17 +641,19 @@ export default {
|
||||
handleDownloadTemplate(row) {
|
||||
downloadReceiptTemplate(row.id).then(response => {
|
||||
// 处理文件下载
|
||||
const blob = new Blob([response.data], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', `${row.templateName}.docx`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
this.download(response, `${row.templateName}.docx`)
|
||||
|
||||
// const blob = new Blob([response.data], {
|
||||
// type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
// })
|
||||
// const url = URL.createObjectURL(blob)
|
||||
// const link = document.createElement('a')
|
||||
// link.href = url
|
||||
// link.setAttribute('download', `${row.templateName}.docx`)
|
||||
// document.body.appendChild(link)
|
||||
// link.click()
|
||||
// document.body.removeChild(link)
|
||||
// URL.revokeObjectURL(url)
|
||||
}).catch(error => {
|
||||
console.error('下载模板失败', error)
|
||||
this.$message.error('下载模板失败')
|
||||
@ -662,13 +664,14 @@ export default {
|
||||
handleDownloadExample() {
|
||||
downloadExampleTemplate().then(response => {
|
||||
// 处理文件下载
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', '收据模板样例.docx')
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
this.download(response, '收据模板样例.docx')
|
||||
// const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||
// const link = document.createElement('a')
|
||||
// link.href = url
|
||||
// link.setAttribute('download', '收据模板样例.docx')
|
||||
// document.body.appendChild(link)
|
||||
// link.click()
|
||||
// document.body.removeChild(link)
|
||||
}).catch(error => {
|
||||
console.error('下载样例模板失败', error)
|
||||
this.$message.error('下载样例模板失败')
|
||||
|
BIN
园区前端/h5.zip
Normal file
BIN
园区前端/h5.zip
Normal file
Binary file not shown.
BIN
园区前端/pc.zip
Normal file
BIN
园区前端/pc.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user