feat(wallet): 新增代理端钱包提现申请功能- 新增代理端钱包提现申请页面及相关接口

- 调整部分页面样式及布局以适配新设计
- 更新应用主题色以匹配品牌视觉规范
-优化部分组件交互逻辑提升用户体验- 调整Vant组件库相关样式变量配置- 修改全局主色调及相关按钮颜色配置- 优化商户端及代理商端页面头部样式- 统一卡片组件样式并移除固定定位按钮
- 调整页面滚动行为以改善移动端表现- 移除部分冗余样式代码并精简结构
This commit is contained in:
bootx
2025-11-08 19:31:28 +08:00
parent 37a908239f
commit 9cbe6dd839
17 changed files with 678 additions and 115 deletions

View File

@@ -15,7 +15,7 @@
;(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const darkMode = mediaQuery.matches ? 'light' : 'light'
const { appTheme = '#5d9dfe' } =
const { appTheme = '#f8d03b' } =
JSON.parse(window.localStorage.getItem('DESIGN-SETTING')) || {}
let htmlRoot = document.getElementById('htmlRoot')

View File

@@ -218,6 +218,14 @@ export const DaxPayAgentMiniRoute: RouteRecordRaw = {
title: '结算信息',
},
},
{
path: '/mini/agent/wallet/cashouts',
name: 'AgentCashoutsApply',
component: () => import('@/views/daxpay/miniapp/agent/wallet/cashouts/CashoutsApply.vue'),
meta: {
title: '提现申请',
},
},
],
}

View File

@@ -35,7 +35,7 @@ export const appThemeList: string[] = [
const setting: DesignSettingState = {
// 系统主题色
appTheme: '#5d9dfe',
appTheme: '#f2d15a',
// 系统内置主题色列表
appThemeList,
// 是否开启路由动画

View File

@@ -4,6 +4,9 @@
// 详情https: //vant-contrib.gitee.io/vant/v4/#/zh-CN/config-provider#bian-liang-lie-biao
body {
--van-primary-color: @primaryColor;
--van-button-default-color: #777777;
--van-button-primary-color: #777777;
--van-button-normal-font-size: 14px;
}
.van-action-sheet__cancel {
@@ -16,3 +19,6 @@ body {
justify-content: center;
height: 100%;
}
.van-button__text{
//color: #777777;
}

View File

@@ -1,4 +1,4 @@
// 这里定义全局变量
@primaryColor: #5d9dfe;
@primaryColor: #f8d03b;
@header-height: 46px;
@footer-height: 50px;

View File

@@ -777,7 +777,7 @@
</van-button>
</div>
<div class="btnBox" style="margin-bottom: 20px">
<van-button type="primary" plain block :disabled="showable" @click="saveTemp">
<van-button type="primary" color="#ff8a00" plain block :disabled="showable" @click="saveTemp">
暂存
</van-button>
</div>
@@ -904,16 +904,16 @@ function initData() {
pca.value = data
})
// 获取权限配置和费率配置
Promise.all([
getAgentPermConfig(),
getConfig(),
]).then(([permRes, rateRes]) => {
agentPerm.value = permRes.data
agentRate.value = rateRes.data
}).catch(() => {
showNotify({ type: 'danger', message: '获取权限或费率配置失败' })
})
// 只有在dax-pay-agent客户端才获取权限配置和费率配置
if (clientCode === 'dax-pay-agent') {
Promise.all([
getAgentPermConfig(),
getConfig(),
]).then(([permRes, rateRes]) => {
agentPerm.value = permRes.data
agentRate.value = rateRes.data
})
}
}
/**
@@ -1154,7 +1154,7 @@ async function readMchData() {
font-weight: 600;
letter-spacing: 2px;
.current {
color: #448ef7;
color: #ff8a00;
}
}
.formBox {
@@ -1171,7 +1171,7 @@ async function readMchData() {
align-items: center;
padding: 0 1.25rem;
background-color: #f7f7f7;
color: #448ef7;
color: #5C4B37;
letter-spacing: 1px;
position: relative;
font-weight: 600;
@@ -1183,7 +1183,7 @@ async function readMchData() {
content: '';
width: 0.3125rem;
height: 1.25rem;
background-color: #448ef7;
background-color: #ff8a00;
}
}
}

View File

@@ -631,7 +631,7 @@
</van-button>
</div>
<div class="btnBox" style="margin-bottom: 20px">
<van-button type="primary" :disabled="showable" plain block @click="saveTemp">
<van-button type="primary" color="#ff8a00" :disabled="showable" plain block @click="saveTemp">
暂存
</van-button>
</div>
@@ -970,7 +970,7 @@ async function readMchData() {
font-weight: 600;
letter-spacing: 2px;
.current {
color: #448ef7;
color: #ff8a00;
}
}
.formBox {
@@ -987,7 +987,7 @@ async function readMchData() {
align-items: center;
padding: 0 1.25rem;
background-color: #f7f7f7;
color: #448ef7;
color: #5C4B37;
letter-spacing: 1px;
position: relative;
font-weight: 600;
@@ -999,7 +999,7 @@ async function readMchData() {
content: '';
width: 0.3125rem;
height: 1.25rem;
background-color: #448ef7;
background-color: #ff8a00;
}
}
}

View File

@@ -507,7 +507,7 @@
</van-button>
</div>
<div class="btnBox" style="margin-bottom: 20px">
<van-button type="primary" :disabled="showable" plain block @click="saveTemp">
<van-button type="primary" color="#ff8a00" :disabled="showable" plain block @click="saveTemp">
暂存
</van-button>
</div>
@@ -813,7 +813,7 @@ async function readMchData() {
font-weight: 600;
letter-spacing: 2px;
.current {
color: #448ef7;
color: #ff8a00;
}
}
.formBox {
@@ -830,7 +830,7 @@ async function readMchData() {
align-items: center;
padding: 0 1.25rem;
background-color: #f7f7f7;
color: #448ef7;
color: #5C4B37;
letter-spacing: 1px;
position: relative;
font-weight: 600;
@@ -842,7 +842,7 @@ async function readMchData() {
content: '';
width: 0.3125rem;
height: 1.25rem;
background-color: #448ef7;
background-color: #ff8a00;
}
}
}

View File

@@ -951,7 +951,7 @@
</van-button>
</div>
<div class="btnBox" style="margin-bottom: 20px">
<van-button type="primary" :disabled="showable" plain block @click="saveTemp">
<van-button type="primary" color="#ff8a00" :disabled="showable" plain block @click="saveTemp">
暂存
</van-button>
</div>
@@ -1356,7 +1356,7 @@ async function readMchData() {
font-weight: 600;
letter-spacing: 2px;
.current {
color: #448ef7;
color: #ff8a00;
}
}
.formBox {
@@ -1373,7 +1373,7 @@ async function readMchData() {
align-items: center;
padding: 0 1.25rem;
background-color: #f7f7f7;
color: #448ef7;
color: #5C4B37;
letter-spacing: 1px;
position: relative;
font-weight: 600;
@@ -1385,7 +1385,7 @@ async function readMchData() {
content: '';
width: 0.3125rem;
height: 1.25rem;
background-color: #448ef7;
background-color: #ff8a00;
}
}
}

View File

@@ -653,11 +653,11 @@ onMounted(() => {
}
.form-content {
/* 主体内容滚动 */
flex: 1;
overflow-y: scroll;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 20px 0;
padding-bottom: 80px; /* 为底部按钮留出空间 */
padding-bottom: 12px;
}
.loading-wrapper {
@@ -672,44 +672,26 @@ onMounted(() => {
/* 卡片样式 */
.status-card,
.info-card {
margin-bottom: 16px;
background: #fff;
border-radius: 0;
margin: 12px 8px;
border-radius: 8px;
overflow: hidden;
box-shadow: none;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.card-header {
height: 50px;
width: 100%;
display: flex;
align-items: center;
padding: 0 20px;
background-color: #f7f7f7;
color: #448ef7;
letter-spacing: 1px;
position: relative;
font-weight: 600;
border-bottom: none;
&::before {
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
content: '';
width: 5px;
height: 20px;
background-color: #448ef7;
}
height: 34px;
padding: 0 14px;
background-color: #fff4c5;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #448ef7;
font-weight: 500;
color: #5c4b37;
line-height: 1.2;
}
/* 认证状态样式优化 */
@@ -758,10 +740,6 @@ onMounted(() => {
}
.bottom-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 15px 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);

View File

@@ -318,14 +318,16 @@ onMounted(() => {
.card-header {
display: flex;
align-items: center;
height: 40px;
height: 34px;
padding: 0 14px;
background-color: #ecf5ff;
background-color: #fff4c5;
}
.card-title {
font-size: 16px;
color: #409eff;
font-size: 14px;
font-weight: 500;
color: #5c4b37;
line-height: 1.2;
}
/* 让内嵌面板去掉默认边距,卡片白底由容器负责 */

View File

@@ -242,14 +242,14 @@ onMounted(() => {
.card-header {
display: flex;
align-items: center;
height: 40px;
height: 34px;
padding: 0 14px;
background-color: #ecf5ff;
background-color: #fff4c5;
}
.card-title {
font-size: 16px;
color: #409eff;
font-size: 14px;
color: #5c4b37;
}
.bottom-btn {

View File

@@ -0,0 +1,160 @@
import { http } from '@/utils/http/axios'
import type { MchEntity, Result } from '#/axios'
/**
* 查询钱包
*/
export function getWallet() {
return http.request<Result<AgentWallet>>({
method: 'get',
url: '/agent/wallet/get',
})
}
/**
* 创建提现申请
*/
export function createApply(params: AgentCashouts) {
return http.request<Result<number>>({
method: 'post',
url: '/agent/cashouts/create',
data: params,
})
}
/**
* 获取提现配置
*/
export function getConfig() {
return http.request<Result<AgentCashoutsConfig>>({
method: 'get',
url: '/agent/cashouts/config/get',
})
}
/**
* 提现手续费金额计算
*/
export function calcCashoutsFee(cashoutsAmount: number) {
return http.request<Result<AgentCashoutsConfig>>({
method: 'get',
url: '/agent/cashouts/calcCashoutsFee',
params: { cashoutsAmount },
})
}
/**
* 提现申请分页
*/
export function pageCashouts(params: any) {
return http.request<Result<any>>({
method: 'get',
url: '/agent/cashouts/page',
params,
})
}
/**
* 提现申请详情
*/
export function getCashouts(id: number) {
return http.request<Result<AgentCashouts>>({
method: 'get',
url: '/agent/cashouts/findById',
params: { id },
})
}
/**
* 修改提现申请
*/
export function updateCashouts(params: AgentCashouts) {
return http.request<Result<void>>({
method: 'post',
url: '/agent/cashouts/update',
data: params,
})
}
/**
* 关闭提现申请
*/
export function closeCashouts(id: number) {
return http.request<Result<void>>({
method: 'post',
url: '/agent/cashouts/close',
params: { id },
})
}
/**
* 钱包
*/
export interface AgentWallet extends MchEntity {
/** 余额 */
balance?: number
/** 可用余额 */
availableBalance?: number
/** 冻结金额 */
freezeBalance?: number
/** 提现中金额 */
withdrawBalance?: number
/** 在途金额 */
onWayBalance?: number
}
/**
* 提现申请
*/
export interface AgentCashouts extends MchEntity {
/** 申请名称 */
name?: string
/** 提现方式 */
type?: string
/** 提现金额 */
amount?: number
/** 到账金额 */
arriveAmount?: number
/** 提现手续费 */
feeAmount?: number
/** 收款人 */
receiver?: string
/** 收款方手机号 */
receiverMobile?: string
/** 提现备注 */
reason?: string
/** 申请资料 */
applyDataUrl?: string
/** 对公账户名称 */
companyName?: string
/** 对公账户号码 */
companyAccount?: string
/** 开户银行 */
bankName?: string
/** 开户行支行编码 */
bankBranchNo?: string
/** 状态 */
status?: string
/** 外部状态 */
outStatus?: string
}
/**
* 提现配置
*/
export interface AgentCashoutsConfig extends MchEntity {
/** 起提金额 */
startAmount?: number
/** 单笔限额 */
singleLimit?: number
/** 每日限额 */
dailyLimit?: number
/** 费率 */
feeRate?: number
/** 固定费用 */
fixedFee?: number
/** 到账金额 */
arriveAmount?: number
/** 提现手续费 */
feeAmount?: number
}

View File

@@ -0,0 +1,429 @@
<template>
<div class="cashouts-apply">
<van-overlay v-show="loading" :show="true">
<div class="loading-wrapper" @click.stop>
<van-loading size="24px">
加载中...
</van-loading>
</div>
</van-overlay>
<div class="form-content">
<div class="form-box">
<van-form ref="formRef" required="auto" colon>
<!-- 基本信息 -->
<div class="info-card">
<div class="card-header">
<div class="card-title">
基本信息
</div>
</div>
<van-cell-group inset>
<!-- 申请名称 -->
<van-field
v-model="form.name"
label-align="top"
name="name"
placeholder="请输入申请名称"
label="申请名称"
clearable
:rules="[{ required: true, message: '请输入申请名称' }]"
/>
<!-- 提现方式 -->
<van-field name="type" label="提现方式" label-align="top" :rules="[{ required: true, message: '请选择提现方式' }]">
<template #input>
<van-radio-group v-model="form.type" direction="horizontal">
<van-radio name="alipay">
支付宝
</van-radio>
<van-radio name="wechat">
微信
</van-radio>
<van-radio name="bank">
银行打款
</van-radio>
</van-radio-group>
</template>
</van-field>
<!-- 提现金额 -->
<van-field
v-model="form.amount"
label-align="top"
name="amount"
:min="cashoutsConfig.startAmount || 0"
:max="wallet.availableBalance || 0"
:placeholder="`请输入要提现的金额,最小${cashoutsConfig.startAmount || 0}元,最大${wallet.availableBalance || 0}元`"
label="提现金额"
type="digit"
clearable
:rules="[
{ required: true, message: '请输入提现金额' },
]"
@blur="calcFee"
/>
<!-- 到账金额 -->
<van-field
:model-value="arriveAmountText"
label-align="top"
name="arriveAmount"
label="到账金额"
readonly
input-align="right"
/>
<!-- 提现手续费 -->
<van-field
:model-value="feeAmountText"
label-align="top"
name="feeAmount"
label="提现手续费"
readonly
input-align="right"
/>
</van-cell-group>
</div>
<!-- 收款信息 -->
<div class="info-card">
<div class="card-header">
<div class="card-title">
收款信息
</div>
</div>
<van-cell-group inset>
<!-- 收款人 -->
<van-field
v-model="form.receiver"
label-align="top"
name="receiver"
placeholder="请输入收款人"
label="收款人"
clearable
:rules="[{ required: true, message: '请输入收款人' }]"
/>
<!-- 收款方手机号 -->
<van-field
v-model="form.receiverMobile"
label-align="top"
name="receiverMobile"
placeholder="请输入收款方手机号"
label="收款方手机号"
type="tel"
clearable
:rules="[
{ required: true, message: '请输入收款方手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', required: true },
]"
/>
<!-- 提现备注 -->
<van-field
v-model="form.reason"
autosize
label-align="top"
name="reason"
placeholder="请输入提现备注"
label="提现备注"
type="textarea"
clearable
:rules="[{ required: true, message: '请输入提现备注' }]"
/>
<!-- 申请资料 -->
<van-field name="applyDataUrl" label="申请资料" label-align="top" :rules="[{ required: true, message: '请上传申请资料' }]">
<template #input>
<BUpload
v-model:pic-url="form.applyDataUrl"
/>
</template>
</van-field>
</van-cell-group>
</div>
<!-- 账户信息 -->
<div class="info-card">
<div class="card-header">
<div class="card-title">
账户信息
</div>
</div>
<van-cell-group inset>
<template v-if="form.type === 'bank'">
<!-- 对公账户名称 -->
<van-field
v-model="form.companyName"
label-align="top"
name="companyName"
placeholder="请输入对公账户名称"
label="对公账户名称"
clearable
:rules="[{ required: true, message: '请输入对公账户名称' }]"
/>
<!-- 对公账户号码 -->
<van-field
v-model="form.companyAccount"
label-align="top"
name="companyAccount"
placeholder="请输入对公账户号码"
label="对公账户号码"
clearable
:rules="[{ required: true, message: '请输入对公账户号码' }]"
/>
<!-- 开户银行 -->
<van-field
v-model="form.bankName"
label-align="top"
name="bankName"
placeholder="请输入开户银行名称"
label="开户银行"
clearable
:rules="[{ required: true, message: '请输入开户银行名称' }]"
/>
<!-- 开户行支行编码 -->
<van-field
v-model="form.bankBranchNo"
label-align="top"
name="bankBranchNo"
placeholder="请输入开户行支行编码"
label="开户行支行编码"
clearable
:rules="[{ required: true, message: '请输入开户行支行编码' }]"
/>
</template>
<template v-else>
<!-- 支付宝/微信账号 -->
<van-field
v-model="form.companyAccount"
label-align="top"
name="companyAccount"
placeholder="请输入支付宝/微信账号"
label="支付宝/微信账号"
clearable
:rules="[{ required: true, message: '请输入支付宝/微信账号' }]"
/>
</template>
</van-cell-group>
</div>
</van-form>
</div>
<!-- 底部按钮 -->
<div class="bottom-btn">
<van-button type="primary" block :loading="loading" @click="handleSubmit">
提交申请
</van-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import { showConfirmDialog, showNotify } from 'vant'
import { useRoute } from 'vue-router'
import { calcCashoutsFee, createApply, getConfig, getWallet } from './Cashouts.api'
import type { AgentCashouts, AgentCashoutsConfig, AgentWallet } from './Cashouts.api'
import { useTokenStore } from '@/store/modules/token'
// 获取路由参数
const route = useRoute()
// 请求头信息
const { setToken, setClientCode } = useTokenStore()
// 从URL参数中获取token
const token = route.query.token as string
if (token) {
setToken(token)
}
setClientCode('dax-pay-agent')
// 表单引用
const formRef = ref()
// 加载状态
const loading = ref(false)
// 钱包数据
const wallet = ref<AgentWallet>({
balance: 0,
availableBalance: 0,
freezeBalance: 0,
withdrawBalance: 0,
onWayBalance: 0,
})
// 提现配置
const cashoutsConfig = ref<AgentCashoutsConfig>({
startAmount: 0,
singleLimit: 0,
dailyLimit: 0,
feeRate: 0,
fixedFee: 0,
})
// 表单数据
const form = ref<AgentCashouts>({
type: 'alipay',
arriveAmount: 0,
feeAmount: 0,
})
// 计算属性,确保金额字段能正确显示
const arriveAmountText = computed(() => {
return (form.value.arriveAmount !== undefined && form.value.arriveAmount !== null) ? `${form.value.arriveAmount}` : '0 元'
})
const feeAmountText = computed(() => {
return (form.value.feeAmount !== undefined && form.value.feeAmount !== null) ? `${form.value.feeAmount}` : '0 元'
})
/**
* 初始化数据
*/
async function initData() {
loading.value = true
try {
// 获取钱包数据
const { data: walletData } = await getWallet()
wallet.value = walletData
// 获取提现配置
const { data: configData } = await getConfig()
cashoutsConfig.value = configData
// 初始化金额字段
form.value.amount = 0
form.value.feeAmount = 0
form.value.arriveAmount = 0
}
catch (error) {
console.error('数据加载失败:', error)
showNotify({ type: 'danger', message: '数据加载失败' })
}
finally {
loading.value = false
}
}
/**
* 计算费用和费率
*/
async function calcFee() {
if (form.value.amount) {
try {
const { data } = await calcCashoutsFee(form.value.amount)
form.value.feeAmount = data.feeAmount
form.value.arriveAmount = data.arriveAmount
}
catch (error) {
console.error('计算费用失败:', error)
form.value.feeAmount = 0
form.value.arriveAmount = 0
}
}
else {
form.value.feeAmount = 0
form.value.arriveAmount = 0
}
}
/**
* 提交表单
*/
async function handleSubmit() {
try {
// 表单验证
await formRef.value?.validate()
// 确认弹窗
await showConfirmDialog({
title: '提示',
message: '是否创建提现申请!',
})
loading.value = true
// 保存提现申请
const { code, msg } = await createApply(form.value)
if (code !== 0) {
showNotify({ type: 'danger', message: msg })
return
}
uni.navigateBack()
}
finally {
loading.value = false
}
}
// 页面加载时初始化数据
onMounted(() => {
initData()
})
</script>
<style lang="less" scoped>
.cashouts-apply {
width: 100%;
height: 100%;
padding-bottom: env(safe-area-inset-bottom);
}
.loading-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff;
}
.form-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.form-box {
width: 100%;
flex: 1;
overflow-y: scroll;
}
.info-card {
margin: 12px 8px;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
align-items: center;
height: 34px;
padding: 0 14px;
background-color: #fff4c5;
}
.card-title {
font-size: 14px;
color: #5c4b37;
}
.bottom-btn {
width: 100%;
padding: 16px;
background: #fff;
border-top: 1px solid #ebedf0;
}
:deep(.van-cell-group--inset) {
margin: 0;
}
</style>

View File

@@ -647,11 +647,11 @@ onMounted(() => {
}
.form-content {
/* 主体内容滚动 */
flex: 1;
overflow-y: scroll;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 20px 0;
padding-bottom: 80px; /* 为底部按钮留出空间 */
padding-bottom: 12px;
}
.loading-wrapper {
@@ -666,44 +666,26 @@ onMounted(() => {
/* 卡片样式 */
.status-card,
.info-card {
margin-bottom: 16px;
background: #fff;
border-radius: 0;
margin: 12px 8px;
border-radius: 8px;
overflow: hidden;
box-shadow: none;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.card-header {
height: 50px;
width: 100%;
display: flex;
align-items: center;
padding: 0 20px;
background-color: #f7f7f7;
color: #448ef7;
letter-spacing: 1px;
position: relative;
font-weight: 600;
border-bottom: none;
&::before {
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
content: '';
width: 5px;
height: 20px;
background-color: #448ef7;
}
height: 34px;
padding: 0 14px;
background-color: #fff4c5;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #448ef7;
font-weight: 500;
color: #5c4b37;
line-height: 1.2;
}
/* 认证状态样式优化 */
@@ -752,10 +734,6 @@ onMounted(() => {
}
.bottom-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 15px 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);

View File

@@ -314,14 +314,16 @@ onMounted(() => {
.card-header {
display: flex;
align-items: center;
height: 40px;
height: 34px;
padding: 0 14px;
background-color: #ecf5ff;
background-color: #fff4c5;
}
.card-title {
font-size: 16px;
color: #409eff;
font-size: 14px;
font-weight: 500;
color: #5c4b37;
line-height: 1.2;
}
/* 让内嵌面板去掉默认边距,卡片白底由容器负责 */

View File

@@ -237,14 +237,14 @@ onMounted(() => {
.card-header {
display: flex;
align-items: center;
height: 40px;
height: 34px;
padding: 0 14px;
background-color: #ecf5ff;
background-color: #fff4c5;
}
.card-title {
font-size: 16px;
color: #409eff;
font-size: 14px;
color: #5c4b37;
}
.bottom-btn {