余额充值,支付

This commit is contained in:
Jason
2023-04-03 15:13:49 +08:00
parent 2b890b65f3
commit b50df74802
42 changed files with 2554 additions and 44 deletions

View File

@@ -0,0 +1,10 @@
import request from '@/utils/request'
export function getRechargeConfig() {
return request.get({ url: '/marketing/recharge/detail' })
}
// 设置
export function setRechargeConfig(params: any) {
return request.post({ url: '/marketing/recharge/save', params })
}

31
admin/src/api/finance.ts Normal file
View File

@@ -0,0 +1,31 @@
import request from '@/utils/request'
// 余额明细
export function accountLog(params?: any) {
return request.get({ url: '/finance/wallet/list', params })
}
// 充值记录
export function rechargeLists(params?: any) {
return request.get({ url: '/finance/recharger/list', params }, { ignoreCancelToken: true })
}
//退款
export function refund(params?: any) {
return request.post({ url: '/finance/recharger/refund', params })
}
//重新退款
export function refundAgain(params?: any) {
return request.post({ url: '/finance/recharger/refundAgain', params })
}
//退款记录
export function refundRecord(params?: any) {
return request.get({ url: '/finance/refund/list', params })
}
//退款日志
export function refundLog(params?: any) {
return request.get({ url: '/finance/refund/log', params })
}

View File

@@ -0,0 +1,26 @@
import request from '@/utils/request'
// 获取支付方式
export function getPayWay() {
return request.get({ url: '/setting/payment/method' })
}
// 设置支付方式
export function setPayWay(params: any) {
return request.post({ url: '/setting/payment/editMethod', params })
}
// 获取支付方式
export function getPayConfigLists() {
return request.get({ url: '/setting/payment/list' })
}
// 设置支付方式
export function setPayConfig(params: any) {
return request.post({ url: '/setting/payment/editConfig', params })
}
// 设置支付方式
export function getPayConfig(params: any) {
return request.get({ url: '/setting/payment/detail', params })
}

View File

@@ -92,6 +92,11 @@ const linkList = ref([
path: '/pages/search/search',
name: '搜索',
type: LinkTypeEnum.SHOP_PAGES
},
{
path: '/packages/pages/user_wallet/user_wallet',
name: '我的钱包',
type: LinkTypeEnum.SHOP_PAGES
}
])

View File

@@ -1,3 +1,4 @@
import { isFunction } from 'lodash'
import { reactive, toRaw } from 'vue'
// 分页钩子函数
@@ -7,10 +8,20 @@ interface Options {
fetchFun: (_arg: any) => Promise<any>
params?: Record<any, any>
firstLoading?: boolean
beforeRequest?(params: Record<any, any>): Record<any, any>
afterRequest?(res: Record<any, any>): void
}
export function usePaging(options: Options) {
const { page = 1, size = 15, fetchFun, params = {}, firstLoading = false } = options
const {
page = 1,
size = 15,
fetchFun,
params = {},
firstLoading = false,
beforeRequest,
afterRequest
} = options
// 记录分页初始参数
const paramsInit: Record<any, any> = Object.assign({}, toRaw(params))
// 分页数据
@@ -19,19 +30,28 @@ export function usePaging(options: Options) {
size,
loading: firstLoading,
count: 0,
lists: [] as any[]
lists: [] as any[],
extend: {} as Record<any, any>
})
// 请求分页接口
const getLists = () => {
pager.loading = true
let requestParams = params
if (isFunction(beforeRequest)) {
requestParams = beforeRequest(params)
}
return fetchFun({
pageNo: pager.page,
pageSize: pager.size,
...params
...requestParams
})
.then((res: any) => {
pager.count = res?.count
pager.lists = res?.lists
pager.extend = res?.extend
if (isFunction(afterRequest)) {
afterRequest(res)
}
return Promise.resolve(res)
})
.catch((err: any) => {

View File

@@ -0,0 +1,54 @@
<template>
<div>
<el-card shadow="never" class="!border-none">
<template #header>
<span class="font-extrabold text-lg">充值设置</span>
</template>
<el-form :model="formData" label-width="120px">
<el-form-item label="状态">
<div>
<el-radio-group v-model="formData.openRecharge" class="ml-4">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">关闭</el-radio>
</el-radio-group>
<div class="form-tips">关闭或开启充值功能关闭后将不显示充值入口</div>
</div>
</el-form-item>
<el-form-item label="最低充值金额">
<div>
<el-input
v-model="formData.minRechargeMoney"
placeholder="请输入最低充值金额"
clearable
/>
<div class="form-tips">
最低充值金额要求不填或填0表示不限制最低充值金额
</div>
</div>
</el-form-item>
</el-form>
</el-card>
<footer-btns v-perms="['marketing:recharge:save']">
<el-button type="primary" @click="handleSubmit">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getRechargeConfig, setRechargeConfig } from '@/api/app/recharge'
import feedback from '@/utils/feedback'
const formData = reactive({
openRecharge: 1, //功能状态 1-开启 0-关闭
minRechargeMoney: '' //最低充值金额
})
const getConfig = async () => {
const data = await getRechargeConfig()
Object.assign(formData, data)
}
const handleSubmit = async () => {
await setRechargeConfig(formData)
feedback.msgSuccess('操作成功')
getConfig()
}
getConfig()
</script>

View File

@@ -0,0 +1,104 @@
<template>
<popup
ref="popupRef"
title="余额调整"
width="500px"
@confirm="handleConfirm"
:async="true"
@close="popupClose"
>
<div class="pr-8">
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
<el-form-item label="当前余额">¥ {{ value }} </el-form-item>
<el-form-item label="余额增减" required prop="action">
<el-radio-group v-model="formData.action">
<el-radio :label="1">增加余额</el-radio>
<el-radio :label="2">扣减余额</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="调整余额" prop="num">
<el-input
:model-value="formData.num"
placeholder="请输入调整的金额"
type="number"
@input="numberValidate"
/>
</el-form-item>
<el-form-item label="调整后余额"> ¥ {{ adjustmentMoney }} </el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="4" />
</el-form-item>
</el-form>
</div>
</popup>
</template>
<script lang="ts" setup>
import Popup from '@/components/popup/index.vue'
import type { FormInstance, FormRules } from 'element-plus'
import feedback from '@/utils/feedback'
const formRef = shallowRef<FormInstance>()
const props = defineProps({
show: {
type: Boolean,
required: true
},
value: {
type: [Number, String],
required: true
}
})
const emit = defineEmits<{
(event: 'update:show', value: boolean): void
(event: 'confirm', value: any): void
}>()
const formData = reactive({
action: 1, //变动类型 1-增加 2-减少
num: '',
remark: ''
})
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const adjustmentMoney = computed(() => {
return Number(props.value) + Number(formData.num) * (formData.action == 1 ? 1 : -1)
})
const formRules: FormRules = {
num: [
{
required: true,
message: '请输入调整的金额'
}
]
}
const numberValidate = (value: string) => {
if (value.includes('-')) {
return feedback.msgError('请输入正整数')
}
formData.num = value
}
const handleConfirm = async () => {
await formRef.value?.validate()
emit('confirm', formData)
}
const popupClose = () => {
emit('update:show', false)
formRef.value?.resetFields()
}
watch(
() => props.show,
(val) => {
if (val) {
popupRef.value?.open()
} else {
popupRef.value?.close()
}
}
)
watch(adjustmentMoney, (val) => {
if (val < 0) {
feedback.msgError('调整后余额需大于0')
formData.num = ''
}
})
</script>

View File

@@ -5,9 +5,25 @@
</el-card>
<el-card class="mt-4 !border-none" header="基本资料" shadow="never">
<el-form ref="formRef" class="ls-form" :model="formData" label-width="120px">
<div class="bg-page py-5 pl-20 mb-10">
<div class="mb-3 text-tx-regular">用户头像</div>
<el-avatar :src="formData.avatar" :size="58" />
<div class="bg-page flex py-5 mb-10 items-center">
<div class="basis-40 flex flex-col justify-center items-center">
<div class="mb-2 text-tx-regular">用户头像</div>
<el-avatar :src="formData.avatar" :size="58" />
</div>
<div class="basis-40 flex flex-col justify-center items-center">
<div class="text-tx-regular">账户余额</div>
<div class="mt-2 flex items-center">
¥{{ formData.user_money }}
<el-button
v-perms="['user.user/adjustMoney']"
type="primary"
link
@click="handleAdjust(formData.user_money)"
>
调整
</el-button>
</div>
</div>
</div>
<el-form-item label="用户编号:"> {{ formData.sn }} </el-form-item>
<el-form-item label="用户昵称:">
@@ -80,6 +96,11 @@
<el-form-item label="最近登录时间:"> {{ formData.lastLoginTime }} </el-form-item>
</el-form>
</el-card>
<account-adjust
v-model:show="adjustState.show"
:value="adjustState.value"
@confirm="handleConfirmAdjust"
/>
</div>
</template>
@@ -88,7 +109,7 @@ import type { FormInstance } from 'element-plus'
import { getUserDetail, userEdit } from '@/api/consumer'
import feedback from '@/utils/feedback'
import { isEmpty } from '@/utils/util'
import AccountAdjust from '../components/account-adjust.vue'
const route = useRoute()
const formData = reactive({
avatar: '',
@@ -105,7 +126,10 @@ const formData = reactive({
})
const formRef = shallowRef<FormInstance>()
const adjustState = reactive({
show: false,
value: ''
})
const getDetails = async () => {
const data = await getUserDetail({
id: route.query.id
@@ -126,6 +150,15 @@ const handleEdit = async (value: string, field: string) => {
feedback.msgSuccess('编辑成功')
getDetails()
}
const handleAdjust = (value: string) => {
adjustState.show = true
adjustState.value = value
}
const handleConfirmAdjust = async (value: any) => {
await adjustMoney({ user_id: route.query.id, ...value })
adjustState.show = false
getDetails()
}
getDetails()
</script>

View File

@@ -44,7 +44,7 @@
<el-table-column label="昵称" prop="nickname" min-width="100" />
<el-table-column label="账号" prop="username" min-width="120" />
<el-table-column label="手机号码" prop="mobile" min-width="100" />
<el-table-column label="性别" prop="sex" min-width="100" />
<!-- <el-table-column label="性别" prop="sex" min-width="100" /> -->
<el-table-column label="注册来源" prop="channel" min-width="100" />
<el-table-column label="注册时间" prop="createTime" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">

View File

@@ -0,0 +1,101 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:用户账户变动记录"
:closable="false"
show-icon
></el-alert>
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
<el-form-item label="用户信息">
<el-input
class="w-[280px]"
v-model="queryParams.keyword"
placeholder="请输入用户编号/昵称/手机号"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="变动类型">
<el-select class="w-[280px]" v-model="queryParams.type">
<el-option label="全部" value />
<el-option
v-for="(value, key) in changeType"
:key="key"
:label="value"
:value="key"
/>
</el-select>
</el-form-item>
<el-form-item label="记录时间">
<daterange-picker
v-model:startTime="queryParams.startTime"
v-model:endTime="queryParams.endTime"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="用户编号" prop="userSn" min-width="100" />
<el-table-column label="用户昵称" min-width="160">
<template #default="{ row }">
<div class="flex items-center">
<image-contain
class="flex-none mr-2"
:src="row.avatar"
:width="40"
:height="40"
preview-teleported
fit="contain"
/>
{{ row.nickname }}
</div>
</template>
</el-table-column>
<el-table-column label="变动金额" prop="changeAmount" min-width="100">
<template #default="{ row }">
<span :class="{ 'text-error': row.action == 2 }">
{{ row.changeAmount }}
</span>
</template>
</el-table-column>
<el-table-column label="剩余金额" prop="leftAmount" min-width="100" />
<el-table-column label="变动类型" prop="changeType" min-width="120" />
<el-table-column label="来源单号" prop="sourceSn" min-width="100" />
<el-table-column label="记录时间" prop="createTime" min-width="120" />
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="articleLists">
import { accountLog } from '@/api/finance'
import { usePaging } from '@/hooks/usePaging'
const queryParams = reactive({
keyword: '',
type: '',
startTime: '',
endTime: ''
})
const changeType = ref<any[]>([])
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: accountLog,
params: queryParams,
afterRequest(res) {
changeType.value = res.extend?.changeType
}
})
getLists()
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div class="code-preview">
<el-dialog v-model="show" width="760px" title="退款日志">
<el-table size="large" v-loading="loading" :data="logLists" height="500">
<el-table-column label="流水单号" prop="sn" min-width="190" />
<el-table-column label="退款金额" min-width="110">
<template #default="{ row }"> ¥{{ row.refundAmount }} </template>
</el-table-column>
<el-table-column label="退款状态" prop="" min-width="100">
<template #default="{ row }">
<el-tag type="warning" v-if="row.refundStatus == 0">
{{ row.refundStatusMsg }}
</el-tag>
<el-tag v-if="row.refundStatus == 1">
{{ row.refundStatusMsg }}
</el-tag>
<el-tag type="danger" v-if="row.refundStatus == 2">
{{ row.refundStatusMsg }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="记录时间" prop="createTime" min-width="180" />
<el-table-column label="操作人" prop="handler" min-width="120" />
</el-table>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { refundLog } from '@/api/finance'
const loading = ref(false)
const logLists = ref([])
const props = defineProps<{
modelValue: boolean
refundId: number
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void
}>()
const show = computed<boolean>({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const getRefundLog = async () => {
loading.value = true
logLists.value = []
try {
const res = await refundLog({
id: props.refundId
})
logLists.value = res
} catch (error) {}
loading.value = false
}
watch(show, (value) => {
if (value) {
getRefundLog()
}
})
</script>

View File

@@ -0,0 +1,141 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:用户充值记录"
:closable="false"
show-icon
></el-alert>
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
<el-form-item label="充值单号">
<el-input
class="w-[280px]"
v-model="queryParams.sn"
placeholder="请输入充值单号"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="用户信息">
<el-input
class="w-[280px]"
v-model="queryParams.keyword"
placeholder="请输入用户编号/昵称/手机号"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="支付方式">
<el-select class="w-[280px]" v-model="queryParams.payWay">
<el-option label="全部" value />
<el-option label="微信支付" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="支付状态">
<el-select class="w-[280px]" v-model="queryParams.payStatus">
<el-option label="全部" value />
<el-option label="未支付" :value="0" />
<el-option label="已支付" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="下单时间">
<daterange-picker
v-model:startTime="queryParams.startTime"
v-model:endTime="queryParams.endTime"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
<export-data
class="ml-2.5"
:fetch-fun="rechargeLists"
:params="queryParams"
:page-size="pager.size"
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="用户信息" min-width="160">
<template #default="{ row }">
<div class="flex items-center">
<image-contain
class="flex-none mr-2"
:src="row.avatar"
:width="40"
:height="40"
preview-teleported
fit="contain"
/>
{{ row.nickname }}
</div>
</template>
</el-table-column>
<el-table-column label="充值单号" prop="orderSn" min-width="190" />
<el-table-column label="充值金额" prop="orderAmount" min-width="100">
</el-table-column>
<el-table-column label="支付方式" prop="payWay" min-width="100" />
<el-table-column label="支付状态" prop="" min-width="100">
<template #default="{ row }">
<span
:class="{
'text-error': row.payStatus == 0
}"
>
{{ row.payStatus == 0 ? '未支付' : '已支付' }}
</span>
</template>
</el-table-column>
<el-table-column label="提交时间" prop="createTime" min-width="180" />
<el-table-column label="支付时间" prop="payTime" min-width="180" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.payStatus == 1"
v-perms="['finance:recharger:refund']"
type="primary"
link
@click="handleRefund(row.id)"
>
退款
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="articleLists">
import { rechargeLists, refund } from '@/api/finance'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
const queryParams = reactive({
sn: '',
keyword: '',
payStatus: '',
payWay: '',
startTime: '',
endTime: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: rechargeLists,
params: queryParams
})
const handleRefund = async (id: number) => {
await feedback.confirm('确认退款?')
await refund({
id
})
feedback.msgSuccess('操作成功')
getLists()
}
getLists()
</script>

View File

@@ -0,0 +1,226 @@
<template>
<div>
<el-card class="!border-none mb-4" shadow="never">
<div class="flex flex-wrap">
<div class="w-1/2 md:w-1/4">
<div class="leading-10">累计退款金额 ()</div>
<div class="text-6xl">{{ refundData.totalRefundAmount }}</div>
</div>
<div class="w-1/2 md:w-1/4">
<div class="leading-10">退款中金额 ()</div>
<div class="text-6xl">{{ refundData.ingRefundAmount }}</div>
</div>
<div class="w-1/2 md:w-1/4">
<div class="leading-10">退款成功金额 ()</div>
<div class="text-6xl">{{ refundData.successRefundAmount }}</div>
</div>
<div class="w-1/2 md:w-1/4">
<div class="leading-10">退款失败金额 ()</div>
<div class="text-6xl">{{ refundData.errorRefundAmount }}</div>
</div>
</div>
</el-card>
<el-card class="!border-none" shadow="never">
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
<el-form-item label="退款单号">
<el-input
class="w-[280px]"
v-model="queryParams.sn"
placeholder="请输入退款单号"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="来源单号">
<el-input
class="w-[280px]"
v-model="queryParams.orderSn"
placeholder="请输入来源单号"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="用户信息">
<el-input
class="w-[280px]"
v-model="queryParams.keyword"
placeholder="请输入用户信息"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="退款类型">
<el-select class="w-[280px]" v-model="queryParams.refundType">
<el-option label="全部" value />
<el-option label="后台退款" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="记录时间">
<daterange-picker
v-model:startTime="queryParams.startTime"
v-model:endTime="queryParams.endTime"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
<!-- <export-data
class="ml-2.5"
:fetch-fun="refundRecord"
:params="queryParams"
:page-size="pager.size"
/> -->
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane
v-for="(item, index) in tabLists"
:label="`${item.name}(${pager.extend[item.numKey] ?? 0})`"
:name="index"
:key="index"
>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="退款单号" prop="sn" min-width="190" />
<el-table-column label="用户信息" min-width="160">
<template #default="{ row }">
<div class="flex items-center">
<image-contain
class="flex-none mr-2"
:src="row.avatar"
:width="40"
:height="40"
preview-teleported
fit="contain"
/>
{{ row.nickname }}
</div>
</template>
</el-table-column>
<el-table-column label="来源单号" prop="orderSn" min-width="190" />
<el-table-column label="退款金额" min-width="100">
<template #default="{ row }"> ¥ {{ row.refundAmount }} </template>
</el-table-column>
<el-table-column label="退款类型" prop="refundTypeMsg" min-width="100" />
<el-table-column label="退款状态" prop="" min-width="100">
<template #default="{ row }">
<el-tag type="warning" v-if="row.refundStatus == 0">
{{ row.refundStatusMsg }}
</el-tag>
<el-tag v-if="row.refundStatus == 1">
{{ row.refundStatusMsg }}
</el-tag>
<el-tag type="danger" v-if="row.refundStatus == 2">
{{ row.refundStatusMsg }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="记录时间" prop="createTime" min-width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['finance:refund:log']"
type="primary"
link
@click="handleShowRefundLog(row.id)"
>
退款日志
</el-button>
<el-button
v-if="row.refundStatus == 2"
v-perms="['finance:recharger:refundAgain']"
type="primary"
link
@click="handleRefund(row.id)"
>
重新退款
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
</div>
</template>
<script lang="ts" setup name="articleLists">
import { refundRecord, refundAgain } from '@/api/finance'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import RefundLog from './component/refund-log.vue'
const queryParams = reactive({
sn: '',
orderSn: '',
keyword: '',
refundType: '',
startTime: '',
endTime: '',
type: ''
})
const refundData = reactive({
errorRefundAmount: 0,
ingRefundAmount: 0,
successRefundAmount: 0,
totalRefundAmount: 0
})
const showRefundLog = ref(false)
const selectRefundId = ref(0)
const activeTab = ref(0)
const tabLists = ref([
{
name: '全部',
type: '',
numKey: 'total'
},
{
name: '退款中',
type: 0,
numKey: 'ing'
},
{
name: '退款成功',
type: 1,
numKey: 'success'
},
{
name: '退款失败',
type: 2,
numKey: 'error'
}
])
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: refundRecord,
params: queryParams,
afterRequest(res) {
Object.assign(refundData, res.extend.stat)
}
})
const handleRefund = async (id: number) => {
await feedback.confirm('确认重新退款?')
await refundAgain({
id
})
feedback.msgSuccess('操作成功')
getLists()
}
const handleShowRefundLog = async (id: number) => {
showRefundLog.value = true
selectRefundId.value = id
}
const handleTabChange = (index: any) => {
queryParams.type = tabLists.value[index].type as string
resetPage()
}
getLists()
</script>

View File

@@ -0,0 +1,279 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="550px"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
<el-form-item label="支付方式">
<el-radio :label="formData.name" :model-value="formData.name" />
</el-form-item>
<el-form-item label="显示名称" prop="showName">
<el-input v-model="formData.showName" placeholder="请输入显示名称" />
</el-form-item>
<el-form-item label="显示图标" prop="icon">
<div>
<material-picker :limit="1" :disabled="false" v-model="formData.icon" />
<span class="form-tips">建议尺寸200*200px</span>
</div>
</el-form-item>
<template v-if="formData.way == PayWayEnum.WECHAT">
<el-form-item prop="params.interface_version" label="微信支付接口版本">
<div>
<el-radio-group v-model="formData.params.interface_version">
<el-radio label="v3"></el-radio>
</el-radio-group>
<div class="form-tips">暂时只支持V3版本</div>
</div>
</el-form-item>
<el-form-item label="商户类型" prop="params.merchant_type">
<div>
<el-radio-group v-model="formData.params.merchant_type">
<el-radio label="ordinary_merchant">普通商户</el-radio>
</el-radio-group>
<div class="form-tips">
暂时只支持普通商户类型服务商户类型模式暂不支持
</div>
</div>
</el-form-item>
<el-form-item label="微信支付商户号" prop="params.mch_id">
<div class="flex-1">
<el-input
v-model="formData.params.mch_id"
placeholder="请输入微信支付商户号"
/>
<div class="form-tips">微信支付商户号MCHID</div>
</div>
</el-form-item>
<el-form-item label="商户API密钥" prop="params.pay_sign_key">
<el-input
v-model="formData.params.pay_sign_key"
placeholder="请输入微信支付商户API密钥"
/>
<span class="form-tips">微信支付商户API密钥paySignKey</span>
</el-form-item>
<el-form-item label="微信支付证书" prop="params.private_cert">
<el-input
type="textarea"
rows="3"
v-model="formData.params.private_cert"
placeholder="请输入微信支付证书"
/>
<span class="form-tips">
微信支付证书apiclient_cert.pem前往微信商家平台生成并黏贴至此处
</span>
</el-form-item>
<el-form-item label="微信支付证书密钥" prop="params.private_key">
<el-input
type="textarea"
rows="3"
v-model="formData.params.private_key"
placeholder="请输入微信支付证书密钥"
/>
<span class="form-tips">
微信支付证书密钥apiclient_key.pem前往微信商家平台生成并黏贴至此处
</span>
</el-form-item>
</template>
<!-- <template v-if="formData.way == PayWayEnum.ALIPAY">
<el-form-item label="模式" prop="params.mode">
<div>
<el-radio-group v-model="formData.params.mode">
<el-radio label="normal_mode">普通模式</el-radio>
</el-radio-group>
<div class="form-tips">暂时仅支持支付宝普通模式</div>
</div>
</el-form-item>
<el-form-item label="商户类型" prop="params.merchant_type">
<div>
<el-radio-group v-model="formData.params.merchant_type">
<el-radio label="ordinary_merchant">普通商户</el-radio>
</el-radio-group>
<div class="form-tips">
暂时只支持普通商户类型服务商户类型模式暂不支持
</div>
</div>
</el-form-item>
<el-form-item label="应用ID" prop="params.app_id">
<div class="flex-1">
<el-input
v-model="formData.params.app_id"
placeholder="请输入支付宝应用ID"
/>
<span class="form-tips"> 支付宝应用APP_ID </span>
</div>
</el-form-item>
<el-form-item label="应用私钥" prop="params.private_key">
<div class="flex-1">
<el-input
type="textarea"
rows="3"
v-model="formData.params.private_key"
placeholder="请输入支付宝应用私钥"
/>
<span class="form-tips">支付宝应用私钥private_key </span>
</div>
</el-form-item>
<el-form-item label="支付宝公钥" prop="params.ali_public_key">
<div class="flex-1">
<el-input
type="textarea"
rows="3"
v-model="formData.params.ali_public_key"
placeholder="请输入支付宝公钥"
/>
<span class="form-tips">支付宝公钥ali_public_key </span>
</div>
</el-form-item>
</template> -->
<el-form-item label="排序" prop="sort">
<div>
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
<div class="form-tips">默认为0 数值越大越排前</div>
</div>
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus'
import { getPayConfig, setPayConfig } from '@/api/setting/pay'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
enum PayWayEnum {
BALANCE = 1,
WECHAT = 2,
ALIPAY = 3
}
const popupTitle = computed(() => {
switch (formData.way) {
case PayWayEnum.BALANCE:
return '余额支付'
case PayWayEnum.WECHAT:
return '微信支付'
case PayWayEnum.ALIPAY:
return '支付宝支付'
}
})
const formData = reactive({
id: '',
way: 0,
name: '',
showName: '',
icon: '',
sort: 0,
remark: '',
params: {
interface_version: '',
merchant_type: '',
mch_id: '',
pay_sign_key: '',
private_cert: '',
private_key: '',
mode: '',
app_id: '',
ali_public_key: ''
}
})
const formRules: FormRules = {
showName: [
{
required: true,
message: '请输入显示名称'
}
],
'params.mch_id': [
{
required: true,
message: '请输入微信支付商户号'
}
],
'params.pay_sign_key': [
{
required: true,
message: '请输入微信支付商户API密钥'
}
],
'params.private_cert': [
{
required: true,
message: '请输入微信支付证书'
}
],
'params.private_key': [
{
required: true,
message: '请输入微信支付证书密钥'
}
],
'params.app_id': [
{
required: true,
message: '请输入支付宝应用ID'
}
],
'params.ali_public_key': [
{
required: true,
message: '请输入支付宝公钥'
}
]
}
const handleSubmit = async () => {
await formRef.value?.validate()
await setPayConfig(formData)
feedback.msgSuccess('操作成功')
popupRef.value?.close()
emit('success')
}
const open = () => {
popupRef.value?.open()
}
const setFormData = (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const getDetail = async (row: Record<string, any>) => {
const data = await getPayConfig({
id: row.id
})
setFormData(data)
}
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData,
getDetail
})
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:设置系统支持的支付方式"
:closable="false"
show-icon
/>
</el-card>
<el-card shadow="never" class="mt-4 !border-none">
<div>
<el-table :data="payConfigList">
<el-table-column prop="name" label="支付方式" min-width="150" />
<el-table-column prop="showName" label="显示名称" min-width="150" />
<el-table-column label="图标" min-width="150">
<template #default="{ row }">
<el-image
:src="row.icon"
alt="图标"
style="width: 34px; height: 34px"
/>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" min-width="150" />
<el-table-column label="操作" min-width="80" fixed="right">
<!-- 操作 -->
<template #default="{ row }">
<el-button
v-perms="['setting:payment:editConfig']"
link
type="primary"
@click="handleEdit(row)"
>
配置
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<edit-popup v-if="showEdit" ref="editRef" @success="getConfig" @close="showEdit = false" />
</div>
</template>
<script lang="ts" setup>
import { getPayConfigLists } from '@/api/setting/pay'
import EditPopup from './edit.vue'
const payConfigList = ref<any[]>([])
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const getConfig = async () => {
const data = await getPayConfigLists()
payConfigList.value = data
}
const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open()
editRef.value?.getDetail(data)
}
getConfig()
</script>

View File

@@ -0,0 +1,138 @@
<template>
<div>
<div>
<el-button
type="primary"
v-perms="['setting:payment:editMethod']"
@click="handelSetupPayWay"
>
设置支付方式
</el-button>
</div>
<el-card
shadow="never"
class="mt-4 !border-none"
v-for="(value, scene) in payWay"
:key="scene"
>
<div>
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.MP_WEIXIN">
微信小程序
<span class="form-tips ml-[10px]">在微信小程序中付款的场景</span>
</div>
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.OA">
微信公众号
<span class="form-tips ml-[10px]">
在微信公众号H5页面中付款的场景公众号类型一般为服务号
</span>
</div>
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.H5">
H5支付
<span class="form-tips ml-[10px]">在浏览器H5页面中付款的场景</span>
</div>
<!-- <div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.PC">
PC支付
<span class="form-tips ml-[10px]">在浏览器PC页面中付款的场景</span>
</div>
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.APP">
APP支付
<span class="form-tips ml-[10px]">在APP付款的场景</span>
</div> -->
<el-table v-if="value.length" :data="value" style="width: 100%">
<el-table-column label="图标" min-width="150">
<template #default="{ row }">
<el-image
:src="row.icon"
alt="图标"
style="width: 34px; height: 34px"
/>
</template>
</el-table-column>
<el-table-column prop="name" label="支付方式" min-width="150" />
<el-table-column label="默认支付" min-width="150">
<template #default="{ row, $index }">
<div>
<template v-if="setupPayWay">
<el-radio
v-model="row.isDefault"
:label="1"
@change="changePayDefault($index, scene)"
>
设为默认
</el-radio>
</template>
<template v-else>
<el-tag v-if="row.isDefault == 1">默认</el-tag>
<span v-else>-</span>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="开启状态" min-width="150">
<template #default="{ row }">
<el-switch
v-if="setupPayWay"
v-model="row.status"
:active-value="1"
:inactive-value="0"
/>
<span v-else>
{{ row.status == 1 ? '开启' : '关闭' }}
</span>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<footer-btns v-if="setupPayWay">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getPayWay, setPayWay } from '@/api/setting/pay'
import feedback from '@/utils/feedback'
import { cloneDeep } from 'lodash'
enum PaySceneEnum {
MP_WEIXIN,
OA,
H5
}
const payWay = ref<Record<number, any[]>>({})
const setupPayWay = ref(false)
let defaultPayWay = {}
const getConfig = async () => {
payWay.value = await getPayWay()
defaultPayWay = cloneDeep(payWay.value)
}
const handelSetupPayWay = () => {
setupPayWay.value = true
}
const changePayDefault = (index: number, scene: number) => {
payWay.value[scene].forEach((item: any) => {
item.isDefault = 0
})
payWay.value[scene][index].isDefault = 1
}
const handleCancel = () => {
payWay.value = cloneDeep(defaultPayWay)
setupPayWay.value = false
}
const handleSubmit = async () => {
await setPayWay({
data: payWay.value
})
setupPayWay.value = false
feedback.msgSuccess('操作成功')
getConfig()
}
getConfig()
</script>

View File

@@ -1,3 +1,4 @@
import wechatOa from '@/utils/wechat'
import request from '@/utils/request'
//发送短信
@@ -24,3 +25,7 @@ export function uploadImage(file: any, token?: string) {
fileType: 'image'
})
}
export function wxJsConfig(data: any) {
return request.get({ url: '/wechat/jsConfig', data })
}

16
uniapp/src/api/pay.ts Normal file
View File

@@ -0,0 +1,16 @@
import request from '@/utils/request'
//支付方式
export function getPayWay(data: any) {
return request.get({ url: '/pay/payWay', data }, { isAuth: true })
}
// 预支付
export function prepay(data: any) {
return request.post({ url: '/pay/prepay', data }, { isAuth: true })
}
// 预支付
export function getPayResult(data: any) {
return request.get({ url: '/pay/payStatus', data }, { isAuth: true })
}

View File

@@ -0,0 +1,16 @@
import request from '@/utils/request'
//充值
export function recharge(data: any) {
return request.post({ url: '/recharge/placeOrder', data }, { isAuth: true })
}
//充值记录
export function rechargeRecord(data: any) {
return request.get({ url: '/recharge/record', data }, { isAuth: true })
}
// 充值配置
export function rechargeConfig() {
return request.get({ url: '/recharge/config' }, { isAuth: true })
}

View File

@@ -41,3 +41,8 @@ export function oaAuthBind(data: any) {
export function updateUser(data: Record<string, any>, header: any) {
return request.post({ url: '/user/updateUser', data, header })
}
//余额明细
export function accountLog(data: any) {
return request.get({ url: '/logs/userMoney', data })
}

View File

@@ -0,0 +1,62 @@
<template>
<view
class="page-status"
v-if="status !== PageStatusEnum['NORMAL']"
:class="{ 'page-status--fixed': fixed }"
>
<!-- Loading -->
<template v-if="status === PageStatusEnum['LOADING']">
<slot name="loading">
<u-loading :size="60" mode="flower" />
</slot>
</template>
<!-- Error -->
<template v-if="status === PageStatusEnum['ERROR']">
<slot name="error"></slot>
</template>
<!-- Empty -->
<template v-if="status === PageStatusEnum['EMPTY']">
<slot name="empty"></slot>
</template>
</view>
<template v-else>
<slot> </slot>
</template>
</template>
<script lang="ts" setup>
import { PageStatusEnum } from '@/enums/appEnums'
const props = defineProps({
status: {
type: String,
default: PageStatusEnum['LOADING']
},
fixed: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.page-status {
height: 100%;
width: 100%;
min-height: 100%;
padding: 0;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
&--fixed {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 900;
}
}
</style>

View File

@@ -0,0 +1,330 @@
<template>
<u-popup
v-model="showPay"
mode="bottom"
safe-area-inset-bottom
:mask-close-able="false"
border-radius="14"
closeable
@close="handleClose"
>
<view class="h-[900rpx]">
<page-status :status="popupStatus" :fixed="false">
<template #error>
<u-empty text="订单信息错误,无法查询到订单信息" mode="order"></u-empty>
</template>
<template #default>
<view class="payment h-full w-full flex flex-col">
<view class="header py-[50rpx] flex flex-col items-center">
<price
:content="payData.orderAmount"
mainSize="44rpx"
minorSize="40rpx"
fontWeight="500"
color="#333"
></price>
</view>
<view class="main flex-1 mx-[20rpx]">
<view>
<view class="payway-lists">
<u-radio-group v-model="payWay" class="w-full">
<view
class="p-[20rpx] flex items-center w-full payway-item"
v-for="(item, index) in payData.list"
:key="index"
@click="selectPayWay(item.id)"
>
<u-icon
class="flex-none"
:size="48"
:name="item.icon"
></u-icon>
<view class="mx-[16rpx] flex-1">
<view class="payway-item--name flex-1">
{{ item.name }}
</view>
<view class="text-muted text-xs">{{
item.extra
}}</view>
</view>
<u-radio class="mr-[-20rpx]" :name="item.id"> </u-radio>
</view>
</u-radio-group>
</view>
</view>
</view>
<view class="submit-btn p-[20rpx]">
<u-button
@click="handlePay"
shape="circle"
type="primary"
:loading="isLock"
>
立即支付
</u-button>
</view>
</view>
</template>
</page-status>
</view>
</u-popup>
<u-popup
class="pay-popup"
v-model="showCheckPay"
round
mode="center"
borderRadius="10"
:maskCloseAble="false"
>
<view class="content bg-white w-[560rpx] p-[40rpx]">
<view class="text-2xl font-medium text-center"> 支付确认 </view>
<view class="pt-[30rpx] pb-[40rpx]">
<view> 请在微信内完成支付如果您已支付成功请点击`已完成支付`按钮 </view>
</view>
<view class="flex justify-between">
<view class="w-[48%]">
<u-button
shape="circle"
type="primary"
plain
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="queryPayResult(false)"
>
重新支付
</u-button>
</view>
<view class="w-[48%]">
<u-button
shape="circle"
type="primary"
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="queryPayResult()"
>
已完成支付
</u-button>
</view>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { pay, PayWayEnum } from '@/utils/pay'
import { getPayWay, prepay, getPayResult } from '@/api/pay'
import { computed, ref, watch } from 'vue'
import { useLockFn } from '@/hooks/useLockFn'
import { series } from '@/utils/util'
import { ClientEnum, PageStatusEnum, PayStatusEnum } from '@/enums/appEnums'
import { useUserStore } from '@/stores/user'
import { client } from '@/utils/client'
import { onShow } from '@dcloudio/uni-app'
/*
页面参数 orderId订单idfrom订单来源
*/
const props = defineProps({
show: {
type: Boolean,
required: true
},
showCheck: {
type: Boolean
},
// 订单id
orderId: {
type: Number,
required: true
},
//订单来源
from: {
type: String,
required: true
},
//h5微信支付回跳路径一般为拉起支付的页面路径
redirect: {
type: String
}
})
const emit = defineEmits(['update:showCheck', 'update:show', 'close', 'success', 'fail'])
const payWay = ref()
const popupStatus = ref(PageStatusEnum.LOADING)
const payData = ref<any>({
orderAmount: '',
list: []
})
const payStatus = ref()
const showCheckPay = computed({
get() {
return props.showCheck
},
set(value) {
emit('update:showCheck', value)
}
})
const showPay = computed({
get() {
return props.show
},
set(value) {
emit('update:show', value)
}
})
const handleClose = () => {
showPay.value = false
emit('close')
}
const getPayData = async () => {
popupStatus.value = PageStatusEnum.LOADING
try {
payData.value = await getPayWay({
orderId: props.orderId,
from: props.from
})
popupStatus.value = PageStatusEnum.NORMAL
const checkPay =
payData.value.list.find((item: any) => item.isDefault) || payData.value.list[0]
payWay.value = checkPay?.id
} catch (error) {
popupStatus.value = PageStatusEnum.ERROR
}
}
const userStore = useUserStore()
const selectPayWay = (pay: number) => {
payWay.value = pay
}
const payment = (() => {
// 查询是否绑定微信
const checkIsBindWx = async () => {
if (
userStore.userInfo.isBindWechat == 0 &&
[ClientEnum.OA_WEIXIN, ClientEnum.MP_WEIXIN].includes(client) &&
payWay.value == PayWayEnum.WECHAT
) {
const res: any = await uni.showModal({
title: '温馨提示',
content: '当前账号未绑定微信,无法完成支付',
confirmText: '去绑定'
})
if (res.confirm) {
uni.navigateTo({
url: '/pages/user_set/user_set'
})
}
return Promise.reject()
}
}
// 调用预支付
const prepayTask = async () => {
uni.showLoading({
title: '正在支付中'
})
const data = await prepay({
orderId: props.orderId,
scene: props.from,
payWay: payWay.value,
redirectUrl: props.redirect
})
return data
}
//拉起支付
const payTask = async (data: any) => {
try {
const res = await pay.payment(payWay.value, data)
return res
} catch (error) {
return Promise.reject(error)
}
}
return series(checkIsBindWx, prepayTask, payTask)
})()
const { isLock, lockFn: handlePay } = useLockFn(async () => {
try {
const res: PayStatusEnum = await payment()
handlePayResult(res)
uni.hideLoading()
} catch (error) {
uni.hideLoading()
console.log(error)
}
})
const handlePayResult = (status: PayStatusEnum) => {
payStatus.value = status
switch (status) {
case PayStatusEnum.SUCCESS:
emit('success')
break
case PayStatusEnum.FAIL:
emit('fail')
break
}
}
const queryPayResult = async (confirm = true) => {
const res = await getPayResult({
orderId: props.orderId,
from: props.from
})
if (res.payStatus === 0) {
if (confirm == true) {
uni.$u.toast('您的订单还未支付,请重新支付')
}
showPay.value = true
handlePayResult(PayStatusEnum.FAIL)
} else {
if (confirm == false) {
uni.$u.toast('您的订单已经支付,请勿重新支付')
}
handlePayResult(PayStatusEnum.SUCCESS)
}
showCheckPay.value = false
}
watch(
() => props.show,
(value) => {
if (value) {
if (!props.orderId) {
popupStatus.value = PageStatusEnum.ERROR
return
}
getPayData()
}
},
{
immediate: true
}
)
onShow(() => {
if (payStatus.value == PayStatusEnum.PENDING) {
showPay.value = false
showCheckPay.value = true
}
})
</script>
<style lang="scss">
.payway-lists {
.payway-item {
border-bottom: 1px solid;
@apply border-page;
}
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<view class="price-container">
<view
:class="['price-wrap', { 'price-wrap--disabled': lineThrough }]"
:style="{ color: color }"
>
<!-- Prefix -->
<view class="fix-pre" :style="{ fontSize: minorSize }">
<slot name="prefix">{{ prefix }}</slot>
</view>
<!-- Content -->
<view :style="{ 'font-weight': fontWeight }">
<!-- Integer -->
<text :style="{ fontSize: mainSize }">{{ integer }}</text>
<!-- Decimals -->
<text :style="{ fontSize: minorSize }">{{ decimals }}</text>
</view>
<!-- Suffix -->
<view class="fix-suf" :style="{ fontSize: minorSize }">
<slot name="suffix">{{ suffix }}</slot>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
/**
* @description 价格展示,适用于有前后缀,小数样式不一
* @property {String|Number} content 价格 (必填项)
* @property {Number} prec 小数位 (默认: 2)
* @property {Boolean} autoPrec 自动小数位【注以prec为最大小数位】 (默认: true)
* @property {String} color 颜色 (默认: 'unset')
* @property {String} mainSize 主要内容字体大小 (默认: 46rpx)
* @property {String} minorSize 主要内容字体大小 (默认: 32rpx)
* @property {Boolean} lineThrough 贯穿线 (默认: false)
* @property {String|Number} fontWeight 字重 (默认: normal)
* @property {String} prefix 前缀 (默认: ¥)
* @property {String} suffix 后缀
* @example <price content="100" suffix="\/元" />
*/
import { computed } from 'vue'
import { formatPrice } from '@/utils/util'
/** Props Start **/
const props = withDefaults(
defineProps<{
content: string | number // 标题
prec?: number // 小数数量
autoPrec?: boolean // 动态小数
color?: string // 颜色
mainSize?: string // 主要内容字体大小
minorSize?: string // 次要内容字体大小
lineThrough?: boolean // 贯穿线
fontWeight?: string // 字重
prefix?: string // 前缀
suffix?: string // 后缀
}>(),
{
content: '',
prec: 2,
autoPrec: true,
color: '#FA8919',
mainSize: '36rpx',
minorSize: '28rpx',
lineThrough: false,
fontWeight: 'normal',
prefix: '¥',
suffix: ''
}
)
/** Props End **/
/** Computed Start **/
/**
* @description 金额主体部分
*/
const integer = computed(() => {
return formatPrice({
price: props.content,
take: 'int'
})
})
/**
* @description 金额小数部分
*/
const decimals = computed(() => {
let decimals = formatPrice({
price: props.content,
take: 'dec',
prec: props.prec
})
// 小数余十不能是 .10||.20||.30以此类推,
decimals = decimals % 10 == 0 ? decimals.substr(0, decimals.length - 1) : decimals
return props.autoPrec ? (decimals * 1 ? '.' + decimals : '') : props.prec ? '.' + decimals : ''
})
/** Computed End **/
</script>
<style lang="scss" scoped>
.price-container {
display: inline-block;
}
.price-wrap {
display: flex;
align-items: baseline;
&--disabled {
position: relative;
&::before {
position: absolute;
left: 0;
top: 50%;
right: 0;
transform: translateY(-50%);
display: block;
content: '';
height: 0.05em;
background-color: currentColor;
}
}
}
</style>

View File

@@ -32,3 +32,18 @@ export enum FieldType {
NICKNAME = 'nickname',
SEX = 'sex'
}
// 支付结果
export enum PayStatusEnum {
SUCCESS = 'success',
FAIL = 'fail',
PENDING = 'pending'
}
// 页面状态
export enum PageStatusEnum {
LOADING = 'loading', // 加载中
NORMAL = 'normal', // 正常
ERROR = 'error', // 异常
EMPTY = 'empty' // 为空
}

View File

@@ -0,0 +1,102 @@
<template>
<view class="recharge p-[20rpx]">
<view class="bg-white rounded-[14rpx] p-[40rpx]">
<view class="text-content">充值金额</view>
<view class="border-0 border-b border-solid border-light">
<input
v-model="money"
class="text-[60rpx] h-[60rpx] py-[24rpx]"
placeholder="0.00"
type="text"
/>
</view>
<view class="mt-[20rpx] text-xs text-muted">
当前可用余额
<text class="text-primary"> {{ wallet.userMoney }}</text>
</view>
</view>
<view class="mt-[40rpx]">
<u-button :loading="isLock" type="primary" shape="circle" @click="rechargeLock">
立即充值
</u-button>
</view>
<view class="flex justify-center m-[60rpx]">
<navigator url="/packages/pages/recharge_record/recharge_record" hover-class="none">
<text class="text-content text-sm">充值记录</text>
</navigator>
</view>
<payment
v-model:show="payState.showPay"
v-model:show-check="payState.showCheck"
:order-id="payState.orderId"
:from="payState.from"
:redirect="payState.redirect"
@success="handlePaySuccess"
@fail="handlePayFail"
/>
</view>
</template>
<script lang="ts" setup>
import { recharge, rechargeConfig } from '@/api/recharge'
import { useLockFn } from '@/hooks/useLockFn'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { reactive, ref } from 'vue'
const money = ref('')
const payState = reactive({
orderId: '',
from: 'recharge',
showPay: false,
showCheck: false,
redirect: '/packages/pages/recharge/recharge'
})
const wallet = reactive({
userMoney: '',
minRechargeMoney: 0
})
const { isLock, lockFn: rechargeLock } = useLockFn(async () => {
const minNum = wallet.minRechargeMoney
if (!money.value) return uni.$u.toast('请输入充值金额')
if (minNum == 0 && Number(money.value) == minNum) {
return uni.$u.toast(`充值金额必须大于0`)
}
if (Number(money.value) < minNum) return uni.$u.toast(`最低充值金额${minNum}`)
const data = await recharge({
orderAmount: money.value,
payWay: 2
})
payState.orderId = data.orderId
payState.showPay = true
})
const handlePaySuccess = async () => {
payState.showPay = false
payState.showCheck = false
uni.navigateTo({
url: `/pages/payment_result/payment_result?id=${payState.orderId}&from=${payState.from}`
})
}
// const handlePayFail = async () => {
// uni.$u.toast('支付失败')
// }
const getWallet = async () => {
const data = await rechargeConfig()
Object.assign(wallet, data)
}
onLoad((options: any) => {
// h5支付用于弹起手动确认支付弹窗
// if (options?.checkPay) {
// payState.orderId = options.orderId
// payState.from = options.scene
// payState.showCheck = true
// }
})
onShow(() => {
getWallet()
})
</script>

View File

@@ -0,0 +1,44 @@
<template>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:show-loading-more-when-reload="true"
>
<view class="pt-2.5">
<view
v-for="item in dataList"
:key="item.id"
class="bg-white border-solid border-b border-0 border-light px-[26rpx] py-[24rpx]"
>
<view class="flex justify-between">
<view class="mr-2">{{ item.tips }}</view>
<view class="text-lg text-primary"> +{{ item.orderAmount }} </view>
</view>
<view class="text-sm text-muted mr-1">{{ item.createTime }}</view>
</view>
</view>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { rechargeRecord } from '@/api/recharge'
const paging = shallowRef()
const dataList = ref<any[]>([])
const queryList = async (pageNo: number, pageSize: number) => {
try {
const data = await rechargeRecord({
page_no: pageNo,
page_size: pageSize
})
paging.value.complete(data.lists)
} catch (error) {
paging.value.complete(false)
}
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,118 @@
<template>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:auto="false"
@onRefresh="getWallet()"
:show-loading-more-when-reload="true"
>
<view class="user-wallet">
<view class="p-[20rpx]">
<view
class="bg-primary rounded-[14rpx] flex items-center justify-between pl-[44rpx] py-[54rpx] text-white"
>
<view>
<view class="text-sm">钱包余额</view>
<view class="text-[60rpx]">{{ wallet.userMoney }}</view>
</view>
<navigator
v-if="wallet.openRecharge"
url="/packages/pages/recharge/recharge"
hover-class="none"
>
<view class="text-primary px-[30rpx] py-[15rpx] bg-white rounded-l-full">
去充值
</view>
</navigator>
</view>
</view>
<u-tabs
:list="tabList"
:is-scroll="false"
v-model="current"
@change="changeType"
></u-tabs>
<view class="pt-2.5">
<view
v-for="item in dataList"
:key="item.id"
class="bg-white border-solid border-b border-0 border-light px-[26rpx] py-[24rpx]"
>
<view class="flex justify-between">
<view class="mr-2">{{ item.tips }}</view>
<view
class="text-lg"
:class="{
'text-primary': item.action == 1
}"
>
{{ item.orderAmount }}
</view>
</view>
<view class="text-sm text-muted mr-1">{{ item.createTime }}</view>
</view>
</view>
</view>
</z-paging>
</template>
<script lang="ts" setup>
import { nextTick, ref, shallowRef } from 'vue'
import { accountLog } from '@/api/user'
import { rechargeConfig } from '@/api/recharge'
import { onLoad, onShow } from '@dcloudio/uni-app'
const tabList = ref([
{
name: '全部',
type: 0
},
{
name: '收入',
type: 1
},
{
name: '支出',
type: 2
}
])
const paging = shallowRef()
const dataList = ref<any[]>([])
const current = ref(0)
const changeType = (index: number) => {
current.value = index
paging.value.reload()
}
const queryList = async (pageNo: number, pageSize: number, from: any) => {
try {
console.log(from)
const type = tabList.value[current.value].type
const data = await accountLog({
type,
page_no: pageNo,
page_size: pageSize
})
paging.value.complete(data.lists)
} catch (error) {
paging.value.complete(false)
}
}
const wallet = ref<any>({})
const getWallet = async () => {
wallet.value = await rechargeConfig()
}
onShow(() => {
getWallet()
})
onLoad(() => {
nextTick(() => {
paging.value?.reload()
})
})
</script>
<style lang="scss" scoped></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -109,7 +109,14 @@
"style": {
"navigationStyle": "custom"
}
},
},
{
"path": "pages/payment_result/payment_result",
"style": {
"navigationBarTitleText": "支付结果"
},
"auth": true
},
{
"path": "uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper",
"style": {
@@ -117,7 +124,33 @@
"navigationBarBackgroundColor": "#000000"
}
}
],
],
"subPackages": [{
"root": "packages",
"pages": [
{
"path": "pages/user_wallet/user_wallet",
"style": {
"navigationBarTitleText": "我的钱包"
},
"auth": true
},
{
"path": "pages/recharge/recharge",
"style": {
"navigationBarTitleText": "充值"
},
"auth": true
},
{
"path": "pages/recharge_record/recharge_record",
"style": {
"navigationBarTitleText": "充值记录"
},
"auth": true
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "商城",

View File

@@ -0,0 +1,157 @@
<template>
<!-- 页面状态 -->
<page-status :status="status">
<template #error>
<u-empty text="订单不存在" mode="order"></u-empty>
</template>
<template #default>
<view class="payment-result p-[20rpx]">
<view class="result bg-white p-[20rpx] rounded-md">
<view class="flex flex-col items-center my-[40rpx]">
<!-- 支付状态图片 -->
<u-image
class="status-image"
:src="paymentStatus['image']"
width="100"
height="100"
shape="circle"
/>
<!-- 支付状态文字 -->
<text class="text-2xl font-medium mt-[20rpx]">{{
paymentStatus['text']
}}</text>
<view class="text-3xl font-medium mt-[20rpx]">
¥ {{ orderInfo.orderAmount }}
</view>
</view>
<!-- 支付信息 -->
<view class="result-info">
<view class="result-info__item">
<text>订单编号</text>
<text>{{ orderInfo.orderSn }}</text>
</view>
<view class="result-info__item">
<text>付款时间</text>
<text>{{ orderInfo.payTime || '-' }}</text>
</view>
<view class="result-info__item">
<text>支付方式</text>
<template v-if="orderInfo.payStatus">
<text>{{ orderInfo.payWay || '-' }}</text>
</template>
<template v-else>
<text>未支付</text>
</template>
</view>
</view>
</view>
<view class="mt-[40rpx]">
<view class="mb-[20rpx]">
<u-button
v-if="pageOptions.from == 'recharge'"
type="primary"
shape="circle"
hover-class="none"
@click="goOrder"
>
继续充值
</u-button>
</view>
<view class="mb-[20rpx]">
<u-button
type="primary"
plain
shape="circle"
hover-class="none"
@click="goHome"
>
返回首页
</u-button>
</view>
</view>
</view>
</template>
</page-status>
</template>
<script lang="ts" setup>
import { getPayResult } from '@/api/pay'
import { PageStatusEnum } from '@/enums/appEnums'
import { onLoad } from '@dcloudio/uni-app'
import { computed, reactive, ref } from 'vue'
const mapStatus = {
succeed: {
text: '支付成功',
image: '/static/images/payment/icon_succeed.png'
},
waiting: {
text: '等待支付',
image: '/static/images/payment/icon_waiting.png'
}
}
const status = ref(PageStatusEnum['LOADING'])
const pageOptions = ref({
id: '',
from: ''
})
const orderInfo = reactive<any>({
order: {}
})
const paymentStatus = computed(() => {
const status = !!orderInfo.payStatus
return mapStatus[status ? 'succeed' : 'waiting']
})
const initPageData = () => {
return new Promise((resolve, reject) => {
getPayResult({
orderId: pageOptions.value.id,
from: pageOptions.value.from
})
.then((data) => {
Object.assign(orderInfo, data)
resolve(data)
})
.catch((err) => {
reject(err)
})
})
}
const goHome = () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
const goOrder = () => {
switch (pageOptions.value.from) {
case 'recharge':
uni.navigateBack()
break
}
}
onLoad(async (options: any) => {
try {
if (!options.id) throw new Error('订单不存在')
pageOptions.value = options
await initPageData()
status.value = PageStatusEnum['NORMAL']
} catch (err) {
console.log(err)
status.value = PageStatusEnum['ERROR']
}
})
</script>
<style lang="scss" scoped>
.result-info {
.result-info__item {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
}
</style>

View File

@@ -145,13 +145,14 @@ const bindWechat = async () => {
await mnpAuthBind({
code: code
})
uni.$u.toast('绑定成功')
//#endif
// #ifdef H5
if (isWeixin.value) {
wechatOa.getUrl()
}
// #endif
uni.$u.toast('绑定成功')
await userStore.getUser()
uni.hideLoading()
} catch (e) {

View File

@@ -1,7 +1,12 @@
import { ClientEnum } from '@/enums/appEnums'
import { BACK_URL } from '@/enums/cacheEnums'
import { useUserStore } from '@/stores/user'
import { getToken } from '@/utils/auth'
import cache from '@/utils/cache'
import { client } from '@/utils/client'
// #ifdef H5
import wechatOa from '@/utils/wechat'
// #endif
import { routes } from './routes'
const whiteList = ['register', 'login', 'forget_pwd']
@@ -41,6 +46,11 @@ export function setupRouter() {
cache.set(BACK_URL, from.fullPath)
}
})
setTimeout(async () => {
if (client == ClientEnum.OA_WEIXIN) {
// jssdk配置
await wechatOa.config()
}
})
// #endif
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -183,13 +183,18 @@
watch: {
// 监听tab的变化重新计算tab菜单的布局信息因为实际使用中菜单可能是通过
// 后台获取的如新闻app顶部的菜单获取返回需要一定时间所以list变化时重新获取布局信息
list(n, o) {
// list变动时重制内部索引否则可能导致超出数组边界的情况
if(n.length !== o.length) this.currentIndex = 0;
// 用$nextTick等待视图更新完毕后再计算tab的局部信息否则可能因为tab还没生成就获取就会有问题
this.$nextTick(() => {
this.init();
});
list: {
immediate: true,
handler(n, o) {
if(o) {
// list变动时重制内部索引否则可能导致超出数组边界的情况
if(n.length !== o.length) this.currentIndex = 0;
// 用$nextTick等待视图更新完毕后再计算tab的局部信息否则可能因为tab还没生成就获取就会有问题
}
setTimeout(() => {
this.init();
},200)
}
},
current: {
immediate: true,
@@ -290,6 +295,7 @@
for (let i = 0; i < this.list.length; i++) {
// 只要size和rect两个参数
query.select(`#u-tab-item-${i}`).fields({
id: true,
size: true,
rect: true
});
@@ -319,6 +325,7 @@
let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
// 计算当前活跃item到组件左边的距离
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
// 第一次移动滑块的时候barFirstTimeMove为true放到延时中将其设置false
// 延时是因为scrollBarLeft作用于computed计算时需要一个过程需否则导致出错
if(this.barFirstTimeMove == true) {

View File

@@ -43,7 +43,7 @@ export const getClient = () => {
// 根据端处理事件
//@ts-ignore
export const handleClientEvent = ({ MP_WEIXIN, OA_WEIXIN, H5, IOS, ANDROID, OTHER }) => {
export const handleClientEvent = ({ MP_WEIXIN, OA_WEIXIN, H5, IOS, ANDROID, OTHER }: any) => {
// #ifdef MP-WEIXIN
return MP_WEIXIN()
// #endif

View File

@@ -0,0 +1,14 @@
import { Pay } from './pay'
import { Wechat } from './wechat'
// 支付方式
enum PayWayEnum {
BALANCE = 1,
WECHAT = 2,
ALIPAY = 3
}
const wechat = new Wechat()
// 注入微信支付
Pay.inject(PayWayEnum[2], wechat)
const pay = new Pay()
export { pay, PayWayEnum }

View File

@@ -0,0 +1,28 @@
import { PayWayEnum } from '.'
export class Pay {
private static modules = new Map()
static inject(name: string, module: any) {
this.modules.set(name, module)
}
constructor() {
//动态注入支付方式
for (const [name, module] of Pay.modules.entries()) {
module.init(name, this)
}
}
//调用支付
async payment(payWay: PayWayEnum, options: any) {
try {
//@ts-ignore
const module = this[PayWayEnum[payWay]]
if (!module) {
throw new Error(`can not find pay way ${payWay}`)
}
return await module.run(options)
} catch (error) {
return Promise.reject(error)
}
}
}

View File

@@ -0,0 +1,58 @@
import { PayStatusEnum } from '@/enums/appEnums'
import { handleClientEvent } from '../client'
//#ifdef H5
import wechatOa from '../wechat'
//#endif
export class Wechat {
init(name: string, pay: any) {
pay[name] = this
}
async run(options: any) {
try {
const res = await handleClientEvent({
MP_WEIXIN: () => {
return new Promise((resolve) => {
console.log(options)
uni.requestPayment({
orderInfo: '',
provider: 'wxpay',
timeStamp: options.timeStamp,
nonceStr: options.nonceStr,
package: options.packageValue,
paySign: options.paySign,
signType: options.signType,
success() {
resolve(PayStatusEnum.SUCCESS)
},
fail() {
resolve(PayStatusEnum.FAIL)
}
})
})
},
OA_WEIXIN: () => {
return new Promise((resolve) => {
wechatOa
.pay(options)
.then(() => {
resolve(PayStatusEnum.SUCCESS)
})
.catch(() => {
resolve(PayStatusEnum.FAIL)
})
})
},
H5: () => {
return new Promise((resolve) => {
window.open(options.url, '_self')
resolve(PayStatusEnum.PENDING)
})
}
})
return res
} catch (error) {
return Promise.reject(error)
}
}
}

View File

@@ -94,6 +94,33 @@ export function objectToQuery(params: Record<string, any>): string {
return query.slice(0, -1)
}
/**
* @description 格式化输出价格
* @param { string } price 价格
* @param { string } take 小数点操作
* @param { string } prec 小数位补
*/
export function formatPrice({ price, take = 'all', prec = undefined }: any) {
let [integer, decimals = ''] = (price + '').split('.')
// 小数位补
if (prec !== undefined) {
const LEN = decimals.length
for (let i = prec - LEN; i > 0; --i) decimals += '0'
decimals = decimals.substr(0, prec)
}
switch (take) {
case 'int':
return integer
case 'dec':
return decimals
case 'all':
return integer + '.' + decimals
}
}
/**
* @description 组合异步任务
* @param { string } task 异步任务

View File

@@ -1,7 +1,7 @@
import weixin from 'weixin-js-sdk'
import { getWxCodeUrl, OALogin } from '@/api/account'
import { useUserStore } from '@/stores/user'
import { isAndroid } from './client'
import { wxJsConfig } from '@/api/app'
const wechatOa = {
getSignLink() {
@@ -15,6 +15,27 @@ const wechatOa = {
location.href = res.url
})
},
config() {
return new Promise((resolve, reject) => {
wxJsConfig({
url: this.getSignLink()
}).then((res) => {
weixin.config({
appId: res.appid, // 必填,公众号的唯一标识
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.nonceStr, // 必填,生成签名的随机串
signature: res.signature, // 必填,签名
jsApiList: res.jsApiList,
success: () => {
resolve('success')
},
fail: (res: any) => {
reject('weixin config is fail')
}
})
})
})
},
authLogin(code: string) {
return new Promise((resolve, reject) => {
OALogin({
@@ -35,6 +56,36 @@ const wechatOa = {
})
})
},
pay(options: Record<any, any>) {
return new Promise((resolve, reject) => {
this.ready()
.then(() => {
weixin.chooseWXPay({
timestamp: options.timeStamp,
nonceStr: options.nonceStr,
package: options.packageValue,
signType: options.signType,
paySign: options.paySign,
success: (res: any) => {
if (res.errMsg === 'chooseWXPay:ok') {
resolve(res)
} else {
reject(res.errMsg)
}
},
cancel: (res: any) => {
reject(res)
},
fail: (res: any) => {
reject(res)
}
})
})
.catch((err) => {
reject(err)
})
})
},
share(options: Record<any, any>) {
this.ready().then(() => {
const { shareTitle, shareLink, shareImage, shareDesc } = options
@@ -91,24 +142,3 @@ const wechatOa = {
}
export default wechatOa
// export function wxOaConfig() {
// return new Promise((resolve, reject) => {
// apiJsConfig().then((res) => {
// console.log(res) //微信配置
// weixin.config({
// debug: false, // 开启调试模式
// appId: res.appId, // 必填,公众号的唯一标识
// timestamp: res.timestamp, // 必填,生成签名的时间戳
// nonceStr: res.nonceStr, // 必填,生成签名的随机串
// signature: res.signature, // 必填,签名
// jsApiList: res.jsApiList, // 必填需要使用的JS接口列表
// success: () => {
// resolve('success')
// },
// fail: (res: any) => {
// reject('weixin config is fail')
// }
// })
// })
// })
// }

View File

@@ -17,5 +17,12 @@
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "typings/**/*.d.ts"]
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"typings/**/*.d.ts",
"../admin/src/api/finance.ts"
]
}