feat(distribution): 增强分销功能与界面优化

- 新增银行卡管理页面,支持用户查看与管理银行卡。
- 实现添加、删除及设置默认银行卡功能。
- 优化邀请与加入页面,展示邀请人信息并改进用户提示。
- 更新海报与二维码生成逻辑,提升用户体验。
- 重构多个组件,提高可读性与可维护性。
This commit is contained in:
pikachu1995@126.com
2026-08-11 15:43:21 +08:00
parent 7d3daf2f02
commit 4b1a09cbc3
18 changed files with 2481 additions and 527 deletions

View File

@@ -4,7 +4,16 @@
<view v-else-if="!plan">
<view class="empty">暂未开放线上招募</view>
</view>
<view v-else class="page-body" :class="{ 'with-footer': showManualApply || isApplyPending }">
<view v-else class="page-body" :class="{ 'with-footer': showLoginFooter || showManualApply || isApplyPending }">
<!-- 邀请人信息来自图文邀请卡 -->
<view class="inviter-header" v-if="showInviterHeader">
<image class="inviter-avatar" :src="inviterAvatar" mode="aspectFill" />
<view class="inviter-meta">
<view class="inviter-label">邀请您加入</view>
<view class="inviter-name">{{ inviterName }}</view>
</view>
</view>
<!-- 招募海报 -->
<view class="section poster-section" v-if="showPoster">
<image
@@ -22,7 +31,9 @@
<!-- 加入条件 -->
<view class="section" v-if="hasJoinCondition">
<view class="title">加入条件</view>
<view class="condition-tip">满足以下条件后可申请成为分销员</view>
<view class="condition-tip">
{{ isUserLoggedIn ? '满足以下条件后可申请成为分销员' : '登录后可查看您的达标进度并申请成为分销员' }}
</view>
<view class="condition-item" v-if="plan.requireSelfPurchaseAmount">
<view class="condition-head">
@@ -87,7 +98,14 @@
</view>
</view>
<view class="apply-footer-fixed" v-if="showManualApply || isApplyPending">
<view class="apply-footer-fixed" v-if="showLoginFooter">
<view class="apply-footer-card">
<view class="login-tip">登录后即可查看加入条件并申请成为分销员</view>
<view class="submit" @click="goLogin">立即登录</view>
</view>
</view>
<view class="apply-footer-fixed" v-else-if="showManualApply || isApplyPending">
<view class="apply-footer-card">
<view class="agree" v-if="showManualApply" @click="agreed = !agreed">
<checkbox :checked="agreed" @click.stop="agreed = !agreed" />
@@ -148,8 +166,13 @@ import { ref, reactive, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import { getRecruitPage, getRecruitProgress, submitRecruitApplication } from '@/api/distribution'
import { tipsToLogin } from '@/utils/filters.js'
import storage from '@/utils/storage.js'
import config from '@/config/config'
import { parseGoodsImageUrl, tipsToLogin } from '@/utils/filters.js'
import { resolveRecruitDistributionIdFromScene } from '@/utils/distributionBind.js'
import { getRecruitPage, getRecruitProgress, getRecruitInviter, submitRecruitApplication } from '@/api/distribution'
const defaultAvatar = config.defaultUserPhoto
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
@@ -166,6 +189,9 @@ const agreementVisible = ref(false)
const applyFormVisible = ref(false)
const formInfoCompleted = ref(false)
const fieldErrors = reactive<Record<string, string>>({})
const inviterInfo = ref<any>(null)
const isUserLoggedIn = computed(() => !!storage.getAccessToken())
const plan = computed(() => pageInfo.value.plan || null)
const fields = computed(() => pageInfo.value.fields || [])
@@ -219,12 +245,25 @@ const isManualApply = computed(() => {
return (plan.value?.applyMode || 'MANUAL') === 'MANUAL'
})
const showLoginFooter = computed(() => {
return !!plan.value && !isUserLoggedIn.value && !loading.value
})
const showInviterHeader = computed(() => {
if (!recruitToken.value) return false
const info = inviterInfo.value
return !!(info?.memberName || info?.memberAvatar)
})
const inviterName = computed(() => inviterInfo.value?.memberName || '分销员')
const inviterAvatar = computed(() => parseGoodsImageUrl(inviterInfo.value?.memberAvatar) || defaultAvatar)
const showManualApply = computed(() => {
return !showApplyResult.value && conditionsMet.value && isManualApply.value
return isUserLoggedIn.value && !showApplyResult.value && conditionsMet.value && isManualApply.value
})
const showAutoApplyTip = computed(() => {
return !showApplyResult.value && conditionsMet.value && plan.value?.applyMode === 'AUTO'
return isUserLoggedIn.value && !showApplyResult.value && conditionsMet.value && plan.value?.applyMode === 'AUTO'
})
const showApplyForm = computed(() => {
@@ -250,10 +289,48 @@ const consumeCountOk = computed(() => {
})
onLoad((query) => {
recruitToken.value = query?.token || query?.distributionId || ''
onLoad(async (query) => {
let token = query?.token || query?.distributionId || ''
if (!token && query?.scene) {
token = await resolveRecruitDistributionIdFromScene(query.scene)
}
recruitToken.value = token ? String(token) : ''
if (recruitToken.value) {
loadInviterInfo(recruitToken.value)
}
})
function loadInviterInfo(distributionId: string) {
getRecruitInviter(distributionId).then((res) => {
if (res.data?.success) {
inviterInfo.value = res.data.result || null
}
})
}
function emptyProgress() {
return {
satisfied: false,
currentSelfPurchaseAmount: 0,
currentConsumeCount: 0,
purchaseGoodsSatisfied: false,
}
}
function hasConditionalJoin(planData: any) {
if (!planData || planData.joinConditionType !== 'CONDITIONAL') return false
return !!(planData.requireSelfPurchaseAmount || planData.requireConsumeCount || planData.requirePurchaseGoods)
}
function goLogin() {
// #ifdef MP-WEIXIN
uni.navigateTo({ url: '/pages/passport/wechatMPLogin' })
// #endif
// #ifndef MP-WEIXIN
uni.navigateTo({ url: '/pages/passport/login' })
// #endif
}
onShow(() => {
loadPageData()
})
@@ -265,10 +342,20 @@ function loadPageData() {
submitApplicationStatus.value = ''
formInfoCompleted.value = false
Object.keys(fieldErrors).forEach((key) => delete fieldErrors[key])
Promise.all([getRecruitPage(), getRecruitProgress()])
.then(([pageRes, progressRes]) => {
pageInfo.value = pageRes.data?.result || {}
progress.value = progressRes.data?.result || null
const tasks: Promise<any>[] = [getRecruitPage()]
if (isUserLoggedIn.value) {
tasks.push(getRecruitProgress())
}
Promise.all(tasks)
.then((results) => {
const pageRes = results[0]
const pageResult = pageRes.data?.result || {}
pageInfo.value = pageResult
if (results.length > 1) {
progress.value = results[1].data?.result || emptyProgress()
} else {
progress.value = hasConditionalJoin(pageResult.plan) ? emptyProgress() : { satisfied: true }
}
})
.finally(() => {
loading.value = false
@@ -401,6 +488,50 @@ function submit() {
font-size: 28rpx;
}
.inviter-header {
display: flex;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 24rpx;
}
.inviter-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: #f5f5f5;
margin-right: 24rpx;
flex-shrink: 0;
}
.inviter-meta {
flex: 1;
min-width: 0;
}
.inviter-label {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.inviter-name {
font-size: 32rpx;
font-weight: 600;
color: #222;
line-height: 1.4;
}
.login-tip {
font-size: 26rpx;
color: #666;
text-align: center;
margin-bottom: 24rpx;
line-height: 1.5;
}
.section {
background: #fff;
border-radius: 16rpx;