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"> <div class="app-loading-dots">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span> <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div> </div>
<div class="app-loading-title"><%= VITE_GLOB_APP_TITLE %></div> <div class="app-loading-title">DaxPay</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import type { App } from 'vue' import type { App } from 'vue'
import {createRouter, createWebHistory} from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { basicRoutes } from './routes' import { basicRoutes } from './routes'
// 白名单应该包含基本静态路由 // 白名单应该包含基本静态路由

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 = { export const OUTSIDE: AppRouteModule = {
path: '/outside', path: '/outside',
name: 'PROJECT_OUTSIDE', name: 'PROJECT_OUTSIDE',
meta: { title: '' }, meta: { title: '' },
children: [ children: [],
{
path: '/checkout/:orderNo',
name: 'CheckoutPay',
component: () => import('@/views/daxpay/outside/checkout/CheckoutPay.vue'),
meta: { title: '收银台', ignoreAuth: true },
},
],
} }
/** /**

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 { defHttp } from '@/utils/http/axios'
import { Result } from '#/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'
/** /**
* 获取收银台订单和配置信息 * 获取收银台订单和配置信息
@@ -17,10 +17,10 @@ export function getOrderAndConfig(orderNo, checkoutType) {
/** /**
* 发起普通支付 * 发起普通支付
*/ */
export function getCheckoutUrl(orderNo,checkoutType) { export function getCheckoutUrl(orderNo, checkoutType) {
return defHttp.get<Result<string>>({ return defHttp.get<Result<string>>({
url: '/unipay/checkout/getCheckoutUrl', url: '/unipay/checkout/getCheckoutUrl',
params: {orderNo,checkoutType}, params: { orderNo, checkoutType },
}) })
} }
/** /**
@@ -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 barCode?: string
} }
/**
* 聚合条码支付参数
*/
export interface AggregateBarPayParam {
/** 订单号 */
orderNo?: string
/** 付款码 */
barCode?: string
}
/** /**
* 收银台配置 * 收银台配置
*/ */
@@ -61,18 +88,6 @@ export interface CheckoutOrderAndConfigResult {
groupConfigs: CheckoutGroupConfigResult[] groupConfigs: CheckoutGroupConfigResult[]
} }
/**
* 收银台聚合支付配置
*/
export interface AggregateOrderAndConfigResult {
/** 订单信息 */
order: CheckoutOrderResult
/** 收银台配置信息 */
config: CheckoutConfigResult
/** 收银台聚合配置信息 */
aggregateConfig: AggregateConfigResult
}
/** /**
* 订单信息 * 订单信息
*/ */
@@ -87,7 +102,6 @@ export interface CheckoutOrderResult {
description?: string description?: string
/** 金额(元) */ /** 金额(元) */
amount?: string amount?: string
} }
/** /**
* 收银台配置信息 * 收银台配置信息
@@ -130,17 +144,3 @@ export interface CheckoutItemConfigResult {
/** 支付方式 */ /** 支付方式 */
payMethod?: string payMethod?: string
} }
/**
* 收银台聚合配置信息
*/
export interface AggregateConfigResult {
/** 支付类型 */
type?: string
/** 通道 */
channel?: string
/** 支付方式 */
payMethod?: string
/** 自动拉起支付 */
autoLaunch?: boolean
}

View File

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

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 { .page {
box-sizing: border-box; box-sizing: border-box;
min-height: 100%; min-height: 100%;
//padding-bottom: 80px; padding-bottom: 80px;
//margin-top: 81px; margin-top: 81px;
} }
.paydemo { .pay {
.content { .content {
max-width: 1120px; max-width: 1120px;
margin: 0 auto; margin: 0 auto;
} }
.paydemo-type-content { .pay-type-content {
padding: 20px; padding: 20px;
margin-bottom: 10px; margin-bottom: 10px;
background-color: #FFFFFF; background-color: #FFFFFF;
@@ -26,46 +26,30 @@
margin-right: 10px; margin-right: 10px;
cursor: pointer; cursor: pointer;
} }
.paydemoType { .payType {
padding: 12px; padding: 12px;
border: solid 2px #e2e2e2; border: solid 2px #e2e2e2;
margin-right: 10px; margin-right: 10px;
cursor: pointer; cursor: pointer;
} }
.paydemo-type-img { .pay-type-img {
width: 40px; width: 40px;
height: 40px; height: 40px;
vertical-align: center; vertical-align: center;
} }
.paydemo-type-name { .pay-type-name {
font-size: 16px; font-size: 16px;
margin-bottom: 12px; margin-bottom: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.paydemo-type-body { .pay-type-body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
} }
.paydemo-type-h5 { .pay-form-item {
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 {
height: 38px; height: 38px;
margin-bottom: 5px; margin-bottom: 5px;
display: flex; display: flex;