feat(daxpay): 优化收银台支付功能

- 新增条码支付和聚合支付功能
- 优化支付流程和用户界面
This commit is contained in:
DaxPay
2024-12-05 14:37:52 +08:00
parent aea858a6fc
commit d2ca977cf2
9 changed files with 407 additions and 215 deletions

View File

@@ -137,7 +137,7 @@
<div class="app-loading-dots">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
<div class="app-loading-title"><%= VITE_GLOB_APP_TITLE %></div>
<div class="app-loading-title">DaxPay</div>
</div>
</div>
</div>

View File

@@ -36,6 +36,22 @@ export const LoginRoute: AppRouteRecordRaw = {
},
}
// Basic routing without permission
/**
* 外部页面, 不需要登陆
*/
export const OUTSIDE: AppRouteModule = {
path: '/not/login/outside/',
name: 'NOT_LOGIN_OUTSIDE',
meta: { title: '' },
children: [
{
path: '/checkout/:orderNo',
name: 'CheckoutPay',
component: () => import('@/views/daxpay/outside/checkout/CheckoutPay.vue'),
meta: { title: '收银台', ignoreAuth: true },
},
],
}
// 未经许可的基本路由
export const basicRoutes = [LoginRoute, RootRoute, REDIRECT_ROUTE, PAGE_NOT_FOUND_ROUTE]
export const basicRoutes = [LoginRoute, RootRoute, REDIRECT_ROUTE, PAGE_NOT_FOUND_ROUTE,OUTSIDE]

View File

@@ -29,20 +29,13 @@ export const INTERNAL: AppRouteModule = {
}
/**
* 位于主框架外的页面
* 位于主框架外的页面 也需要登陆
*/
export const OUTSIDE: AppRouteModule = {
path: '/outside',
name: 'PROJECT_OUTSIDE',
meta: { title: '' },
children: [
{
path: '/checkout/:orderNo',
name: 'CheckoutPay',
component: () => import('@/views/daxpay/outside/checkout/CheckoutPay.vue'),
meta: { title: '收银台', ignoreAuth: true },
},
],
children: [],
}
/**

View File

@@ -0,0 +1,62 @@
<template>
<a-modal :open="visible" title="条码支付" :width="350" @cancel="handleClose">
<a-spin :spinning="loading">
<div>
<div
style="margin-bottom: 10px; display: flex; flex-direction: row; justify-content: start"
>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between">
<a-input allowClear v-model:value="authCode" placeholder="请输入付款条码" />
</div>
</div>
</a-spin>
<template #footer>
<a-button key="cancel" @click="handleClose">取消</a-button>
<a-button key="forward" :loading="loading" type="primary" @click="handleOk">确定</a-button>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
let visible = ref(false)
let aggregate = ref(false)
let loading = ref(false)
let authCode = ref('')
const emits = defineEmits(['ok', 'cancel'])
/**
* 初始化
*/
function init(isAggregate: boolean) {
visible.value = true
loading.value = false
aggregate.value = isAggregate
authCode.value = ''
}
/**
* 回调
*/
function handleOk() {
if (authCode.value) {
loading.value = true
emits('ok', authCode.value, aggregate.value)
} else {
handleClose()
}
}
/**
* 关闭
*/
function handleClose() {
visible.value = false
}
defineExpose({ init, handleClose })
</script>
<style scoped></style>

View File

@@ -1,6 +1,6 @@
import { defHttp } from '@/utils/http/axios'
import { Result } from '#/axios'
import {PayResult} from "@/views/daxpay/common/develop/trade/DevelopTrade.api";
import { PayResult } from '@/views/daxpay/common/develop/trade/DevelopTrade.api'
/**
* 获取收银台订单和配置信息
@@ -33,7 +33,25 @@ export function checkoutPay(param: CheckoutPayParam) {
})
}
/**
* 聚合条码支付
*/
export function aggregateBarPay(param: AggregateBarPayParam) {
return defHttp.post<Result<PayResult>>({
url: '/unipay/checkout/aggregateBarPay',
data: param,
})
}
/**
* 查询订单状态
*/
export function findStatusByOrderNo(orderNo) {
return defHttp.get<Result<boolean>>({
url: '/unipay/checkout/findStatusByOrderNo',
params: { orderNo },
})
}
/**
* 收银台支付参数
@@ -49,6 +67,15 @@ export interface CheckoutPayParam {
barCode?: string
}
/**
* 聚合条码支付参数
*/
export interface AggregateBarPayParam {
/** 订单号 */
orderNo?: string
/** 付款码 */
barCode?: string
}
/**
* 收银台配置
*/
@@ -61,18 +88,6 @@ export interface CheckoutOrderAndConfigResult {
groupConfigs: CheckoutGroupConfigResult[]
}
/**
* 收银台聚合支付配置
*/
export interface AggregateOrderAndConfigResult {
/** 订单信息 */
order: CheckoutOrderResult
/** 收银台配置信息 */
config: CheckoutConfigResult
/** 收银台聚合配置信息 */
aggregateConfig: AggregateConfigResult
}
/**
* 订单信息
*/
@@ -87,7 +102,6 @@ export interface CheckoutOrderResult {
description?: string
/** 金额(元) */
amount?: string
}
/**
* 收银台配置信息
@@ -130,17 +144,3 @@ export interface CheckoutItemConfigResult {
/** 支付方式 */
payMethod?: string
}
/**
* 收银台聚合配置信息
*/
export interface AggregateConfigResult {
/** 支付类型 */
type?: string
/** 通道 */
channel?: string
/** 支付方式 */
payMethod?: string
/** 自动拉起支付 */
autoLaunch?: boolean
}

View File

@@ -1,53 +1,71 @@
<template>
<div>
<div class="page paydemo">
<!-- 云闪付表单方式专用 -->
<div v-html="payForm"></div>
<div class="page pay">
<div class="blog-container" id="container">
<a-spin :spinning="loading">
<div class="content" style="padding-top: 70px">
<div style="width: 100%">
<div class="paydemo-type-content" style="display: flex;height: 300px">
<div style="width: 50%;">
<div class="paydemo-type-name">支付信息</div>
<div class="pay-type-content" style="display: flex; height: 300px">
<div style="width: 50%">
<div class="pay-type-name">支付信息</div>
<a-form>
<div class="paydemo-form-item">
<div class="pay-form-item">
<label>标题</label>
<span>
{{ orderAndConfig.order.title }}
</span>
</div>
<div class="paydemo-form-item">
<div class="pay-form-item">
<label>商户订单号</label>
<span>
{{ orderAndConfig.order.bizOrderNo }}
</span>
</div>
<div class="paydemo-form-item">
<div class="pay-form-item">
<label>订单号</label>
<span>
{{ orderAndConfig.order.orderNo }}
</span>
</div>
<div class="paydemo-form-item">
<div class="pay-form-item">
<label>描述</label>
<span>
{{ orderAndConfig.order.description }}
</span>
</div>
</a-form>
</div>
<div style="width: 50%;display: flex;flex-direction:column;align-items: center" v-if="aggregateUrl">
<a-alert message="请使用支付宝、微信等应用扫码支付" type="info" style="width: 250px;margin-bottom: 10px"/>
<div
style="width: 50%; display: flex; flex-direction: column; align-items: center"
v-if="aggregateUrl"
>
<a-alert
message="请使用支付宝、微信等应用扫码支付"
type="info"
style="width: 250px; margin-bottom: 10px"
/>
<a-qrcode :value="aggregateUrl" :size="200" />
<a-button style="margin-top: 10px" @click="barPayShow(true)"
>付款码支付(聚合)</a-button
>
</div>
</div>
<div class="paydemo-type-content" style="height: 350px">
<div class="pay-type-content" style="height: 350px">
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane v-for="group in orderAndConfig.groupConfigs" :key="group.id" :tab="group.name">
<div class="paydemo-type-body">
<div @click="()=>currentItem=config" v-for="config in group.items" :key="config.id">
<div :class="config.id === currentItem.id ? 'colorChange' : 'paydemoType'">
<a-tab-pane
v-for="group in orderAndConfig.groupConfigs"
:key="group.id"
:tab="group.name"
>
<div class="pay-type-body">
<div
@click="() => (currentItem = config)"
v-for="config in group.items"
:key="config.id"
>
<div :class="config.id === currentItem.id ? 'colorChange' : 'payType'">
<span class="color-change">{{ config.name }}</span>
</div>
</div>
@@ -56,46 +74,57 @@
</a-tabs>
</div>
<div style="margin-top: 20px; text-align: right">
<span style="color: #fd482c; font-size: 18px; padding-right: 10px">{{ '¥ ' + orderAndConfig.order.amount }} </span>
<span style="color: #fd482c; font-size: 18px; padding-right: 10px"
>{{ '¥ ' + orderAndConfig.order.amount }} </span
>
<a-button type="primary" :disabled="!currentItem" @click="pay">立即支付</a-button>
</div>
</div>
</div>
</a-spin>
</div>
<checkout-bar-code ref="cashierBarCode" @ok="barPay" />
<checkout-qr-code ref="cashierQrCode" />
</div>
</div>
</template>
<script setup lang="ts">
import {useRoute} from "vue-router";
import {onMounted, ref} from "vue";
import { useRoute } from 'vue-router'
import { nextTick, onMounted, ref } from 'vue'
import {
aggregateBarPay,
AggregateBarPayParam,
CheckoutItemConfigResult,
CheckoutOrderAndConfigResult, checkoutPay, CheckoutPayParam, getCheckoutUrl,
getOrderAndConfig
} from "./CheckoutPay.api";
import {CheckoutCallTypeEnum, CheckoutTypeEnum,} from "@/enums/daxpay/channelEnum";
import {useMessage} from "@/hooks/web/useMessage";
CheckoutOrderAndConfigResult,
checkoutPay,
CheckoutPayParam,
findStatusByOrderNo,
getCheckoutUrl,
getOrderAndConfig,
} from './CheckoutPay.api'
import { CheckoutCallTypeEnum, CheckoutTypeEnum } from '@/enums/daxpay/channelEnum'
import { useMessage } from '@/hooks/web/useMessage'
import { PayResult } from '@/views/daxpay/common/develop/trade/DevelopTrade.api'
import { useIntervalFn } from '@vueuse/shared'
import CheckoutBarCode from './CheckoutBarCode.vue'
import CheckoutQrCode from './CheckoutQrCode.vue'
const route = useRoute()
const { orderNo } = route.params
const activeKey = ref<string>()
const { createMessage } = useMessage()
let payForm = ref<string>()
const aggregateUrl = ref('1')
const aggregateUrl = ref('')
const cashierQrCode = ref<any>()
const cashierBarCode = ref<any>()
// 当前选择支付渠道和方式
let currentItem = ref<CheckoutItemConfigResult>({})
const loading = ref(false)
const orderAndConfig = ref<CheckoutOrderAndConfigResult>({
order: {},
@@ -103,10 +132,31 @@ const orderAndConfig = ref<CheckoutOrderAndConfigResult>({
groupConfigs: [],
})
// 检查支付状态
const { pause, resume } = useIntervalFn(
() => {
findStatusByOrderNo(orderNo as string)
.then((res) => {
// 成功
if (res.data) {
createMessage.success('支付成功')
handleCancel()
}
})
.catch(() => {
// 失败
handleCancel()
})
},
1000 * 3,
{ immediate: false },
)
/**
* 初始化
*/
onMounted(() => {
resume()
initData()
})
@@ -118,7 +168,8 @@ async function initData() {
await getOrderAndConfig(orderNo, CheckoutTypeEnum.PC).then(({ data }) => {
orderAndConfig.value = data
activeKey.value = orderAndConfig.value.groupConfigs?.[0].id
currentItem.value = orderAndConfig.value.groupConfigs?.[0].items?.[0] as CheckoutItemConfigResult
currentItem.value = orderAndConfig.value.groupConfigs?.[0]
.items?.[0] as CheckoutItemConfigResult
})
// 是否同时显示聚合支付码
if (orderAndConfig.value.config.aggregateShow) {
@@ -128,33 +179,21 @@ async function initData() {
}
}
/**
* 条码支付框显示
*/
function barPayShow(aggregate) {
cashierBarCode.value.init(aggregate)
}
/**
* 发起支付
*/
function pay() {
// 条码支付
if (CheckoutCallTypeEnum.BAR_CODE === currentItem.value.callType) {
}
// 支付调起类型
switch (currentItem.value.callType){
case CheckoutCallTypeEnum.LINK:{
break;
}
case CheckoutCallTypeEnum.FROM:{
break;
}
case CheckoutCallTypeEnum.QR_CODE:{
break;
}
case CheckoutCallTypeEnum.BAR_CODE:{
break;
}
default:{
createMessage.warning('暂不支持该支付方式')
}
barPayShow(false)
return
}
loading.value = true
@@ -164,13 +203,75 @@ function pay() {
} as CheckoutPayParam
checkoutPay(from).then(({ data }) => {
loading.value = false
// 跳转到支付页面
location.replace(data.payBody as string)
payCall(data)
})
}
</script>
/**
* 支付调起操作
*/
function payCall(data: PayResult) {
// 支付调起类型
switch (currentItem.value.callType) {
// 跳转到支付页面
case CheckoutCallTypeEnum.LINK: {
window.location.href = data.payBody as string
break
}
// 本地提交提交支付表单
case CheckoutCallTypeEnum.FROM: {
payForm.value = data.payBody
nextTick(() => {
console.log(document.forms[0])
document.forms[0].submit()
})
break
}
// 扫码支付
case CheckoutCallTypeEnum.QR_CODE: {
cashierQrCode.value.init(data.payBody)
break
}
default: {
createMessage.warning('暂不支持该支付方式')
}
}
}
/**
* 条码支付
*/
function barPay(barCode: string, aggregate: boolean) {
if (aggregate) {
const param = {
orderNo,
barCode,
} as AggregateBarPayParam
aggregateBarPay(param).then(() => {
cashierBarCode.value.handleClose()
})
} else {
const param = {
orderNo,
itemId: currentItem.value,
barCode,
} as CheckoutPayParam
checkoutPay(param).then(() => {
cashierBarCode.value.handleClose()
})
}
}
/**
* 关闭
*/
function handleCancel() {
cashierQrCode.value.handleClose()
cashierBarCode.value.handleClose()
loading.value = false
pause()
}
</script>
<style scoped lang="less">
@import 'style.less';

View File

@@ -0,0 +1,36 @@
<template>
<a-modal :open="visible" title="扫码支付" @cancel="handleCancel" :footer="null" :width="250">
<div style="display: flex; flex-direction: column; align-items: center; padding-top: 15px">
<qr-code :options="{ margin: 0 }" :width="180" :value="qrUrl" />
<div
class="mt-15px mb-15px"
style="display: flex; flex-direction: row; align-items: center; justify-content: center"
>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import QrCode from '/@/components/Qrcode/src/Qrcode.vue'
import { ref } from 'vue'
let visible = ref(false)
let qrUrl = ref('')
const emits = defineEmits(['cancel'])
function init(url:string) {
visible.value = true
qrUrl.value = url
}
function handleCancel() {
handleClose()
emits('cancel')
}
function handleClose() {
visible.value = false
}
defineExpose({ init, handleClose })
</script>
<style scoped></style>

View File

@@ -1,15 +1,15 @@
.page {
box-sizing: border-box;
min-height: 100%;
//padding-bottom: 80px;
//margin-top: 81px;
padding-bottom: 80px;
margin-top: 81px;
}
.paydemo {
.pay {
.content {
max-width: 1120px;
margin: 0 auto;
}
.paydemo-type-content {
.pay-type-content {
padding: 20px;
margin-bottom: 10px;
background-color: #FFFFFF;
@@ -26,46 +26,30 @@
margin-right: 10px;
cursor: pointer;
}
.paydemoType {
.payType {
padding: 12px;
border: solid 2px #e2e2e2;
margin-right: 10px;
cursor: pointer;
}
.paydemo-type-img {
.pay-type-img {
width: 40px;
height: 40px;
vertical-align: center;
}
.paydemo-type-name {
.pay-type-name {
font-size: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
}
.paydemo-type-body {
.pay-type-body {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.paydemo-type-h5 {
padding: 12px;
border: solid 2px #e2e2e2;
margin-right: 10px;
cursor: pointer;
font-size: 14px;
}
.codeImg_wx_h5 {
position: absolute;
z-index: 1001;
display: flex;
flex-direction: column;
align-items: center;
background-color: #ffffff;
width: 160px;
}
.paydemo-form-item {
.pay-form-item {
height: 38px;
margin-bottom: 5px;
display: flex;