文件
fenleixinxi/uniapp/pages/distribution/index.vue

2506 行
66 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="dist fc-page">
<!-- 顶部导航:自适应状态栏与胶囊,右上角增加分销规则入口 -->
<fc-navbar title="分销中心" theme="gradient" :show-back="true">
<template #right>
<view class="dist__nav-rule" @click="showRuleModal = true">
<image class="dist__nav-rule-icon" :src="iconUrl('info')" mode="aspectFit" />
<text class="dist__nav-rule-text">规则</text>
</view>
</template>
</fc-navbar>
<!-- 佣金 Hero -->
<view class="dist__hero">
<view class="dist__hero-copy">
<text class="dist__hero-title">分享好内容,天天赚佣金</text>
<text class="dist__hero-sub">好友消费自动返佣 · {{ overview.settleCycle || 'T+7' }} 自动结算提现</text>
</view>
</view>
<!-- 佣金钱包(悬浮卡片) -->
<view class="dist__wallet">
<view class="dist__wallet-head">
<view class="dist__wallet-label-wrap">
<image class="dist__wallet-icon" :src="iconUrl('wallet')" mode="aspectFit" />
<text class="dist__wallet-label">可提现佣金</text>
</view>
<view class="dist__withdraw-btn" @click="openWithdraw">
<text>提现</text>
</view>
</view>
<view class="dist__wallet-amount">
<text class="dist__wallet-symbol">¥</text>
<text class="dist__wallet-num">{{ money(overview.withdrawable) }}</text>
</view>
<view class="dist__wallet-stats">
<view class="dist__wallet-stat">
<text class="dist__wallet-stat-num">{{ money(overview.totalCommission) }}</text>
<text class="dist__wallet-stat-label">累计佣金</text>
</view>
<view class="dist__wallet-stat">
<text class="dist__wallet-stat-num">{{ money(overview.pendingCommission) }}</text>
<text class="dist__wallet-stat-label">待结算</text>
</view>
<view class="dist__wallet-stat">
<text class="dist__wallet-stat-num">{{ money(overview.settled) }}</text>
<text class="dist__wallet-stat-label">已结算</text>
</view>
<view class="dist__wallet-stat">
<text class="dist__wallet-stat-num">{{ money(overview.todayCommission) }}</text>
<text class="dist__wallet-stat-label">今日收益</text>
</view>
</view>
</view>
<!-- 数据加载失败提示 -->
<view v-if="loadError" class="dist__notice" @click="init">
<image class="dist__notice-icon" :src="iconUrl('info')" mode="aspectFit" />
<text class="dist__notice-text">数据加载失败,点击重试</text>
</view>
<!-- 邀请好友推广卡片 -->
<view class="dist__card">
<view class="dist__card-head">
<view class="dist__card-title">
<view class="dist__card-icon-wrap">
<image class="dist__card-icon" :src="iconUrl('link-dist')" mode="aspectFit" />
</view>
<text>邀请好友推广</text>
</view>
<text class="dist__card-extra">已邀请 {{ overview.teamCount || 0 }} 人</text>
</view>
<!-- 邀请码展示与复制 -->
<view class="dist__invite-box">
<view class="dist__invite-code">
<text class="dist__invite-label">我的专属邀请码</text>
<text class="dist__invite-value">{{ overview.inviteCode || '—' }}</text>
</view>
<view class="dist__invite-copy" @click="copyCode">
<text>复制</text>
</view>
</view>
<!-- 核心分享动作区(支持:原生分享给好友、生成专属海报、复制链接) -->
<view class="dist__invite-actions">
<!-- #ifdef MP-WEIXIN -->
<!-- 微信小程序端:button open-type="share" 直接唤起微信好友选择列表 -->
<button class="dist__btn dist__btn--primary" open-type="share">
<view class="dist__btn-icon-circle">
<image class="dist__btn-icon" :src="iconUrl('share')" mode="aspectFit" />
</view>
<text>发给好友</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<!-- H5/App 端:调用系统分享或复制邀请链接 -->
<view class="dist__btn dist__btn--primary" @click="shareInvite">
<view class="dist__btn-icon-circle">
<image class="dist__btn-icon" :src="iconUrl('share')" mode="aspectFit" />
</view>
<text>分享好友</text>
</view>
<!-- #endif -->
<!-- 专属海报按钮:点击唤起 Canvas 高保真海报绘制与保存相册 -->
<view class="dist__btn dist__btn--secondary" @click="openPosterModal">
<image class="dist__btn-icon dist__btn-icon--plain" :src="iconUrl('bar-poster')" mode="aspectFit" />
<text>专属海报</text>
</view>
<!-- 复制链接按钮 -->
<view class="dist__btn dist__btn--ghost" @click="copyLink">
<image class="dist__btn-icon dist__btn-icon--plain" :src="iconUrl('network')" mode="aspectFit" />
<text>复制链接</text>
</view>
</view>
</view>
<!-- 业务板块导航切换:我的团队 / 收益明细 / 提现记录 -->
<view class="dist__tabs-bar">
<view
v-for="tab in mainTabs"
:key="tab.key"
class="dist__tab-item"
:class="{ 'dist__tab-item--active': activeTab === tab.key }"
@click="switchTab(tab.key)"
>
<text class="dist__tab-text">{{ tab.name }}</text>
<view v-if="activeTab === tab.key" class="dist__tab-line"></view>
</view>
</view>
<!-- ==================== Tab 1: 我的团队 ==================== -->
<view v-if="activeTab === 'team'" class="dist__card">
<view class="dist__card-head">
<view class="dist__card-title">
<view class="dist__card-icon-wrap">
<image class="dist__card-icon" :src="iconUrl('users')" mode="aspectFit" />
</view>
<text>团队成员</text>
</view>
<text class="dist__card-extra">共 {{ overview.teamCount || 0 }} 人</text>
</view>
<!-- 层级分类 Tab -->
<view class="dist__seg">
<view
v-for="t in levelTabs"
:key="t.value"
class="dist__seg-item"
:class="{ 'dist__seg-item--active': level === t.value }"
@click="changeLevel(t.value)"
>
<text>{{ t.label }}</text>
</view>
</view>
<view class="dist__team-summary">
<text>一级直推 {{ overview.l1Count || 0 }} 人(返佣 {{ overview.level1Rate || 20 }}%)</text>
<text class="dist__team-dot">·</text>
<text>二级间推 {{ overview.l2Count || 0 }} 人(返佣 {{ overview.level2Rate || 10 }}%)</text>
</view>
<!-- 团队骨架屏 -->
<view v-if="teamLoading" class="dist__skeleton">
<view v-for="i in 3" :key="i" class="dist__skeleton-row">
<view class="dist__skeleton-avatar fc-skeleton"></view>
<view class="dist__skeleton-lines">
<view class="dist__skeleton-line fc-skeleton"></view>
<view class="dist__skeleton-line dist__skeleton-line--short fc-skeleton"></view>
</view>
</view>
</view>
<!-- 团队为空 -->
<view v-else-if="!team.length" class="dist__empty">
<view class="dist__empty-icon-wrap">
<image class="dist__empty-icon" :src="iconUrl('users')" mode="aspectFit" />
</view>
<text class="dist__empty-text">还没有团队成员,立即邀请好友加入</text>
<view class="dist__empty-btn" @click="openPosterModal">
<text>生成邀请海报</text>
</view>
</view>
<!-- 团队成员列表 -->
<view v-else class="dist__team-list">
<view v-for="t in visibleTeam" :key="t.id + '-' + t.level" class="dist__member">
<view class="dist__member-avatar-wrap">
<image
v-if="t.avatar"
class="dist__member-avatar-img"
:src="assetUrl(t.avatar)"
mode="aspectFill"
/>
<view v-else class="dist__member-avatar">
<text>{{ firstChar(t.nickname) }}</text>
</view>
</view>
<view class="dist__member-main">
<view class="dist__member-top">
<text class="dist__member-name">{{ t.nickname || '同城合伙人' }}</text>
<text class="dist__badge" :class="t.level === 1 ? 'dist__badge--l1' : 'dist__badge--l2'">
{{ t.level === 1 ? '一级直推' : '二级间推' }}
</text>
</view>
<text class="dist__member-meta">{{ joinText(t) }} · 累计消费 ¥{{ money(t.performance) }}</text>
</view>
<view class="dist__member-income">
<text class="dist__member-income-num">+¥{{ money(t.commission) }}</text>
<text class="dist__member-income-label">贡献佣金</text>
</view>
</view>
<!-- 展开收起 -->
<view v-if="team.length > 5" class="dist__more" @click="teamExpanded = !teamExpanded">
<text>{{ teamExpanded ? '收起' : '展开全部 ' + team.length + ' 位成员' }}</text>
<view class="dist__chevron" :class="{ 'dist__chevron--up': teamExpanded }"></view>
</view>
</view>
</view>
<!-- ==================== Tab 2: 收益明细(佣金流水账单) ==================== -->
<view v-if="activeTab === 'commissions'" class="dist__card">
<view class="dist__card-head">
<view class="dist__card-title">
<view class="dist__card-icon-wrap">
<image class="dist__card-icon" :src="iconUrl('home-crown')" mode="aspectFit" />
</view>
<text>佣金账单</text>
</view>
<text v-if="commissionsList.length" class="dist__card-extra">共 {{ commissionsList.length }} 笔</text>
</view>
<!-- 状态筛选 -->
<view class="dist__seg">
<view
v-for="s in commissionStatusTabs"
:key="s.value"
class="dist__seg-item"
:class="{ 'dist__seg-item--active': commissionStatus === s.value }"
@click="changeCommissionStatus(s.value)"
>
<text>{{ s.label }}</text>
</view>
</view>
<!-- 收益明细骨架屏 -->
<view v-if="commissionsLoading" class="dist__skeleton">
<view v-for="i in 3" :key="'comm-' + i" class="dist__skeleton-record">
<view class="dist__skeleton-lines">
<view class="dist__skeleton-line fc-skeleton"></view>
<view class="dist__skeleton-line dist__skeleton-line--short fc-skeleton"></view>
</view>
<view class="dist__skeleton-amount fc-skeleton"></view>
</view>
</view>
<!-- 收益明细为空 -->
<view v-else-if="!commissionsList.length" class="dist__empty">
<view class="dist__empty-icon-wrap">
<image class="dist__empty-icon" :src="iconUrl('receipt')" mode="aspectFit" />
</view>
<text class="dist__empty-text">暂无收益记录,好友消费后自动入账</text>
</view>
<!-- 收益明细列表 -->
<view v-else class="dist__record-list">
<view v-for="c in visibleCommissions" :key="c.id" class="dist__record">
<view class="dist__record-main">
<view class="dist__record-top">
<text class="dist__record-title">{{ c.orderTypeName || '同城消费' }}</text>
<text
class="dist__record-status"
:class="c.status === 1 ? 'dist__record-status--paid' : (c.status === 2 ? 'dist__record-status--failed' : 'dist__record-status--auditing')"
>
{{ c.statusText || (c.status === 1 ? '已结算' : (c.status === 2 ? '已失效' : '待结算')) }}
</text>
</view>
<text class="dist__record-time">{{ timeText(c.createdAt) }} · {{ c.level === 1 ? '一级好友' : '二级好友' }}:{{ (c.fromUser && c.fromUser.nickname) || '同城好友' }}</text>
<text class="dist__record-no fc-ellipsis">消费 ¥{{ money(c.orderAmount) }} · 分佣比例 {{ c.rate }}% · 单号 {{ c.orderNo }}</text>
</view>
<view class="dist__record-right">
<text class="dist__record-amount dist__record-amount--income">+¥{{ money(c.amount) }}</text>
</view>
</view>
<!-- 展开收起 -->
<view v-if="commissionsList.length > 5" class="dist__more" @click="commissionsExpanded = !commissionsExpanded">
<text>{{ commissionsExpanded ? '收起' : '查看全部收益账单' }}</text>
<view class="dist__chevron" :class="{ 'dist__chevron--up': commissionsExpanded }"></view>
</view>
</view>
</view>
<!-- ==================== Tab 3: 提现记录 ==================== -->
<view v-if="activeTab === 'withdraw'" class="dist__card">
<view class="dist__card-head">
<view class="dist__card-title">
<view class="dist__card-icon-wrap">
<image class="dist__card-icon" :src="iconUrl('receipt')" mode="aspectFit" />
</view>
<text>提现记录</text>
</view>
<text v-if="records.length" class="dist__card-extra">共 {{ records.length }} 笔</text>
</view>
<view v-if="loading" class="dist__skeleton">
<view v-for="i in 3" :key="'record-' + i" class="dist__skeleton-record">
<view class="dist__skeleton-lines">
<view class="dist__skeleton-line fc-skeleton"></view>
<view class="dist__skeleton-line dist__skeleton-line--short fc-skeleton"></view>
</view>
<view class="dist__skeleton-amount fc-skeleton"></view>
</view>
</view>
<view v-else-if="!records.length" class="dist__empty">
<view class="dist__empty-icon-wrap">
<image class="dist__empty-icon" :src="iconUrl('receipt')" mode="aspectFit" />
</view>
<text class="dist__empty-text">暂无提现记录</text>
</view>
<view v-else class="dist__record-list">
<view v-for="w in visibleRecords" :key="w.id" class="dist__record">
<view class="dist__record-main">
<view class="dist__record-top">
<text class="dist__record-title">提现到微信零钱</text>
<text class="dist__record-status" :class="'dist__record-status--' + w.status">
{{ statusText(w.status) }}
</text>
</view>
<text class="dist__record-time">{{ timeText(w.createdAt) }}</text>
<text class="dist__record-no fc-ellipsis">单号 {{ w.withdrawNo }}</text>
<text v-if="w.status === 'failed' && w.failReason" class="dist__record-no fc-ellipsis">失败原因:{{ w.failReason }}</text>
</view>
<view class="dist__record-right">
<text class="dist__record-amount">-¥{{ money(w.amount) }}</text>
<view v-if="w.status === 'paying'" class="dist__record-action" @click="confirmTransfer(w)">
<text>{{ w.transferStatus === 'WAIT_USER_CONFIRM' ? '去确认收款' : '转账中' }}</text>
</view>
</view>
</view>
<view v-if="records.length > 5" class="dist__more" @click="recordsExpanded = !recordsExpanded">
<text>{{ recordsExpanded ? '收起' : '查看全部提现记录' }}</text>
<view class="dist__chevron" :class="{ 'dist__chevron--up': recordsExpanded }"></view>
</view>
</view>
</view>
<view class="dist__bottom-space"></view>
<!-- ==================== 提现申请弹层 ==================== -->
<view v-if="showWithdraw" class="dist__mask" @click="closeWithdraw" @touchmove.stop.prevent>
<view class="dist__sheet" @click.stop>
<view class="dist__sheet-handle"></view>
<view class="dist__sheet-head">
<text class="dist__sheet-title">申请提现</text>
<view class="dist__sheet-close" @click="closeWithdraw">
<image class="dist__sheet-close-img" :src="iconUrl('close')" mode="aspectFit" />
</view>
</view>
<view class="dist__sheet-balance">
<text class="dist__sheet-balance-label">可提现余额(元)</text>
<text class="dist__sheet-balance-num">{{ money(overview.withdrawable) }}</text>
</view>
<!-- 收款人真实姓名校验 -->
<view v-if="!realname" class="dist__sheet-field">
<input
v-model="realnameInput"
class="dist__sheet-input dist__sheet-input--name"
type="text"
maxlength="20"
placeholder="收款人真实姓名(需与微信实名一致)"
placeholder-class="dist__sheet-placeholder"
/>
</view>
<!-- 提现金额输入 -->
<view class="dist__sheet-field">
<text class="dist__sheet-symbol">¥</text>
<input
v-model="withdrawAmount"
class="dist__sheet-input"
type="digit"
maxlength="10"
placeholder="0.00"
placeholder-class="dist__sheet-placeholder"
/>
<text class="dist__sheet-all" @click="fillAll">全部</text>
</view>
<!-- 快捷金额选择 -->
<view v-if="quickAmounts.length" class="dist__sheet-chips">
<view
v-for="c in quickAmounts"
:key="c.value"
class="dist__chip"
:class="{ 'dist__chip--active': Number(withdrawAmount) === c.value }"
@click="withdrawAmount = String(c.value)"
>
<text>{{ c.label }}</text>
</view>
</view>
<view class="dist__sheet-tip-row">
<image class="dist__sheet-tip-icon" :src="iconUrl('info')" mode="aspectFit" />
<text class="dist__sheet-tip">最低提现 ¥{{ minWithdrawText }},预计 24 小时内直转微信零钱</text>
</view>
<view
class="dist__sheet-btn"
:class="{ 'dist__sheet-btn--disabled': !canSubmit }"
@click="submitWithdraw"
>
<text>{{ submitting ? '提交中…' : '确认提现' }}</text>
</view>
<view class="dist__sheet-safe"></view>
</view>
</view>
<!-- ==================== 分销规则说明弹层 ==================== -->
<view v-if="showRuleModal" class="dist__mask" @click="showRuleModal = false" @touchmove.stop.prevent>
<view class="dist__sheet" @click.stop>
<view class="dist__sheet-handle"></view>
<view class="dist__sheet-head">
<text class="dist__sheet-title">分销推广奖励规则</text>
<view class="dist__sheet-close" @click="showRuleModal = false">
<image class="dist__sheet-close-img" :src="iconUrl('close')" mode="aspectFit" />
</view>
</view>
<view class="dist__rule-content">
<view class="dist__rule-item">
<view class="dist__rule-badge">1</view>
<view class="dist__rule-text-wrap">
<text class="dist__rule-h">一级直推返佣:{{ overview.level1Rate || 20 }}%</text>
<text class="dist__rule-p">好友通过您的专属海报或链接注册,其在平台发布信息、置顶、开通会员或购买便民服务,您立享订单金额 {{ overview.level1Rate || 20 }}% 的现金佣金。</text>
</view>
</view>
<view class="dist__rule-item">
<view class="dist__rule-badge">2</view>
<view class="dist__rule-text-wrap">
<text class="dist__rule-h">二级间推返佣:{{ overview.level2Rate || 10 }}%</text>
<text class="dist__rule-p">您的一级好友再次邀请新朋友消费,您仍可获得订单金额 {{ overview.level2Rate || 10 }}% 的间接分销奖励,裂变无忧。</text>
</view>
</view>
<view class="dist__rule-item">
<view class="dist__rule-badge">3</view>
<view class="dist__rule-text-wrap">
<text class="dist__rule-h">结算周期与到账:{{ overview.settleCycle || 'T+7' }}</text>
<text class="dist__rule-p">订单支付完成后佣金即刻进入待结算,满 {{ overview.settleCycle || 'T+7' }} 天后自动转入可提现余额,随时提现至微信零钱。</text>
</view>
</view>
<view class="dist__rule-item">
<view class="dist__rule-badge">4</view>
<view class="dist__rule-text-wrap">
<text class="dist__rule-h">提现规则与到账保障</text>
<text class="dist__rule-p">最低提现金额为 ¥{{ minWithdrawText }},平台支持微信商家转账直接到账微信零钱,无任何手续费。</text>
</view>
</view>
</view>
<view class="dist__sheet-btn" @click="showRuleModal = false">
<text>我知道了</text>
</view>
<view class="dist__sheet-safe"></view>
</view>
</view>
<!-- ==================== 专属邀请海报弹层 ==================== -->
<view v-if="showPosterModal" class="dist__mask" @click="showPosterModal = false" @touchmove.stop.prevent>
<view class="dist__poster-sheet" @click.stop>
<view class="dist__poster-sheet-head">
<text class="dist__poster-sheet-title">专属合伙人海报</text>
<view class="dist__sheet-close" @click="showPosterModal = false">
<image class="dist__sheet-close-img" :src="iconUrl('close')" mode="aspectFit" />
</view>
</view>
<!-- 海报正在绘制态 -->
<view v-if="posterLoading" class="dist__poster-loading">
<view class="dist__poster-spinner"></view>
<text class="dist__poster-loading-text">正在生成专属推广海报…</text>
</view>
<!-- 海报预览大图 -->
<view v-else class="dist__poster-preview">
<image
v-if="posterImagePath"
class="dist__poster-img"
:src="posterImagePath"
mode="widthFix"
@click="previewPoster"
/>
<text class="dist__poster-tip">点击大图可全屏预览,长按图片直接转发</text>
</view>
<!-- 海报底部快捷操作按钮组 -->
<view class="dist__poster-actions">
<view class="dist__poster-act-btn dist__poster-act-btn--primary" @click="savePoster">
<image class="dist__btn-icon" :src="iconUrl('image-white')" mode="aspectFit" />
<text>保存到相册</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<button class="dist__poster-act-btn dist__poster-act-btn--share" @click="sharePosterImage">
<image class="dist__btn-icon" :src="iconUrl('share')" mode="aspectFit" />
<text>发给好友</text>
</button>
<!-- #endif -->
</view>
</view>
</view>
<!-- 离屏 Canvas(600 x 880 逻辑分辨率,导出 2x 超清海报) -->
<canvas
canvas-id="distPosterCanvas"
id="distPosterCanvas"
style="width: 600px; height: 740px; position: fixed; left: -9999px; top: -9999px; z-index: -999;"
/>
</view>
</template>
<script>
/**
* 分销中心模块(用户端)
* 包含:佣金资产看板、一二级团队管理、收益流水账单、微信商家转账提现、合伙人海报生成与多端分享裂变
*/
import distributionApi from '@/common/api/distribution.js'
import authApi from '@/common/api/auth.js'
import { WITHDRAW_TEXT } from '@/common/constants/ui.js'
import { fromNow, assetUrl } from '@/common/utils/format.js'
import { iconUrl, loadUiConfig, uiState, requestSubscribe } from '@/common/store/ui.js'
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import QRCode from '@/common/utils/qrcode-generator.js'
export default {
components: { FcNavbar },
data() {
return {
overview: {},
team: [],
records: [],
commissionsList: [],
activeTab: 'team', // 'team' | 'commissions' | 'withdraw'
level: 0,
commissionStatus: '',
minWithdraw: 50,
showWithdraw: false,
showRuleModal: false,
showPosterModal: false,
posterLoading: false,
posterImagePath: '',
posterMeta: null,
withdrawAmount: '',
realname: '',
realnameInput: '',
submitting: false,
loading: true,
loadError: false,
teamLoading: true,
commissionsLoading: false,
teamExpanded: false,
recordsExpanded: false,
commissionsExpanded: false,
teamSeq: 0,
hasShown: false,
mainTabs: [
{ key: 'team', name: '我的团队' },
{ key: 'commissions', name: '收益明细' },
{ key: 'withdraw', name: '提现记录' },
],
levelTabs: [
{ label: '全部', value: 0 },
{ label: '一级直推', value: 1 },
{ label: '二级间推', value: 2 },
],
commissionStatusTabs: [
{ label: '全部', value: '' },
{ label: '已结算', value: '1' },
{ label: '待结算', value: '0' },
],
}
},
computed: {
/**
* 可提现金额数值
* @returns {number}
*/
withdrawableNum() {
const n = Number(this.overview && this.overview.withdrawable)
return isNaN(n) ? 0 : Math.max(n, 0)
},
/**
* 最低提现金额展示文案
* @returns {string}
*/
minWithdrawText() {
const n = Number(this.minWithdraw)
if (isNaN(n) || n <= 0) return '0'
return n % 1 === 0 ? String(n) : n.toFixed(2)
},
/**
* 团队列表折叠展示
* @returns {Array}
*/
visibleTeam() {
const list = this.team || []
return this.teamExpanded ? list : list.slice(0, 5)
},
/**
* 收益账单折叠展示
* @returns {Array}
*/
visibleCommissions() {
const list = this.commissionsList || []
return this.commissionsExpanded ? list : list.slice(0, 5)
},
/**
* 提现记录折叠展示
* @returns {Array}
*/
visibleRecords() {
const list = this.records || []
return this.recordsExpanded ? list : list.slice(0, 5)
},
/**
* 快捷提现金额候选(不超过可提现余额)
* @returns {Array}
*/
quickAmounts() {
const max = this.withdrawableNum
const min = Number(this.minWithdraw || 0)
if (max <= 0) return []
return [50, 100, 200, 500, 1000]
.filter((v) => v <= max && v >= min)
.slice(0, 3)
.map((v) => ({ value: v, label: '¥' + v }))
},
/**
* 提现申请是否符合提交要求
* @returns {boolean}
*/
canSubmit() {
if (this.submitting) return false
const amount = Number(this.withdrawAmount)
const min = Number(this.minWithdraw || 0)
return amount > 0 && amount >= min && amount <= this.withdrawableNum + 0.001
},
/**
* 邀请专属推广链接
* @returns {string}
*/
/**
* 邀请专属推广链接(确保全平台生成完整可访问的 HTTPS 绝对链接)
* @returns {string}
*/
inviteLink() {
const code = this.overview.inviteCode || ''
if (!code) return ''
if (this.overview.shareUrl) {
return this.overview.shareUrl
}
// #ifdef H5
if (typeof location !== 'undefined' && location.origin) {
return location.origin + '/#/pages/index/index?ic=' + encodeURIComponent(code)
}
// #endif
return 'https://fenleixinxi.gzyakj.com.cn/#/pages/index/index?ic=' + encodeURIComponent(code)
},
},
/**
* 页面初始化加载
*/
onLoad() {
loadUiConfig()
this.init()
},
/**
* 页面显示时静默刷新(从其它页面返回)
*/
onShow() {
if (this.hasShown) this.init()
this.hasShown = true
},
/**
* 下拉刷新生命周期
*/
async onPullDownRefresh() {
await this.init()
uni.stopPullDownRefresh()
},
/**
* 微信小程序原生分享给好友(卡片带参绑定邀请码)
*/
onShareAppMessage() {
const code = this.overview.inviteCode || ''
const site = uiState.siteName || '同城分类信息'
return {
title: `【合伙人邀请】加入${site},分享好内容,随时赚佣金!`,
path: '/pages/index/index?ic=' + encodeURIComponent(code),
imageUrl: this.posterImagePath || undefined,
}
},
/**
* 微信小程序分享到朋友圈
*/
onShareTimeline() {
const code = this.overview.inviteCode || ''
const site = uiState.siteName || '同城分类信息'
return {
title: `【合伙人邀请】加入${site},便民生活更轻松,分享赚佣金!`,
query: 'ic=' + encodeURIComponent(code),
imageUrl: this.posterImagePath || undefined,
}
},
methods: {
iconUrl,
assetUrl,
/**
* 页面主数据加载
*/
async init() {
this.teamLoading = true
this.loading = true
try {
const [overview, records] = await Promise.all([
distributionApi.overview(),
distributionApi.withdrawRecords(),
])
this.overview = overview || {}
this.records = Array.isArray(records) ? records : (records && records.list) || []
this.loadError = false
if (overview && overview.minWithdraw != null) {
this.minWithdraw = Number(overview.minWithdraw) || 0
}
if (overview && overview.realname != null) {
this.realname = String(overview.realname)
}
} catch (e) {
this.loadError = true
uni.showToast({ title: '分销数据加载失败', icon: 'none' })
} finally {
this.loading = false
}
await Promise.all([
this.fetchTeam(true),
this.fetchCommissions(true),
])
},
/**
* 切换底部主板块
* @param {'team'|'commissions'|'withdraw'} key 板块标识
*/
switchTab(key) {
this.activeTab = key
if (key === 'commissions' && !this.commissionsList.length) {
this.fetchCommissions(false)
}
},
/**
* 拉取团队成员列表
* @param {boolean} silent 失败时是否静默
*/
async fetchTeam(silent = true) {
const seq = ++this.teamSeq
this.teamLoading = true
try {
const res = await distributionApi.team({
level: this.level || undefined,
page: 1,
size: 50,
})
if (seq !== this.teamSeq) return
this.team = (res && res.list) || []
} catch (e) {
if (seq !== this.teamSeq) return
if (!silent) {
this.team = []
uni.showToast({ title: '团队数据加载失败', icon: 'none' })
}
} finally {
if (seq === this.teamSeq) this.teamLoading = false
}
},
/**
* 拉取收益明细流水账单
* @param {boolean} silent 失败时是否静默
*/
async fetchCommissions(silent = true) {
this.commissionsLoading = true
try {
const res = await distributionApi.commissions({
status: this.commissionStatus !== '' ? this.commissionStatus : undefined,
page: 1,
size: 50,
})
this.commissionsList = (res && res.list) || []
} catch (e) {
if (!silent) {
uni.showToast({ title: '收益账单加载失败', icon: 'none' })
}
} finally {
this.commissionsLoading = false
}
},
/**
* 切换团队层级筛选
* @param {number} v 0 全部 / 1 一级 / 2 二级
*/
changeLevel(v) {
if (this.level === v) return
this.level = v
this.teamExpanded = false
this.fetchTeam(false)
},
/**
* 切换佣金账单状态筛选
* @param {string} s 状态标识
*/
changeCommissionStatus(s) {
if (this.commissionStatus === s) return
this.commissionStatus = s
this.commissionsExpanded = false
this.fetchCommissions(false)
},
/**
* 提现状态展示文案
* @param {string} s 状态枚举
* @returns {string}
*/
statusText(s) {
return WITHDRAW_TEXT[s] || s
},
/**
* 相对时间格式化
* @param {string} t ISO时间字符串
* @returns {string}
*/
timeText(t) {
return fromNow(t) || '—'
},
/**
* 加入时间展示文案
* @param {Object} t 成员对象
* @returns {string}
*/
joinText(t) {
const s = fromNow(t && t.joinedAt)
return s ? s + '加入' : '最近加入'
},
/**
* 格式化金额数值(保留两位小数)
* @param {number|string} v 金额
* @returns {string}
*/
money(v) {
const n = Number(v)
if (!isFinite(n)) return '0.00'
return n.toFixed(2)
},
/**
* 提取昵称首字(用于头像占位)
* @param {string} name 昵称
* @returns {string}
*/
firstChar(name) {
const s = String(name || '').trim()
return s ? s.charAt(0) : '友'
},
/**
* 剪贴板文本复制通用逻辑
* @param {string} text 复制内容
* @param {string} tip 成功提示文案
*/
/**
* 剪贴板文本复制通用逻辑
* @param {string} text 复制内容
* @param {string} tip 成功提示文案
*/
copyText(text, tip = '已复制') {
const data = String(text || '').trim()
if (!data) {
uni.showToast({ title: '内容生成中,请稍后重试', icon: 'none' })
return
}
uni.setClipboardData({
data,
success: () => {
// #ifndef MP-WEIXIN
// 微信小程序端 setClipboardData 会自带系统原生“内容已复制”提示,非微信端主动提示
uni.showToast({ title: tip, icon: 'none' })
// #endif
},
fail: () => {
uni.showToast({ title: '复制失败,请重试', icon: 'none' })
},
})
},
/**
* 复制我的专属邀请码
*/
copyCode() {
if (!this.overview.inviteCode) {
uni.showToast({ title: '邀请码加载中,请稍后重试', icon: 'none' })
return
}
this.copyText(this.overview.inviteCode, '邀请码已复制')
},
/**
* 复制我的专属推广链接
*/
copyLink() {
if (!this.inviteLink) {
uni.showToast({ title: '邀请链接生成中,请稍后重试', icon: 'none' })
return
}
this.copyText(this.inviteLink, '邀请链接已复制')
},
/**
* 分享邀请(H5 / App 端后备逻辑)
*/
shareInvite() {
if (!this.overview.inviteCode) {
uni.showToast({ title: '邀请码加载中,请稍后重试', icon: 'none' })
return
}
// #ifdef H5
if (typeof navigator !== 'undefined' && navigator.share) {
try {
navigator
.share({
title: uiState.shareTitle || uiState.siteName || '同城分类信息 · 全民合伙人',
text: `邀请你加入${uiState.siteName || '同城分类信息'},分享好内容,随时赚佣金!`,
url: this.inviteLink,
})
.catch(() => this.copyLink())
} catch (e) {
this.copyLink()
}
return
}
this.copyLink()
// #endif
// #ifndef H5
this.copyLink()
// #endif
},
/**
* 打开专属合伙人海报弹窗并按需生成海报
*/
async openPosterModal() {
this.showPosterModal = true
if (!this.posterImagePath) {
await this.generatePoster()
}
},
/**
* 网络图片转化为本地可绘制路径(带超时熔断与相对路径补全,防止阻塞海报生成)
* @param {string} src 网络图片地址或本地相对地址
* @param {number} timeoutMs 最长等待毫秒数
* @returns {Promise<string>}
*/
getImageLocalPath(src, timeoutMs = 1500) {
return new Promise((resolve) => {
const fullUrl = assetUrl(src)
if (!fullUrl) return resolve('')
// 若已是小程序本地临时路径(wxfile:// 等),直接复用
if (fullUrl.indexOf('http://') !== 0 && fullUrl.indexOf('https://') !== 0) {
return resolve(fullUrl)
}
let done = false
const timer = setTimeout(() => {
if (!done) {
done = true
resolve('')
}
}, timeoutMs)
uni.getImageInfo({
src: fullUrl,
success: (res) => {
if (!done) {
done = true
clearTimeout(timer)
resolve((res && res.path) || '')
}
},
fail: (err) => {
console.warn('getImageInfo fail for avatar', fullUrl, err)
if (!done) {
done = true
clearTimeout(timer)
resolve('')
}
},
})
})
},
/**
* 绘制圆角矩形
*/
drawRoundRect(ctx, x, y, width, height, radius, fillColor, strokeColor, lineWidth = 1) {
ctx.save()
ctx.beginPath()
ctx.moveTo(x + radius, y)
ctx.lineTo(x + width - radius, y)
ctx.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI)
ctx.lineTo(x + width, y + height - radius)
ctx.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI)
ctx.lineTo(x + radius, y + height)
ctx.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI)
ctx.lineTo(x, y + radius)
ctx.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI)
ctx.closePath()
if (fillColor) {
ctx.setFillStyle(fillColor)
ctx.fill()
}
if (strokeColor) {
ctx.setStrokeStyle(strokeColor)
ctx.setLineWidth(lineWidth)
ctx.stroke()
}
ctx.restore()
},
/**
* 绘制文本
*/
drawSafeText(ctx, text, x, y, options = {}) {
const { fontSize = 20, color = '#1E293B', align = 'left', baseline = 'top', bold = false } = options
ctx.save()
ctx.setFontSize(fontSize)
ctx.setFillStyle(color)
ctx.setTextAlign(align)
ctx.setTextBaseline(baseline)
ctx.fillText(String(text), x, y)
ctx.restore()
},
/**
* 计算文本预估宽度
*/
calcTextWidth(text, fontSize) {
if (!text) return 0
let width = 0
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i)
width += code > 255 ? fontSize : fontSize * 0.6
}
return width
},
/**
* 绘制深墨绿合规二维码
*/
drawRealQRCode(ctx, text, x, y, size) {
try {
const qr = QRCode.create(text, { errorCorrectionLevel: 'M' })
const modules = qr.modules
const count = modules.size
const padding = 8
const qrRealSize = size - padding * 2
const cellSize = qrRealSize / count
this.drawRoundRect(ctx, x, y, size, size, 12, '#FFFFFF', '#DFF2E8', 1)
ctx.save()
ctx.setFillStyle('#04301E')
for (let r = 0; r < count; r++) {
for (let c = 0; c < count; c++) {
if (modules.get(r, c) === 1) {
ctx.fillRect(
x + padding + c * cellSize,
y + padding + r * cellSize,
cellSize + 0.35,
cellSize + 0.35
)
}
}
}
ctx.restore()
} catch (e) {
console.error('QR draw error', e)
}
},
/**
* Canvas 高保真绘制专属合伙人邀请海报(轻氧浅绿微质感,无重叠几何流式排版)
*/
async generatePoster() {
if (this.posterImagePath) {
this.posterLoading = false
return
}
this.posterLoading = true
// 6秒最长超时兜底,防止任何未知异常卡住 loading 状态
const globalTimeout = setTimeout(() => {
if (this.posterLoading) {
this.posterLoading = false
uni.showToast({ title: '生成超时,请重试', icon: 'none' })
}
}, 6000)
const W = 600
const H = 740
const ctx = uni.createCanvasContext('distPosterCanvas', this)
try {
// 获取海报要素
let meta = this.posterMeta
if (!meta) {
try {
meta = await distributionApi.poster()
this.posterMeta = meta
} catch (e) {}
}
const code = (meta && meta.inviteCode) || this.overview.inviteCode || ''
const rawNickname = (meta && meta.nickname) || '同城合伙人'
const nickname = rawNickname.length > 8 ? rawNickname.slice(0, 7) + '…' : rawNickname
const rawSiteName = (meta && meta.siteName) || uiState.siteName || '同城分类信息'
const siteName = rawSiteName.length > 10 ? rawSiteName.slice(0, 9) + '…' : rawSiteName
const slogan = (meta && meta.slogan) || '分享本地好便民 · 天天躺赚现金佣金'
// 1. 全局轻氧浅绿全屏底色(#F2FEF7)
ctx.setFillStyle('#F2FEF7')
ctx.fillRect(0, 0, W, H)
// 2. 纯白立体高光主卡片(圆角 24px,细边框 #D1FAE5)
this.drawRoundRect(ctx, 20, 20, 560, 700, 24, '#FFFFFF', '#D1FAE5', 1.5)
// 3. 顶部 Header 栏(y: 44 ~ 76)
// 左侧绿色品牌胶囊
this.drawRoundRect(ctx, 40, 44, 150, 32, 16, '#E8FAF0', null)
this.drawSafeText(ctx, '同城合伙人计划', 115, 52, {
fontSize: 16,
color: '#00A854',
align: 'center',
})
// 右侧站点名称(右对齐,带截断防重叠)
this.drawSafeText(ctx, siteName, 560, 52, {
fontSize: 16,
color: '#64748B',
align: 'right',
})
// 4. 用户信息栏(y: 96 ~ 160,高度 64)
this.drawRoundRect(ctx, 40, 96, 520, 64, 14, '#F8FAFC', '#F1F5F9', 1)
// 头像加载(带短超时)
let avatarLocal = ''
if (meta && meta.avatar) {
avatarLocal = await this.getImageLocalPath(meta.avatar, 1200)
}
const avatarX = 54
const avatarY = 104
if (avatarLocal) {
ctx.save()
this.drawRoundRect(ctx, avatarX, avatarY, 48, 48, 24, '#E8FAF0', '#A7F3D0', 1)
ctx.drawImage(avatarLocal, avatarX, avatarY, 48, 48)
ctx.restore()
} else {
this.drawRoundRect(ctx, avatarX, avatarY, 48, 48, 24, '#00E070', null)
this.drawSafeText(ctx, this.firstChar(rawNickname), avatarX + 24, avatarY + 12, {
fontSize: 22,
color: '#04301E',
align: 'center',
})
}
// 昵称与身份徽章
this.drawSafeText(ctx, nickname, 114, 108, {
fontSize: 20,
color: '#1E293B',
align: 'left',
})
const nickW = this.calcTextWidth(nickname, 20)
const badgeX = 114 + nickW + 10
this.drawRoundRect(ctx, badgeX, 108, 54, 22, 11, '#FEF3C7', null)
this.drawSafeText(ctx, '合伙人', badgeX + 27, 112, {
fontSize: 13,
color: '#D97706',
align: 'center',
})
// 宣言
this.drawSafeText(ctx, '邀你加入同城便民生活圈,共享现金返佣', 114, 136, {
fontSize: 15,
color: '#64748B',
align: 'left',
})
// 5. 核心主旨标语卡片(y: 176 ~ 250,高度 74)
this.drawRoundRect(ctx, 40, 176, 520, 74, 16, '#F0FDF4', '#D1FAE5', 1)
this.drawSafeText(ctx, slogan, 300, 188, {
fontSize: 24,
color: '#04301E',
align: 'center',
})
const l1Rate = (this.overview && this.overview.level1Rate) || 20
const l2Rate = (this.overview && this.overview.level2Rate) || 10
const cycle = (this.overview && this.overview.settleCycle) || 'T+7'
this.drawSafeText(ctx, `直推返佣 ${l1Rate}% · 间推返佣 ${l2Rate}% · ${cycle} 自动结算到账`, 300, 222, {
fontSize: 16,
color: '#00A854',
align: 'center',
})
// 6. 专属邀请码卡片(y: 266 ~ 416,高度 150)
this.drawRoundRect(ctx, 40, 266, 520, 150, 18, '#FFFFFF', '#00E070', 2)
this.drawSafeText(ctx, '—— 我的专属邀请码 ——', 300, 282, {
fontSize: 16,
color: '#64748B',
align: 'center',
})
this.drawSafeText(ctx, code || '888888', 300, 314, {
fontSize: 52,
color: '#00A854',
align: 'center',
})
this.drawSafeText(ctx, '注册登录填写此邀请码,立即锁定终身分销关系', 300, 384, {
fontSize: 15,
color: '#94A3B8',
align: 'center',
})
// 7. 三大权益标签网格(y: 432 ~ 506,高度 74)
const featY = 432
const feats = [
{ t: '高额返佣', s: '全品类消费享' },
{ t: '自动结算', s: '收益随时提现' },
{ t: '微信零钱', s: '转账零手续费' },
]
feats.forEach((f, idx) => {
const fx = 40 + idx * 179
this.drawRoundRect(ctx, fx, featY, 162, 74, 12, '#F8FAFC', '#E2E8F0', 1)
this.drawSafeText(ctx, f.t, fx + 81, featY + 12, {
fontSize: 19,
color: '#1E293B',
align: 'center',
})
this.drawSafeText(ctx, f.s, fx + 81, featY + 40, {
fontSize: 14,
color: '#64748B',
align: 'center',
})
})
// 8. 优雅虚线分割线(y: 524)
ctx.save()
ctx.setStrokeStyle('#E2E8F0')
ctx.setLineWidth(1.5)
ctx.beginPath()
for (let x = 40; x < 560; x += 12) {
ctx.moveTo(x, 524)
ctx.lineTo(x + 6, 524)
}
ctx.stroke()
ctx.restore()
// 9. 底部扫码与专属二维码转化区(y: 546 ~ 696)
const qrUrl = `https://fenleixinxi.gzyakj.com.cn/#/pages/index/index?ic=${encodeURIComponent(code)}`
const qrX = 48
const qrY = 546
const qrSize = 150
let wxacodeLocal = ''
if (meta && meta.wxacodeUrl) {
wxacodeLocal = await this.getImageLocalPath(meta.wxacodeUrl, 1500)
}
if (wxacodeLocal) {
this.drawRoundRect(ctx, qrX, qrY, qrSize, qrSize, 12, '#FFFFFF', '#DFF2E8', 1)
ctx.drawImage(wxacodeLocal, qrX + 6, qrY + 6, qrSize - 12, qrSize - 12)
} else {
this.drawRealQRCode(ctx, qrUrl, qrX, qrY, qrSize)
}
// 二维码右侧文字说明(从 x = 216 开始,宽度 344px 极为充裕,排版整洁绝不重叠)
const textX = 218
this.drawSafeText(ctx, '微信长按识别小程序', textX, 558, {
fontSize: 23,
color: '#1E293B',
align: 'left',
})
this.drawSafeText(ctx, '找工作 · 租售房 · 本地便民', textX, 594, {
fontSize: 17,
color: '#00A854',
align: 'left',
})
this.drawSafeText(ctx, '同城生活分类信息 · 官方严选保障', textX, 626, {
fontSize: 15,
color: '#64748B',
align: 'left',
})
// 专属邀请码小徽章
this.drawRoundRect(ctx, textX, 656, 210, 28, 14, '#E8FAF0', '#A7F3D0', 1)
this.drawSafeText(ctx, `专属邀请码:${code}`, textX + 105, 661, {
fontSize: 15,
color: '#00A854',
align: 'center',
})
// 10. 执行绘制并双重触发导出(彻底规避微信小程序真机 draw 回调偶发不触发的缺陷)
let hasExported = false
const finishExport = () => {
if (hasExported) return
hasExported = true
clearTimeout(globalTimeout)
uni.canvasToTempFilePath(
{
canvasId: 'distPosterCanvas',
width: W,
height: H,
destWidth: W * 2,
destHeight: H * 2,
fileType: 'png',
quality: 1,
success: (res) => {
this.posterImagePath = res.tempFilePath
this.posterLoading = false
},
fail: (err) => {
console.error('canvasToTempFilePath error', err)
this.posterLoading = false
uni.showToast({ title: '海报导出失败,请重试', icon: 'none' })
},
},
this
)
}
// 方式1:在 draw 回调中导出
ctx.draw(false, () => {
setTimeout(finishExport, 80)
})
// 方式2:延时兜底导出(防止平台未执行 draw 回调)
setTimeout(finishExport, 450)
} catch (e) {
clearTimeout(globalTimeout)
console.error('generatePoster error', e)
this.posterLoading = false
uni.showToast({ title: '海报绘制失败', icon: 'none' })
}
},
/**
* 全屏预览生成的海报大图
*/
/**
* 唤起微信原生图片转发菜单(一键发给好友/发朋友圈/收藏),不支持时回退全屏预览长按转发
*/
sharePosterImage() {
if (!this.posterImagePath) {
uni.showToast({ title: '海报正在生成,请稍候', icon: 'none' })
return
}
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && typeof wx.showShareImageMenu === 'function') {
wx.showShareImageMenu({
path: this.posterImagePath,
fail: (err) => {
if (err && err.errMsg && !err.errMsg.includes('cancel')) {
this.previewPoster()
}
},
})
return
}
// #endif
this.previewPoster()
},
/**
* 全屏预览生成的海报大图
*/
previewPoster() {
if (!this.posterImagePath) return
uni.previewImage({
urls: [this.posterImagePath],
current: this.posterImagePath,
})
},
/**
* 保存海报到手机系统相册
*/
savePoster() {
if (!this.posterImagePath) {
uni.showToast({ title: '海报正在生成,请稍候', icon: 'none' })
return
}
// #ifdef H5
uni.showToast({ title: '请长按上方图片保存到相册', icon: 'none' })
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath: this.posterImagePath,
success: () => {
uni.showToast({ title: '海报已保存到系统相册', icon: 'success' })
},
fail: (err) => {
if (err && err.errMsg && (err.errMsg.includes('auth') || err.errMsg.includes('deny'))) {
uni.showModal({
title: '授权提示',
content: '需要访问您的相册权限以保存海报,请在系统设置中开启授权。',
confirmText: '去设置',
success: (r) => {
if (r.confirm) uni.openSetting()
},
})
} else {
uni.showToast({ title: '保存取消或失败', icon: 'none' })
}
},
})
// #endif
},
/**
* 复制海报推广文案到剪贴板
*/
copyPosterText() {
const code = this.overview.inviteCode || ''
const site = uiState.siteName || '同城分类信息'
const text = `【合伙人邀请】推荐你加入${site}!\n汇聚本地招聘求职、房产租售、二手闲置、便民服务,方便又实惠!\n我的专属邀请码:${code}\n在微信小程序搜索“${site}”或识别海报加入即可天天赚佣金!`
this.copyText(text, '推广文案已复制')
},
/**
* 打开提现弹层
*/
openWithdraw() {
if (this.withdrawableNum <= 0) {
uni.showToast({ title: '暂无可提现佣金,快去邀请好友吧', icon: 'none' })
return
}
this.withdrawAmount = ''
this.showWithdraw = true
},
/**
* 拉起微信「确认收款」页(商家转账模式)
* @param {Object} w 提现记录项
*/
confirmTransfer(w) {
if (!w || !w.packageInfo) {
uni.showToast({ title: '转账处理中,请在微信消息中确认收款', icon: 'none' })
return
}
if (typeof uni === 'undefined' || !uni.requestMerchantTransfer) {
uni.showToast({ title: '请在微信消息中确认收款', icon: 'none' })
return
}
uni.requestMerchantTransfer({
mchId: (this.overview && this.overview.transferMchId) || '',
appId: (this.overview && this.overview.transferAppId) || '',
package: w.packageInfo,
success: () => {
uni.showToast({ title: '已确认,等待到账', icon: 'none' })
setTimeout(() => this.refreshAfterWithdraw(), 1500)
},
fail: () => uni.showToast({ title: '已取消确认', icon: 'none' }),
})
},
/**
* 关闭提现弹层
*/
closeWithdraw() {
if (this.submitting) return
this.showWithdraw = false
this.withdrawAmount = ''
},
/**
* 填入全部可提现金额
*/
fillAll() {
this.withdrawAmount = this.withdrawableNum.toFixed(2)
},
/**
* 提交提现申请
*/
async submitWithdraw() {
if (this.submitting) return
if (!this.canSubmit) {
const amount = Number(this.withdrawAmount)
if (!amount || amount <= 0) {
uni.showToast({ title: '请输入提现金额', icon: 'none' })
} else if (amount < Number(this.minWithdraw || 0)) {
uni.showToast({ title: '最低提现 ¥' + this.minWithdrawText, icon: 'none' })
} else {
uni.showToast({ title: '提现金额超过可提现余额', icon: 'none' })
}
return
}
// 授权提现到账通知订阅消息
await requestSubscribe('transfer')
this.submitting = true
try {
if (!this.realname) {
const name = String(this.realnameInput || '').trim()
if (!/^[\u4e00-\u9fa5·]{2,20}$/.test(name)) {
uni.showToast({ title: '请填写 2-20 位中文真实姓名', icon: 'none' })
return
}
try {
await authApi.saveRealname({ realname: name })
this.realname = name
} catch (e) {
uni.showToast({ title: e.message || '姓名保存失败', icon: 'none' })
return
}
}
const amount = Math.round(Number(this.withdrawAmount) * 100) / 100
await distributionApi.withdraw({ amount, channel: 'wxpay' })
this.showWithdraw = false
this.withdrawAmount = ''
uni.showToast({ title: '提现申请已提交', icon: 'none' })
await this.refreshAfterWithdraw()
} catch (e) {
uni.showToast({ title: e.message || '提现失败,请稍后重试', icon: 'none' })
} finally {
this.submitting = false
}
},
/**
* 提现操作后静默刷新
*/
async refreshAfterWithdraw() {
try {
const [overview, records] = await Promise.all([
distributionApi.overview(),
distributionApi.withdrawRecords(),
])
if (overview) this.overview = overview
this.records = Array.isArray(records) ? records : (records && records.list) || []
} catch (e) {}
},
},
}
</script>
<style lang="scss" scoped>
/* ==================== 顶部导航右侧规则按钮 ==================== */
.dist__nav-rule {
display: flex;
align-items: center;
padding: 8rpx 18rpx;
border-radius: $fc-radius-pill;
background: rgba(255, 255, 255, 0.7);
border: 1rpx solid rgba(0, 224, 112, 0.3);
}
.dist__nav-rule-icon {
width: 26rpx;
height: 26rpx;
margin-right: 6rpx;
}
.dist__nav-rule-text {
font-size: 24rpx;
font-weight: 600;
color: #00A854;
}
/* ==================== Hero ==================== */
.dist__hero {
background: $fc-gradient-hero;
padding: 12rpx 0 150rpx;
border-radius: 0 0 40rpx 40rpx;
}
.dist__hero-copy {
padding: 16rpx 40rpx 0;
display: flex;
flex-direction: column;
}
.dist__hero-title {
color: $fc-text;
font-size: 38rpx;
font-weight: 700;
letter-spacing: 0.5rpx;
}
.dist__hero-sub {
margin-top: 10rpx;
color: $fc-text-secondary;
font-size: 24rpx;
}
/* ==================== 佣金钱包 ==================== */
.dist__wallet {
position: relative;
z-index: 2;
margin: -116rpx 28rpx 0;
padding: 28rpx 30rpx 24rpx;
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(4, 48, 30, 0.14);
}
.dist__wallet-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.dist__wallet-label-wrap {
display: flex;
align-items: center;
min-width: 0;
}
.dist__wallet-icon {
width: 38rpx;
height: 38rpx;
flex-shrink: 0;
}
.dist__wallet-label {
margin-left: 10rpx;
font-size: 24rpx;
color: $fc-text-secondary;
}
.dist__withdraw-btn {
flex-shrink: 0;
padding: 10rpx 30rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 24rpx;
font-weight: 700;
box-shadow: 0 6rpx 16rpx rgba(0, 184, 92, 0.24);
transition: transform 0.15s ease;
&:active {
transform: scale(0.96);
}
}
.dist__wallet-amount {
display: flex;
align-items: baseline;
margin-top: 16rpx;
}
.dist__wallet-symbol {
font-size: 36rpx;
font-weight: 700;
color: #04301E;
}
.dist__wallet-num {
margin-left: 8rpx;
font-size: 64rpx;
font-weight: 800;
line-height: 1;
letter-spacing: 1rpx;
color: #04301E;
}
.dist__wallet-stats {
display: flex;
align-items: center;
margin-top: 28rpx;
padding-top: 22rpx;
border-top: 1rpx solid #EDF5F0;
}
.dist__wallet-stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.dist__wallet-stat-num {
font-size: 28rpx;
font-weight: 700;
color: $fc-text;
}
.dist__wallet-stat-label {
margin-top: 6rpx;
font-size: 22rpx;
color: $fc-text-secondary;
}
/* ==================== 错误提示 ==================== */
.dist__notice {
margin: 20rpx 28rpx 0;
padding: 16rpx 22rpx;
border-radius: 16rpx;
background: #fff3f3;
border: 1rpx solid #ffd6d6;
display: flex;
align-items: center;
}
.dist__notice-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.dist__notice-text {
margin-left: 12rpx;
font-size: 24rpx;
color: #d93025;
}
/* ==================== 通用卡片 ==================== */
.dist__card {
margin: 24rpx 28rpx 0;
padding: 28rpx;
border-radius: 24rpx;
background: #ffffff;
box-shadow: $fc-shadow-sm;
}
.dist__card-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.dist__card-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 700;
color: $fc-text;
}
.dist__card-icon-wrap {
width: 52rpx;
height: 52rpx;
margin-right: 14rpx;
border-radius: 16rpx;
background: $fc-primary-lighter;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.dist__card-icon {
width: 36rpx;
height: 36rpx;
}
.dist__card-extra {
flex-shrink: 0;
font-size: 22rpx;
color: $fc-text-secondary;
}
/* ==================== 邀请推广 ==================== */
.dist__invite-box {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
padding: 24rpx 26rpx;
border-radius: 20rpx;
border: 1rpx solid $fc-border;
background: $fc-primary-lighter;
}
.dist__invite-code {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.dist__invite-label {
font-size: 22rpx;
color: $fc-text-secondary;
}
.dist__invite-value {
margin-top: 6rpx;
font-size: 38rpx;
font-weight: 700;
letter-spacing: 4rpx;
color: #00A854;
}
.dist__invite-copy {
flex-shrink: 0;
margin-left: 20rpx;
padding: 10rpx 28rpx;
border-radius: $fc-radius-pill;
background: #ffffff;
font-size: 24rpx;
font-weight: 600;
color: #00A854;
box-shadow: $fc-shadow-sm;
transition: transform 0.15s ease;
&:active {
transform: scale(0.95);
}
}
.dist__invite-actions {
display: flex;
gap: 16rpx;
margin-top: 24rpx;
}
.dist__btn {
flex: 1;
height: 82rpx;
border-radius: $fc-radius-pill;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
border: none;
outline: none;
padding: 0 12rpx;
box-sizing: border-box;
transition: transform 0.15s ease;
line-height: 1;
&::after {
border: none;
}
&:active {
transform: scale(0.97);
}
}
.dist__btn--primary {
background: $fc-gradient-primary;
color: $fc-primary-ink;
box-shadow: 0 8rpx 20rpx rgba(0, 224, 112, 0.28);
}
.dist__btn--secondary {
background: #E8FAF0;
color: #00A854;
border: 1rpx solid rgba(0, 168, 84, 0.2);
}
.dist__btn--ghost {
background: #F8FAFC;
color: #475569;
border: 1rpx solid #E2E8F0;
}
.dist__btn-icon-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(4, 48, 30, 0.12);
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
}
.dist__btn-icon {
width: 28rpx;
height: 28rpx;
}
.dist__btn-icon--plain {
margin-right: 8rpx;
}
/* ==================== 顶部 Tab 导航 ==================== */
.dist__tabs-bar {
display: flex;
margin: 28rpx 28rpx 4rpx;
padding: 0 10rpx;
}
.dist__tab-item {
position: relative;
margin-right: 40rpx;
padding: 12rpx 4rpx 18rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.dist__tab-text {
font-size: 30rpx;
color: #64748B;
font-weight: 500;
transition: all 0.2s;
}
.dist__tab-item--active .dist__tab-text {
color: #1E293B;
font-size: 32rpx;
font-weight: 700;
}
.dist__tab-line {
position: absolute;
bottom: 6rpx;
width: 44rpx;
height: 6rpx;
border-radius: 4rpx;
background: #00E070;
}
/* ==================== 分段器与小结 ==================== */
.dist__seg {
display: flex;
background: #F1F5F9;
border-radius: 14rpx;
padding: 6rpx;
margin-top: 20rpx;
}
.dist__seg-item {
flex: 1;
text-align: center;
padding: 12rpx 0;
font-size: 24rpx;
color: #64748B;
border-radius: 10rpx;
transition: all 0.15s;
}
.dist__seg-item--active {
background: #ffffff;
color: #00A854;
font-weight: 700;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
}
.dist__team-summary {
display: flex;
align-items: center;
margin-top: 18rpx;
font-size: 22rpx;
color: #64748B;
}
.dist__team-dot {
margin: 0 10rpx;
}
/* ==================== 骨架屏与空状态 ==================== */
.dist__skeleton {
margin-top: 20rpx;
}
.dist__skeleton-row {
display: flex;
align-items: center;
padding: 20rpx 0;
}
.dist__skeleton-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
flex-shrink: 0;
}
.dist__skeleton-lines {
flex: 1;
margin-left: 18rpx;
}
.dist__skeleton-line {
height: 24rpx;
border-radius: 6rpx;
width: 60%;
margin-bottom: 12rpx;
}
.dist__skeleton-line--short {
width: 40%;
margin-bottom: 0;
}
.dist__skeleton-record {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
}
.dist__skeleton-amount {
width: 100rpx;
height: 36rpx;
border-radius: 8rpx;
}
.dist__empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0 30rpx;
}
.dist__empty-icon-wrap {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: $fc-primary-lighter;
display: flex;
align-items: center;
justify-content: center;
}
.dist__empty-icon {
width: 52rpx;
height: 52rpx;
}
.dist__empty-text {
margin-top: 20rpx;
font-size: 24rpx;
color: $fc-text-secondary;
}
.dist__empty-btn {
margin-top: 24rpx;
padding: 12rpx 36rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 24rpx;
font-weight: 700;
}
/* ==================== 团队列表 ==================== */
.dist__team-list {
margin-top: 10rpx;
}
.dist__member {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #F1F5F9;
}
.dist__member:last-child {
border-bottom: none;
}
.dist__member-avatar-wrap {
width: 76rpx;
height: 76rpx;
flex-shrink: 0;
}
.dist__member-avatar-img {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
}
.dist__member-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
background: $fc-primary-lighter;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 700;
color: #00A854;
}
.dist__member-main {
flex: 1;
min-width: 0;
margin-left: 18rpx;
}
.dist__member-top {
display: flex;
align-items: center;
}
.dist__member-name {
font-size: 28rpx;
font-weight: 600;
color: $fc-text;
}
.dist__badge {
margin-left: 10rpx;
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
}
.dist__badge--l1 {
background: #E8FAF0;
color: #00A854;
}
.dist__badge--l2 {
background: #EFF6FF;
color: #2563EB;
}
.dist__member-meta {
margin-top: 6rpx;
font-size: 22rpx;
color: #64748B;
}
.dist__member-income {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.dist__member-income-num {
font-size: 30rpx;
font-weight: 700;
color: #00A854;
}
.dist__member-income-label {
margin-top: 4rpx;
font-size: 20rpx;
color: #64748B;
}
/* ==================== 账单与提现记录 ==================== */
.dist__record-list {
margin-top: 10rpx;
}
.dist__record {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #F1F5F9;
}
.dist__record:last-child {
border-bottom: none;
}
.dist__record-main {
flex: 1;
min-width: 0;
margin-right: 16rpx;
}
.dist__record-top {
display: flex;
align-items: center;
}
.dist__record-title {
font-size: 28rpx;
font-weight: 600;
color: $fc-text;
}
.dist__record-status {
margin-left: 12rpx;
padding: 2rpx 12rpx;
border-radius: 6rpx;
font-size: 20rpx;
font-weight: 600;
}
.dist__record-status--auditing {
background: #FEF3C7;
color: #D97706;
}
.dist__record-status--paying {
background: #E0E7FF;
color: #4F46E5;
}
.dist__record-status--paid {
background: #DCFCE7;
color: #16A34A;
}
.dist__record-status--failed {
background: #FEE2E2;
color: #DC2626;
}
.dist__record-time {
margin-top: 6rpx;
font-size: 22rpx;
color: #64748B;
}
.dist__record-no {
margin-top: 4rpx;
font-size: 20rpx;
color: #94A3B8;
}
.dist__record-right {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.dist__record-amount {
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
}
.dist__record-amount--income {
color: #00A854;
}
.dist__record-action {
margin-top: 8rpx;
padding: 6rpx 16rpx;
border-radius: $fc-radius-pill;
background: #00E070;
color: #04301E;
font-size: 20rpx;
font-weight: 700;
}
/* ==================== 展开收起 ==================== */
.dist__more {
display: flex;
align-items: center;
justify-content: center;
padding: 22rpx 0 6rpx;
font-size: 24rpx;
color: #64748B;
}
.dist__chevron {
width: 12rpx;
height: 12rpx;
border-right: 3rpx solid #94A3B8;
border-bottom: 3rpx solid #94A3B8;
transform: rotate(45deg);
margin-left: 8rpx;
transition: transform 0.2s ease;
}
.dist__chevron--up {
transform: rotate(-135deg);
}
.dist__bottom-space {
height: 60rpx;
}
/* ==================== 遮罩弹层 ==================== */
.dist__mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: flex-end;
}
.dist__sheet {
width: 100%;
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
padding: 24rpx 36rpx calc(24rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.dist__sheet-handle {
width: 64rpx;
height: 8rpx;
border-radius: 4rpx;
background: #E2E8F0;
margin: 0 auto 20rpx;
}
.dist__sheet-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.dist__sheet-title {
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
}
.dist__sheet-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dist__sheet-close-img {
width: 32rpx;
height: 32rpx;
}
.dist__sheet-balance {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-top: 24rpx;
padding: 18rpx 22rpx;
border-radius: 16rpx;
background: #F8FAFC;
}
.dist__sheet-balance-label {
font-size: 24rpx;
color: #64748B;
}
.dist__sheet-balance-num {
font-size: 32rpx;
font-weight: 700;
color: #04301E;
}
.dist__sheet-field {
display: flex;
align-items: center;
margin-top: 24rpx;
padding: 16rpx 20rpx;
border-radius: 18rpx;
border: 2rpx solid #E2E8F0;
background: #ffffff;
}
.dist__sheet-symbol {
font-size: 36rpx;
font-weight: 700;
color: #1E293B;
margin-right: 12rpx;
}
.dist__sheet-input {
flex: 1;
font-size: 38rpx;
font-weight: 700;
color: #1E293B;
}
.dist__sheet-input--name {
font-size: 28rpx;
font-weight: 500;
}
.dist__sheet-placeholder {
font-size: 26rpx;
font-weight: 400;
color: #94A3B8;
}
.dist__sheet-all {
font-size: 26rpx;
font-weight: 600;
color: #00A854;
padding: 8rpx 14rpx;
}
.dist__sheet-chips {
display: flex;
gap: 16rpx;
margin-top: 20rpx;
}
.dist__chip {
padding: 10rpx 28rpx;
border-radius: $fc-radius-pill;
background: #F1F5F9;
font-size: 24rpx;
font-weight: 600;
color: #475569;
}
.dist__chip--active {
background: #E8FAF0;
color: #00A854;
border: 1rpx solid #00E070;
}
.dist__sheet-tip-row {
display: flex;
align-items: center;
margin-top: 22rpx;
}
.dist__sheet-tip-icon {
width: 26rpx;
height: 26rpx;
flex-shrink: 0;
}
.dist__sheet-tip {
margin-left: 10rpx;
font-size: 22rpx;
color: #64748B;
}
.dist__sheet-btn {
margin-top: 32rpx;
height: 88rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
display: flex;
align-items: center;
justify-content: center;
color: $fc-primary-ink;
font-size: 30rpx;
font-weight: 700;
box-shadow: 0 8rpx 20rpx rgba(0, 224, 112, 0.28);
}
.dist__sheet-btn--disabled {
opacity: 0.45;
pointer-events: none;
}
.dist__sheet-safe {
height: 20rpx;
}
/* ==================== 规则弹层 ==================== */
.dist__rule-content {
margin-top: 24rpx;
max-height: 60vh;
overflow-y: auto;
}
.dist__rule-item {
display: flex;
margin-bottom: 24rpx;
}
.dist__rule-badge {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: #E8FAF0;
color: #00A854;
font-size: 22rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 4rpx;
}
.dist__rule-text-wrap {
flex: 1;
margin-left: 16rpx;
}
.dist__rule-h {
font-size: 28rpx;
font-weight: 700;
color: #1E293B;
display: block;
}
.dist__rule-p {
margin-top: 6rpx;
font-size: 24rpx;
color: #64748B;
line-height: 1.5;
display: block;
}
/* ==================== 海报弹层 ==================== */
.dist__poster-sheet {
width: 100%;
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
padding: 28rpx 36rpx calc(32rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.dist__poster-sheet-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.dist__poster-sheet-title {
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
}
.dist__poster-loading {
height: 520rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.dist__poster-spinner {
width: 56rpx;
height: 56rpx;
border: 6rpx solid #E8FAF0;
border-top-color: #00E070;
border-radius: 50%;
animation: posterSpin 0.8s linear infinite;
}
@keyframes posterSpin {
to {
transform: rotate(360deg);
}
}
.dist__poster-loading-text {
margin-top: 24rpx;
font-size: 26rpx;
color: #64748B;
}
.dist__poster-preview {
margin-top: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.dist__poster-img {
width: 500rpx;
height: 616rpx;
border-radius: 20rpx;
box-shadow: 0 16rpx 40rpx rgba(4, 48, 30, 0.16);
background: #ffffff;
}
.dist__poster-tip {
margin-top: 16rpx;
font-size: 22rpx;
color: #94A3B8;
}
.dist__poster-actions {
display: flex;
gap: 16rpx;
margin-top: 28rpx;
}
.dist__poster-act-btn {
flex: 1;
height: 82rpx;
border-radius: $fc-radius-pill;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
border: none;
line-height: 1;
&::after {
border: none;
}
&:active {
transform: scale(0.97);
}
}
.dist__poster-act-btn--primary {
background: $fc-gradient-primary;
color: $fc-primary-ink;
box-shadow: 0 8rpx 20rpx rgba(0, 224, 112, 0.28);
}
.dist__poster-act-btn--share {
background: #E8FAF0;
color: #00A854;
border: 1rpx solid rgba(0, 168, 84, 0.2);
}
.dist__poster-act-btn--ghost {
background: #F1F5F9;
color: #475569;
}
</style>