本次提交主要实现微信小程序虚拟支付提审防审机制,包括: 1. 后台配置虚拟支付防审开关,支持开启/关闭模式 2. 前端根据开关状态及设备类型,iOS设备自动触发合规提示 3. 分销模块调整提现规则,无门槛即时提现 4. 登录页面优化,增加返回和暂不登录入口,符合微信审核规范 5. 新增提现记录页面,优化分销中心界面布局 该功能确保微信提审期间能顺利通过审核,审核通过后可一键切换为正常运营模式,无需重新发布小程序代码。
633 行
14 KiB
Vue
633 行
14 KiB
Vue
<template>
|
||
<view class="records-page fc-page">
|
||
<!-- 顶部沉浸式导航栏 -->
|
||
<fc-navbar title="提现记录" theme="gradient" :show-back="true"></fc-navbar>
|
||
|
||
<!-- 顶部状态分类轻筛选栏 -->
|
||
<view class="records__filter-bar">
|
||
<view
|
||
v-for="tab in filterTabs"
|
||
:key="tab.value"
|
||
class="records__filter-item"
|
||
:class="{ 'records__filter-item--active': activeStatus === tab.value }"
|
||
@click="switchStatus(tab.value)"
|
||
>
|
||
<text class="records__filter-text">{{ tab.label }}</text>
|
||
<text v-if="tab.count > 0" class="records__filter-badge">{{ tab.count }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 提现列表主体 -->
|
||
<view class="records__body">
|
||
<!-- 骨架屏加载状态 -->
|
||
<view v-if="loading" class="records__skeleton">
|
||
<view v-for="i in 4" :key="'skel-' + i" class="records__skeleton-card">
|
||
<view class="records__skeleton-left">
|
||
<view class="records__skeleton-line fc-skeleton"></view>
|
||
<view class="records__skeleton-line records__skeleton-line--short fc-skeleton"></view>
|
||
</view>
|
||
<view class="records__skeleton-right fc-skeleton"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 纯净通透空状态(去白色卡片束缚,自然居中透气) -->
|
||
<view v-else-if="!filteredRecords.length" class="records__empty">
|
||
<view class="records__empty-icon-wrap">
|
||
<image class="records__empty-icon" :src="iconUrl('receipt')" mode="aspectFit" />
|
||
</view>
|
||
<text class="records__empty-title">{{ activeStatus === 'all' ? '暂无提现记录' : '暂无此分类记录' }}</text>
|
||
<text class="records__empty-sub">推广赚取佣金后,可随时发起微信零钱提现</text>
|
||
</view>
|
||
|
||
<!-- 记录列表卡片 -->
|
||
<view v-else class="records__list">
|
||
<view
|
||
v-for="item in filteredRecords"
|
||
:key="item.id"
|
||
class="records__card"
|
||
@click="goDetail(item)"
|
||
>
|
||
<view class="records__card-top">
|
||
<view class="records__card-type">
|
||
<image class="records__type-icon" :src="iconUrl('wallet')" mode="aspectFit" />
|
||
<text class="records__type-title">提现到微信零钱</text>
|
||
</view>
|
||
<!-- 状态标签 -->
|
||
<view class="records__badge" :class="'records__badge--' + item.status">
|
||
<text class="records__badge-text">{{ statusText(item.status) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="records__card-middle">
|
||
<view class="records__card-meta">
|
||
<text class="records__meta-time">{{ formatTimeText(item.createdAt) }}</text>
|
||
<text class="records__meta-no fc-ellipsis">单号 {{ item.withdrawNo }}</text>
|
||
</view>
|
||
<view class="records__card-amount">
|
||
<text class="records__amount-symbol">-¥</text>
|
||
<text class="records__amount-num">{{ formatAmount(item.amount) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 失败原因提示栏 -->
|
||
<view v-if="item.status === 'failed' && item.failReason" class="records__fail-row">
|
||
<text class="records__fail-label">驳回/失败说明:</text>
|
||
<text class="records__fail-text">{{ item.failReason }}</text>
|
||
</view>
|
||
|
||
<!-- 底部操作/去确认收款条 -->
|
||
<view class="records__card-footer">
|
||
<view v-if="item.status === 'paying'" class="records__action-wrap">
|
||
<button
|
||
v-if="item.transferStatus === 'WAIT_USER_CONFIRM' && item.packageInfo"
|
||
class="records__confirm-btn"
|
||
@click.stop="confirmTransfer(item)"
|
||
>
|
||
<text>去微信确认收款 ›</text>
|
||
</button>
|
||
<text v-else class="records__wait-text">微信商户打款中,预计稍后到账</text>
|
||
</view>
|
||
<view v-else class="records__detail-link">
|
||
<text>查看流转详情</text>
|
||
<text class="records__link-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部安全间距与完成提示 -->
|
||
<view class="records__list-footer">
|
||
<text class="records__list-footer-text">仅展示近期提现流水记录</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* 提现记录列表页面
|
||
* 纯粹轻量财务明细页:状态筛选、流转追溯与微信商家转账收款唤起
|
||
*/
|
||
import distributionApi from '@/common/api/distribution.js'
|
||
import { iconUrl } from '@/common/store/ui.js'
|
||
import { formatTime } from '@/common/utils/format.js'
|
||
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
|
||
|
||
export default {
|
||
name: 'WithdrawRecordsPage',
|
||
components: { FcNavbar },
|
||
data() {
|
||
return {
|
||
loading: true,
|
||
records: [],
|
||
overview: null,
|
||
activeStatus: 'all',
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 状态分类选项列表及对应条数
|
||
* @returns {Array}
|
||
*/
|
||
filterTabs() {
|
||
const counts = {
|
||
all: this.records.length,
|
||
paid: 0,
|
||
paying: 0,
|
||
failed: 0,
|
||
}
|
||
for (const r of this.records) {
|
||
if (r.status === 'paid') counts.paid++
|
||
else if (r.status === 'paying' || r.status === 'auditing') counts.paying++
|
||
else if (r.status === 'failed' || r.status === 'rejected') counts.failed++
|
||
}
|
||
return [
|
||
{ label: '全部', value: 'all', count: 0 },
|
||
{ label: '已到账', value: 'paid', count: counts.paid },
|
||
{ label: '处理中', value: 'paying', count: counts.paying },
|
||
{ label: '未成功', value: 'failed', count: counts.failed },
|
||
]
|
||
},
|
||
/**
|
||
* 筛选后的提现流水列表
|
||
* @returns {Array}
|
||
*/
|
||
filteredRecords() {
|
||
if (this.activeStatus === 'all') return this.records
|
||
if (this.activeStatus === 'paid') {
|
||
return this.records.filter((r) => r.status === 'paid')
|
||
}
|
||
if (this.activeStatus === 'paying') {
|
||
return this.records.filter((r) => r.status === 'paying' || r.status === 'auditing')
|
||
}
|
||
if (this.activeStatus === 'failed') {
|
||
return this.records.filter((r) => r.status === 'failed' || r.status === 'rejected')
|
||
}
|
||
return this.records
|
||
},
|
||
},
|
||
/**
|
||
* 页面加载初始化
|
||
*/
|
||
onLoad() {
|
||
this.fetchData()
|
||
},
|
||
/**
|
||
* 下拉刷新列表
|
||
*/
|
||
async onPullDownRefresh() {
|
||
await this.fetchData()
|
||
uni.stopPullDownRefresh()
|
||
},
|
||
methods: {
|
||
iconUrl,
|
||
/**
|
||
* 切换状态筛选
|
||
* @param {string} val 状态值
|
||
*/
|
||
switchStatus(val) {
|
||
this.activeStatus = val
|
||
},
|
||
/**
|
||
* 格式化金额
|
||
* @param {number|string} val 数值
|
||
* @returns {string} 格式化两位小数
|
||
*/
|
||
formatAmount(val) {
|
||
const n = Number(val)
|
||
return isNaN(n) ? '0.00' : n.toFixed(2)
|
||
},
|
||
/**
|
||
* 格式化时间字符串
|
||
* @param {string} dateStr 时间串
|
||
* @returns {string}
|
||
*/
|
||
formatTimeText(dateStr) {
|
||
if (!dateStr) return '--'
|
||
return formatTime(dateStr)
|
||
},
|
||
/**
|
||
* 提现状态文本映射
|
||
* @param {string} status 状态码
|
||
* @returns {string} 语义化文本
|
||
*/
|
||
statusText(status) {
|
||
const map = {
|
||
auditing: '审核中',
|
||
paying: '打款处理中',
|
||
paid: '提现到账',
|
||
rejected: '审核驳回',
|
||
failed: '转账失败',
|
||
}
|
||
return map[status] || '处理中'
|
||
},
|
||
/**
|
||
* 加载提现记录
|
||
*/
|
||
async fetchData() {
|
||
this.loading = true
|
||
try {
|
||
const [recordsRes, overviewRes] = await Promise.all([
|
||
distributionApi.withdrawRecords(),
|
||
distributionApi.overview().catch(() => null),
|
||
])
|
||
this.records = Array.isArray(recordsRes)
|
||
? recordsRes
|
||
: (recordsRes && recordsRes.list) || []
|
||
if (overviewRes) {
|
||
this.overview = overviewRes
|
||
}
|
||
} catch (e) {
|
||
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
/**
|
||
* 跳转提现单详情页
|
||
* @param {Object} item 提现单据对象
|
||
*/
|
||
goDetail(item) {
|
||
if (!item || !item.id) return
|
||
uni.navigateTo({
|
||
url: `/pages/distribution/withdraw-detail?id=${item.id}&withdrawNo=${encodeURIComponent(item.withdrawNo || '')}`,
|
||
})
|
||
},
|
||
/**
|
||
* 唤起微信商家转账确认收款页(小程序原生组件)
|
||
* @param {Object} item 提现单据对象
|
||
*/
|
||
confirmTransfer(item) {
|
||
if (!item || !item.packageInfo) {
|
||
this.goDetail(item)
|
||
return
|
||
}
|
||
// #ifdef MP-WEIXIN
|
||
if (typeof wx !== 'undefined' && typeof wx.openBusinessView === 'function') {
|
||
wx.openBusinessView({
|
||
businessType: 'confirmTransfer',
|
||
extraData: {
|
||
package_info: item.packageInfo,
|
||
mch_id: item.transferMchId || (this.overview && this.overview.transferMchId) || '',
|
||
appid: item.transferAppId || (this.overview && this.overview.transferAppId) || '',
|
||
},
|
||
success: (res) => {
|
||
if (res && res.extraData && res.extraData.status === 'SUCCESS') {
|
||
uni.showToast({ title: '已确认收款', icon: 'success' })
|
||
this.fetchData()
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.goDetail(item)
|
||
},
|
||
})
|
||
return
|
||
}
|
||
// #endif
|
||
this.goDetail(item)
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.records-page {
|
||
min-height: 100vh;
|
||
background: #f2fef7;
|
||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 顶部状态筛选分类栏 */
|
||
.records__filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20rpx 32rpx 12rpx;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.records__filter-item {
|
||
padding: 12rpx 28rpx;
|
||
border-radius: 32rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(226, 238, 230, 0.85);
|
||
display: flex;
|
||
align-items: center;
|
||
transition: all 0.2s ease;
|
||
|
||
&--active {
|
||
background: #00d26a;
|
||
border-color: #00d26a;
|
||
|
||
.records__filter-text {
|
||
color: #04301e;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.records__filter-badge {
|
||
background: #04301e;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.records__filter-text {
|
||
font-size: 26rpx;
|
||
color: #64748b;
|
||
line-height: 1;
|
||
}
|
||
|
||
.records__filter-badge {
|
||
font-size: 20rpx;
|
||
background: #f1f5f9;
|
||
color: #64748b;
|
||
border-radius: 16rpx;
|
||
padding: 2rpx 10rpx;
|
||
margin-left: 8rpx;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
/* 列表主体 */
|
||
.records__body {
|
||
padding: 12rpx 32rpx;
|
||
}
|
||
|
||
/* 骨架屏 */
|
||
.records__skeleton {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.records__skeleton-card {
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.records__skeleton-left {
|
||
width: 60%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.records__skeleton-line {
|
||
height: 28rpx;
|
||
border-radius: 8rpx;
|
||
|
||
&--short {
|
||
width: 65%;
|
||
height: 22rpx;
|
||
}
|
||
}
|
||
|
||
.records__skeleton-right {
|
||
width: 140rpx;
|
||
height: 48rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
/* 纯净通透空状态 */
|
||
.records__empty {
|
||
padding: 140rpx 40rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.records__empty-icon-wrap {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 50%;
|
||
background: rgba(0, 210, 106, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.records__empty-icon {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.records__empty-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.records__empty-sub {
|
||
font-size: 24rpx;
|
||
color: #64748b;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 单条提现卡片 */
|
||
.records__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.records__card {
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
padding: 28rpx 32rpx 24rpx;
|
||
border: 1rpx solid rgba(226, 238, 230, 0.9);
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: all 0.2s ease;
|
||
|
||
&:active {
|
||
background: #f9fbf9;
|
||
}
|
||
}
|
||
|
||
.records__card-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-bottom: 20rpx;
|
||
border-bottom: 1rpx solid #f1f6f3;
|
||
}
|
||
|
||
.records__card-type {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.records__type-icon {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.records__type-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
}
|
||
|
||
/* 状态药丸标签 */
|
||
.records__badge {
|
||
padding: 6rpx 18rpx;
|
||
border-radius: 20rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
|
||
&--paid {
|
||
background: #e8f9f0;
|
||
color: #00a854;
|
||
}
|
||
|
||
&--paying {
|
||
background: #eff6ff;
|
||
color: #2563eb;
|
||
}
|
||
|
||
&--auditing {
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
}
|
||
|
||
&--rejected,
|
||
&--failed {
|
||
background: #fef2f2;
|
||
color: #dc2626;
|
||
}
|
||
}
|
||
|
||
.records__badge-text {
|
||
line-height: 1;
|
||
}
|
||
|
||
/* 中部数值与单号 */
|
||
.records__card-middle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 0 20rpx;
|
||
}
|
||
|
||
.records__card-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.records__meta-time {
|
||
font-size: 24rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
.records__meta-no {
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
max-width: 360rpx;
|
||
}
|
||
|
||
.records__card-amount {
|
||
display: flex;
|
||
align-items: baseline;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.records__amount-symbol {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
margin-right: 2rpx;
|
||
}
|
||
|
||
.records__amount-num {
|
||
font-size: 42rpx;
|
||
font-weight: 800;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
/* 驳回与失败行 */
|
||
.records__fail-row {
|
||
background: #fef2f2;
|
||
border-radius: 12rpx;
|
||
padding: 12rpx 18rpx;
|
||
margin-bottom: 16rpx;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.records__fail-label {
|
||
font-size: 22rpx;
|
||
color: #dc2626;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.records__fail-text {
|
||
font-size: 22rpx;
|
||
color: #dc2626;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 底部操作行 */
|
||
.records__card-footer {
|
||
padding-top: 16rpx;
|
||
border-top: 1rpx dashed #edf3f0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.records__detail-link {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 24rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
.records__link-arrow {
|
||
font-size: 28rpx;
|
||
margin-left: 6rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.records__confirm-btn {
|
||
background: #00d26a;
|
||
color: #04301e;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
border-radius: 24rpx;
|
||
padding: 8rpx 24rpx;
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
border: none;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.records__wait-text {
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
/* 列表底部 */
|
||
.records__list-footer {
|
||
text-align: center;
|
||
padding: 30rpx 0 10rpx;
|
||
}
|
||
|
||
.records__list-footer-text {
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
}
|
||
</style>
|