feat 收银台配置和支付订单显示优化

This commit is contained in:
DaxPay
2024-09-30 17:55:50 +08:00
parent af40ab5fc1
commit a9374cf191
9 changed files with 469 additions and 96 deletions

View File

@@ -7,6 +7,7 @@ import {
Image,
Badge,
Popover,
QRCode,
InputNumber,
Empty,
Popconfirm,
@@ -55,6 +56,7 @@ export function registerGlobComp(app: App) {
app.use(Upload)
app.use(Badge)
app.use(Popover)
app.use(QRCode)
app.use(Checkbox)
app.use(List)
app.use(Space)

View File

@@ -112,7 +112,7 @@
function add() {
dictItemEdit.value.init(null, FormEditType.Add, dictInfo)
}
// 查看
// 编辑
function edit(record) {
dictItemEdit.value.init(record.id, FormEditType.Edit, dictInfo)
}

View File

@@ -208,7 +208,7 @@
* 收银配置
*/
function showCashierConfig(record) {
channelCashierConfigList.value.init(record.appId)
channelCashierConfigList.value.init(record)
}
/**

View File

@@ -5,8 +5,11 @@ import { MchEntity } from '#/web'
/**
* 配置列表
*/
export function findAll() {
return defHttp.get<Result<MchEntity>>({ url: '/channel/cashier/config/list' })
export function findAll(appId) {
return defHttp.get<Result<ChannelCashierConfig[]>>({
url: '/channel/cashier/config/findByAppId',
params: { appId },
})
}
/**
@@ -19,10 +22,30 @@ export function get(id) {
})
}
/**
* 配置是否存在
*/
export function existsByType(appId, type) {
return defHttp.get<Result<boolean>>({
url: '/channel/cashier/config/existsByType',
params: { appId, type },
})
}
/**
* 配置是否存在
*/
export function existsByTypeNotId(appId, type, id) {
return defHttp.get<Result<boolean>>({
url: '/channel/cashier/config/existsByTypeNotId',
params: { appId, type, id },
})
}
/**
* 配置保存
*/
export function save(data: ChannelCashierConfig) {
export function add(data: ChannelCashierConfig) {
return defHttp.post<Result<ChannelCashierConfig>>({
url: '/channel/cashier/config/save',
data,
@@ -44,19 +67,25 @@ export function update(data: ChannelCashierConfig) {
*/
export function remove(id) {
return defHttp.post<Result<ChannelCashierConfig>>({
url: '/channel/cashier/config/remove',
url: '/channel/cashier/config/delete',
params: { id },
})
}
/**
* 获取码牌地址
*/
export function getQrCodeUrl(appId) {
return defHttp.get<Result<string>>({
url: '/channel/cashier/config/qrCodeUrl',
params: { appId },
})
}
/**
* 通道收银台配置
*/
export interface ChannelCashierConfig {
// 商户号
mchNo?: string
// 应用号
appId?: string
export interface ChannelCashierConfig extends MchEntity {
// 收银台类型
cashierType?: string
// 收银台名称
@@ -69,4 +98,6 @@ export interface ChannelCashierConfig {
allocation?: boolean
// 自动分账
autoAllocation?: boolean
// 备注
remark?: string
}

View File

@@ -1,11 +1,231 @@
<script setup lang="ts">
</script>
<template>
<basic-modal
v-bind="$attrs"
:loading="confirmLoading"
:width="modalWidth"
:title="title"
:open="visible"
:mask-closable="showable"
@cancel="handleCancel"
>
<a-form
class="small-from-item"
ref="formRef"
:model="form"
:rules="rules"
:validate-trigger="['blur', 'change']"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="主键" name="id" :hidden="true">
<a-input v-model:value="form.id" :disabled="showable" />
</a-form-item>
<a-form-item label="收银台类型" validate-first name="cashierType">
<a-select
style="width: 300px"
v-model:value="form.cashierType"
:disabled="showable"
:options="cashierTypeList"
allow-clear
placeholder="请选择收银台类型"
/>
</a-form-item>
<a-form-item label="收银台名称" name="cashierName">
<a-input
v-model:value="form.cashierName"
:disabled="showable"
placeholder="请输入收银台名称"
/>
</a-form-item>
<a-form-item label="支付通道" name="channel">
<a-select
style="width: 300px"
v-model:value="form.channel"
:disabled="showable"
:options="channelList"
allow-clear
placeholder="请选择支付通道"
/>
</a-form-item>
<a-form-item label="支付方式" name="payMethod">
<a-select
style="width: 300px"
v-model:value="form.payMethod"
:disabled="showable"
:options="methodList"
allow-clear
placeholder="请选择支付方式"
/>
</a-form-item>
<a-form-item label="是否开启分账" name="allocation">
<a-switch
checked-children="启用"
un-checked-children="停用"
v-model:checked="form.allocation"
:disabled="showable"
/>
</a-form-item>
<a-form-item label="自动分账" name="autoAllocation">
<a-switch
checked-children="启用"
un-checked-children="停用"
v-model:checked="form.autoAllocation"
:disabled="showable"
/>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="form.remark" :disabled="showable" placeholder="请输入备注" />
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button key="cancel" @click="handleCancel">取消</a-button>
<a-button
v-if="!showable"
key="forward"
:loading="confirmLoading"
type="primary"
@click="handleOk"
>保存</a-button
>
</a-space>
</template>
</basic-modal>
</template>
<style scoped lang="less">
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref, unref } from 'vue'
import useFormEdit from '@/hooks/bootx/useFormEdit'
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { FormEditType } from '@/enums/formTypeEnum'
import { BasicModal } from '@/components/Modal'
import { LabeledValue } from 'ant-design-vue/lib/select'
import {
get,
add,
update,
existsByType,
existsByTypeNotId,
ChannelCashierConfig,
} from './ChannelCashierConfig.api'
import { useDict } from '@/hooks/bootx/useDict'
import { MchApp } from '@/views/daxpay/common/merchant/app/MchApp.api'
</style>
const {
initFormEditType,
handleCancel,
labelCol,
wrapperCol,
modalWidth,
title,
confirmLoading,
visible,
showable,
formEditType,
} = useFormEdit()
const { dictDropDown } = useDict()
// 表单
const formRef = ref<FormInstance>()
const cashierTypeList = ref<LabeledValue[]>([])
const channelList = ref<LabeledValue[]>([])
const methodList = ref<LabeledValue[]>([])
let form = ref<ChannelCashierConfig>({
allocation: false,
autoAllocation: false,
})
// 校验
const rules = reactive({
cashierType: [
{ required: true, message: '请选择收银台类型' },
{ validator: validateCode, trigger: 'blur' },
],
cashierName: [{ required: true, message: '请输入收银台名称' }],
name: [{ required: true, message: '请输入字典项名称' }],
} as Record<string, Rule[]>)
// 事件
const emits = defineEmits(['ok'])
onMounted(() => {
initData()
})
/**
* 初始化数据
*/
async function initData() {
cashierTypeList.value = await dictDropDown('cashier_type')
channelList.value = await dictDropDown('channel')
methodList.value = await dictDropDown('pay_method')
}
/**
* 入口
*/
function init(id, editType: FormEditType, mchApp: MchApp) {
initFormEditType(editType)
resetForm()
form.value.appId = unref(mchApp.appId)
form.value.mchNo = unref(mchApp.mchNo)
getInfo(id, editType)
}
/**
* 获取信息
*/
function getInfo(id, editType: FormEditType) {
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
confirmLoading.value = true
get(id).then(({ data }) => {
form.value = data
confirmLoading.value = false
})
} else {
confirmLoading.value = false
}
}
/**
* 保存
*/
function handleOk() {
formRef.value?.validate().then(async () => {
confirmLoading.value = true
if (formEditType.value === FormEditType.Add) {
await add(form.value).finally(() => (confirmLoading.value = false))
} else if (formEditType.value === FormEditType.Edit) {
await update(form.value).finally(() => (confirmLoading.value = false))
}
handleCancel()
emits('ok')
})
}
/**
* 校验编码重复
*/
async function validateCode() {
const { cashierType, appId, id } = form.value
if (id) {
const res = await existsByTypeNotId(appId, cashierType, id)
return res.data ? Promise.reject('该收银台类型已存在') : Promise.resolve()
} else {
const res = await existsByType(appId, cashierType)
return res.data ? Promise.reject('该收银台类型已存在') : Promise.resolve()
}
}
// 重置表单的校验
function resetForm() {
nextTick(() => {
formRef.value?.resetFields()
})
}
defineExpose({
init,
})
</script>
<style lang="less" scoped></style>

View File

@@ -3,92 +3,105 @@
showFooter
v-bind="$attrs"
width="70%"
title="通道配置"
title="收银配置"
:mask-closable="true"
:open="visible"
@close="handleCancel"
>
<div class="m-3 p-3 bg-white">
<a-spin :spinning="confirmLoading">
<a-list
style="margin-left: 20px"
:grid="{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 4, xxl: 5 }"
:data-source="channelConfigs"
>
<template #renderItem="{ item }">
<a-card hoverable style="max-width: 200px; margin-bottom: 20px" @click="setting(item)">
<template #cover>
<a-image
style="width: 150px; height: 150px; margin-top: 20px"
:preview="false"
:src="getIcon(item.channel)"
:fallback="fallbackImg"
/>
</template>
<a-card-meta style="display: flex; justify-content: space-between" :title="item.name">
<template #description>
<template v-if="item.enable">
<a-badge dot color="green" />
<span style="color: green">已启用</span>
</template>
<template v-else-if="item.enable === false">
<a-badge dot color="red" />
<span style="color: red">未启用</span>
</template>
<template v-else>
<a-badge dot color="grey" />
<span style="color: grey">未配置</span>
</template>
</template>
</a-card-meta>
</a-card>
</template>
</a-list>
</a-spin>
</div>
<channel-cashier-config-edit ref="channelCashierConfigEdit" @ok="query" />
<vxe-toolbar ref="xToolbar" custom :refresh="{ queryMethod: queryPage }">
<template #buttons>
<a-space>
<a-button type="primary" pre-icon="ant-design:plus-outlined" @click="add">新建</a-button>
</a-space>
</template>
</vxe-toolbar>
<vxe-table ey-field="id" ref="xTable" :data="records" :loading="loading">
<vxe-column type="seq" width="60" />
<vxe-column field="cashierType" title="收银台类型" :min-width="150">
<template #default="{ row }">
<a-tag color="green">{{ dictConvert('cashier_type', row.cashierType) }}</a-tag>
</template>
</vxe-column>
<vxe-column field="cashierName" title="收银台名称" :min-width="150" />
<vxe-column field="channel" title="关联通道" :min-width="150">
<template #default="{ row }">
<a-tag color="green">{{ dictConvert('channel', row.channel) }}</a-tag>
</template>
</vxe-column>
<vxe-column field="channel" title="关联支付方式" :min-width="150">
<template #default="{ row }">
<a-tag color="green">{{ dictConvert('pay_method', row.payMethod) }}</a-tag>
</template>
</vxe-column>
<vxe-column field="allocation" title="开启分账" :min-width="150">
<template #default="{ row }">
<a-tag v-if="row.allocation" color="green">开启</a-tag>
<a-tag v-else color="red">关闭</a-tag>
</template>
</vxe-column>
<vxe-column field="autoAllocation" title="自动分账" :min-width="150">
<template #default="{ row }">
<a-tag v-if="row.autoAllocation" color="green">开启</a-tag>
<a-tag v-else color="red">关闭</a-tag>
</template>
</vxe-column>
<vxe-column field="remark" title="备注" :min-width="170" />
<vxe-column field="createTime" title="创建时间" :min-width="170" />
<vxe-column fixed="right" :width="150" :showOverflow="false" title="操作">
<template #default="{ row }">
<a href="javascript:" @click="show(row)">查看</a>
<a-divider type="vertical" />
<a href="javascript:" @click="edit(row)">编辑</a>
<a-divider type="vertical" />
<a href="javascript:" style="color: red" @click="del(row)">删除</a>
</template>
</vxe-column>
</vxe-table>
<ChannelCashierConfigEdit ref="channelCashierConfigEdit" @ok="queryPage" />
</basic-drawer>
</template>
<script setup lang="ts">
import { BasicDrawer } from '@/components/Drawer'
import { ref } from 'vue'
import { findAll, ChannelCashierConfig } from './ChannelCashierConfig.api'
import { findAll, remove, ChannelCashierConfig } from './ChannelCashierConfig.api'
import ChannelCashierConfigEdit from './ChannelCashierConfigEdit.vue'
import { FormEditType } from '@/enums/formTypeEnum'
import { useMessage } from '@/hooks/web/useMessage'
import { useDict } from '@/hooks/bootx/useDict'
import { MchApp } from '@/views/daxpay/common/merchant/app/MchApp.api'
const confirmLoading = ref(false)
const { createConfirm, createMessage } = useMessage()
const { dictConvert } = useDict()
const currentAppId = ref('')
const currentApp = ref<MchApp>({})
const loading = ref(false)
const visible = ref(false)
const records = ref<ChannelCashierConfig[]>([])
const channelCashierConfigEdit = ref<any>()
/**
* 初始化并展示
*/
async function init(appId: string) {
async function init(mchApp: MchApp) {
visible.value = true
currentAppId.value = appId
query()
loading.value = false
currentApp.value = mchApp
queryPage()
}
/**
* 查询
*/
function query() {
function queryPage() {
// 列表信息
confirmLoading.value = true
findAll(currentAppId.value).then(({ data }) => {
channelConfigs.value = data
confirmLoading.value = false
loading.value = true
findAll(currentApp.value.appId).then(({ data }) => {
records.value = data
loading.value = false
})
}
/**
* 打开支付设置界面
*/
function setting(record: ChannelConfig) {
channelConfigEdit.value.show(record)
}
/**
* 关闭页面
*/
@@ -97,19 +110,43 @@
}
/**
* 获取icon
* 新增
*/
function getIcon(type: string) {
switch (type) {
case DaxpayEnum.ALI:
return alipay
case DaxpayEnum.WECHAT:
return wechat
case DaxpayEnum.UNION_PAY:
return unionPay
default:
return ''
}
function add() {
channelCashierConfigEdit.value.init(null, FormEditType.Add, currentApp.value)
}
/**
* 编辑
*/
function edit(record) {
channelCashierConfigEdit.value.init(record.id, FormEditType.Edit, currentApp.value)
}
/**
* 查看
*/
function show(record) {
channelCashierConfigEdit.value.init(record.id, FormEditType.Show, currentApp.value)
}
/**
* 删除
*/
function del(record) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '是否删除该数据',
onOk: () => {
loading.value = true
remove(record.id).then(() => {
createMessage.success('删除成功')
queryPage()
})
},
})
}
defineExpose({
init,
})
</script>
<style scoped lang="less"></style>

View File

@@ -1,11 +1,82 @@
<script setup lang="ts">
</script>
<template>
<a-modal
title="商户收款码牌"
:loading="confirmLoading"
:width="500"
:open="visible"
:mask-closable="false"
:footer="null"
@cancel="handleCancel"
>
<div class="flex justify-center" style="padding: 30px 0">
<a-qrcode
ref="qrcodeCanvasRef"
:status="confirmLoading ? 'loading' : 'active'"
:size="350"
:value="url"
:error-level="'Q'"
color="black"
bg-color="white"
/>
</div>
<div class="flex justify-center" style="margin-bottom: 20px">
<a-button type="primary" @click="download">保存码牌</a-button>
<a-button style="margin-left: 25px" @click="copy">复制地址</a-button>
</div>
</a-modal>
</template>
<style scoped lang="less">
<script setup lang="ts">
import { ref } from 'vue'
import { copyText } from '@/utils/copyTextToClipboard'
import { getQrCodeUrl } from './ChannelCashierConfig.api'
</style>
const confirmLoading = ref(false)
const visible = ref(false)
const url = ref('')
const qrcodeCanvasRef = ref()
/**
* 初始化
*/
function init(appId: string) {
visible.value = true
confirmLoading.value = true
getQrCodeUrl(appId).then((res) => {
url.value = res.data
confirmLoading.value = false
})
}
/**
* 复制
*/
function copy() {
copyText(url.value)
}
/**
* 关闭
*/
function handleCancel() {
visible.value = false
}
/**
* 下嘴
*/
async function download() {
const url = qrcodeCanvasRef.value.toDataURL()
const a = document.createElement('a')
a.download = 'QRCode.png'
a.href = url
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
defineExpose({
init,
})
</script>
<style scoped lang="less"></style>

View File

@@ -19,6 +19,13 @@
<a-descriptions-item label="可退余额(元)" :span="8">
{{ order.refundableBalance }}
</a-descriptions-item>
<a-descriptions-item label="支付通道" :span="8">
{{ dictConvert('channel', order.channel) }}
</a-descriptions-item>
<a-descriptions-item label="支付方式" :span="8">
{{ dictConvert('pay_method', order.method) }}
</a-descriptions-item>
<a-descriptions-item label="" :span="8" />
<a-descriptions-item label="支付状态" :span="8">
{{ dictConvert('pay_status', order.status) }}
</a-descriptions-item>

View File

@@ -37,7 +37,12 @@
</a-tree>
</a-spin>
<template #footer>
<a-select v-if="isAdmin()" style="min-width: 100px" @change="clientSwitch" v-model:value="clientCode">
<a-select
v-if="isAdmin()"
style="min-width: 100px"
@change="clientSwitch"
v-model:value="clientCode"
>
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
</a-select>
<a-dropdown style="margin-left: 5px" :trigger="['click']" placement="top">