feat(distribution): 实现分销员中心完整功能

- 新增分销业绩统计页面,包含时间范围筛选和自定义日期选择功能
- 重构分销认证页面,改为跳转至招募页面申请分销员身份
- 添加分销商品访问记录功能,支持分享追踪和佣金计算
- 更新分销历史记录页面,优化数据结构和显示字段
- 完全重写分销员首页,包含用户信息、收益统计、等级系统等功能
This commit is contained in:
pikachu1995@126.com
2026-08-05 18:07:27 +08:00
parent 6754a54f1b
commit 7d3daf2f02
34 changed files with 10053 additions and 2530 deletions

View File

@@ -1,127 +1,756 @@
<template>
<view class="wrapper" :style="themeStyle">
<u-tabs
:list="stepList"
:scrollable="false"
v-model:current="currentStep"
:lineColor="lightColor"
:activeStyle="{ color: lightColor }"
></u-tabs>
<view class="feedBack-box">
<up-form
:model="formData"
label-position="top"
ref="uFormRef"
>
<up-form-item label="会员昵称" prop="name">
<u-input
border="none"
class="field-input"
v-model="formData.name"
:custom-style="fieldInputStyle"
/>
</up-form-item>
<up-form-item label="账户类型" prop="name"></up-form-item>
<up-form-item label="收款人姓名" prop="name">
<u-input
border="none"
class="field-input"
v-model="formData.name"
placeholder="请输入收款人姓名"
:custom-style="fieldInputStyle"
/>
</up-form-item>
<up-form-item label="收款账号" prop="name">
<u-input
border="none"
class="field-input"
v-model="formData.name"
placeholder="请输入收款人账号"
:custom-style="fieldInputStyle"
/>
</up-form-item>
<up-form-item label="银行名称" prop="name">
<u-input
border="none"
class="field-input"
v-model="formData.name"
placeholder="请输入开户银行支行名称"
:custom-style="fieldInputStyle"
/>
</up-form-item>
</up-form>
</view>
<view class="submit" @click="submitForm">提交</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { onReady } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import { fieldInputStyle } from '@/utils/form-style.js'
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const lightColor = computed(() => store.getters.lightColor)
const currentStep = ref(0)
const uFormRef = ref<any>(null)
const stepList = [
{ name: '推广人资料' },
{ name: '平台审核' },
{ name: '完成' },
]
const formData = reactive({
name: '',
radio: '',
})
const rules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
}
onReady(() => {
uFormRef.value?.setRules(rules)
})
function submitForm() {
uFormRef.value?.validate().catch(() => {
uni.showToast({ title: '请填写有效信息', icon: 'none' })
})
}
</script>
<style lang="scss">
page {
background: #f8f8f8;
}
</style>
<style lang="scss" scoped>
@import '@/pages/passport/entry/seller/entry-form.scss';
.wrapper {
box-sizing: border-box;
min-height: 100vh;
padding: 20rpx 24rpx 40rpx;
background: #f8f8f8;
@include seller-entry-form;
}
.feedBack-box {
background: #fff;
border-radius: 20rpx;
padding: 32rpx;
margin-top: 20rpx;
}
.submit {
@include seller-entry-submit;
}
</style>
<template>
<view class="wrapper" :style="themeStyle">
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="!plan">
<view class="empty">暂未开放线上招募</view>
</view>
<view v-else class="page-body" :class="{ 'with-footer': showManualApply || isApplyPending }">
<!-- 招募海报 -->
<view class="section poster-section" v-if="showPoster">
<image
class="poster-image"
:src="plan.posterBackground"
mode="widthFix"
@click="previewPoster"
/>
<view class="poster-text" v-if="posterTitle">{{ posterTitle }}</view>
<view class="poster-benefit" v-if="posterBenefitText">{{ posterBenefitText }}</view>
<view class="poster-rule" v-if="posterRuleText">{{ posterRuleText }}</view>
<view class="poster-contact" v-if="posterContact">联系方式{{ posterContact }}</view>
</view>
<!-- 加入条件 -->
<view class="section" v-if="hasJoinCondition">
<view class="title">加入条件</view>
<view class="condition-tip">满足以下条件后可申请成为分销员</view>
<view class="condition-item" v-if="plan.requireSelfPurchaseAmount">
<view class="condition-head">
<text class="condition-label">自购金额</text>
<text class="condition-status" :class="selfPurchaseOk ? 'ok' : 'pending'">
{{ selfPurchaseOk ? '已达标' : '未达标' }}
</text>
</view>
<view class="condition-value">
{{ formatMoney(progress?.currentSelfPurchaseAmount) }} / {{ formatMoney(plan.minSelfPurchaseAmount) }}
</view>
</view>
<view class="condition-item" v-if="plan.requireConsumeCount">
<view class="condition-head">
<text class="condition-label">消费笔数</text>
<text class="condition-status" :class="consumeCountOk ? 'ok' : 'pending'">
{{ consumeCountOk ? '已达标' : '未达标' }}
</text>
</view>
<view class="condition-value">
{{ progress?.currentConsumeCount || 0 }} / {{ plan.minConsumeCount || 0 }}
</view>
</view>
<view class="condition-item" v-if="plan.requirePurchaseGoods">
<view class="condition-head">
<text class="condition-label">购买指定商品</text>
<text class="condition-status" :class="progress?.purchaseGoodsSatisfied ? 'ok' : 'pending'">
{{ progress?.purchaseGoodsSatisfied ? '已达标' : '未达标' }}
</text>
</view>
<view class="goods-list" v-if="recruitGoods.length">
<view class="goods-item" v-for="item in recruitGoods" :key="item.skuId || item.goodsId">
<image class="goods-thumb" :src="item.thumbnail" mode="aspectFill" />
<view class="goods-meta">
<view class="goods-name">{{ item.goodsName }}</view>
<view class="goods-spec" v-if="item.simpleSpecs">{{ item.simpleSpecs }}</view>
<view class="goods-price" v-if="item.price != null">¥{{ formatMoney(item.price) }}</view>
</view>
</view>
</view>
<view class="condition-value" v-else>需购买指定商品中的任意一件</view>
</view>
<view class="summary-status" :class="progress?.satisfied ? 'ok' : 'pending'">
{{ progress?.satisfied ? '已满足全部加入条件' : '未满足加入条件' }}
</view>
</view>
<!-- 申请通过 -->
<view class="section result-section" v-if="isApplyApproved">
<view class="result-icon approved"></view>
<view class="result-title">已成为分销员</view>
<view class="result-desc">恭喜您已通过审核可进入分销员中心开始推广</view>
<view class="submit" @click="goDistributionHome">进入分销员中心</view>
</view>
<!-- 自动申请提示 -->
<view class="section auto-tip" v-else-if="showAutoApplyTip">
<view class="auto-tip-text">您已满足加入条件系统将自动为您申请成为分销员</view>
</view>
</view>
<view class="apply-footer-fixed" v-if="showManualApply || isApplyPending">
<view class="apply-footer-card">
<view class="agree" v-if="showManualApply" @click="agreed = !agreed">
<checkbox :checked="agreed" @click.stop="agreed = !agreed" />
<text>我已阅读并同意</text>
<text class="agreement-link" @click.stop="openAgreement">分销员推广服务协议</text>
</view>
<view
class="submit"
:class="{ disabled: isApplyPending || !canApply }"
@click="submit"
>
{{ isApplyPending ? '等待审核' : '申请成为分销员' }}
</view>
</view>
</view>
<u-popup v-model:show="applyFormVisible" mode="bottom" round="16" :closeable="true">
<view class="apply-form-popup">
<view class="apply-form-header">
<view class="apply-form-title">填写审核信息</view>
<view class="apply-form-subtitle">商家设置了信息审核请填写真实信息</view>
</view>
<view
v-for="field in fields"
:key="field.id"
class="popup-field"
>
<view class="popup-label">
<text v-if="field.required" class="required">*</text>
<text>{{ field.fieldName }}</text>
</view>
<input
v-model="formItems[field.id]"
class="popup-input"
:placeholder="field.placeholder || `请输入${field.fieldName}`"
@input="clearFieldError(field.id)"
/>
<view v-if="fieldErrors[field.id]" class="field-error">{{ fieldErrors[field.id] }}</view>
</view>
<view class="popup-next-btn" @click="confirmApplyForm">下一步</view>
</view>
</u-popup>
<u-popup v-model:show="agreementVisible" mode="bottom" round="16" :closeable="true">
<view class="agreement-popup">
<view class="agreement-popup-title">分销员推广服务协议</view>
<scroll-view scroll-y class="agreement-popup-body">
<rich-text v-if="agreementContent" :nodes="agreementContent" />
<view v-else class="agreement-empty">暂无协议内容</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
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'
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const loading = ref(true)
const pageInfo = ref<any>({})
const progress = ref<any>(null)
const formItems = reactive<Record<string, string>>({})
const agreed = ref(false)
const recruitToken = ref('')
const applySubmitted = ref(false)
const submitAuditMode = ref('')
const submitApplicationStatus = ref('')
const agreementVisible = ref(false)
const applyFormVisible = ref(false)
const formInfoCompleted = ref(false)
const fieldErrors = reactive<Record<string, string>>({})
const plan = computed(() => pageInfo.value.plan || null)
const fields = computed(() => pageInfo.value.fields || [])
const recruitGoods = computed(() => plan.value?.recruitGoods || [])
const agreementContent = computed(() => plan.value?.agreementContent || '')
const showPoster = computed(() => {
const p = plan.value
if (!p) return false
if (p.posterEnabled === false) return false
return !!(p.posterBackground && String(p.posterBackground).trim())
})
const posterTitle = computed(() => plan.value?.posterTitle || '')
const posterBenefitText = computed(() => plan.value?.posterBenefitText || '')
const posterRuleText = computed(() => plan.value?.posterRuleText || '')
const posterContact = computed(() => plan.value?.posterContact || '')
const hasJoinCondition = computed(() => {
const p = plan.value
if (!p || p.joinConditionType !== 'CONDITIONAL') return false
return !!(p.requireSelfPurchaseAmount || p.requireConsumeCount || p.requirePurchaseGoods)
})
const conditionsMet = computed(() => {
if (!hasJoinCondition.value) return true
return !!progress.value?.satisfied
})
const distributionStatus = computed(() => pageInfo.value.distributionStatus || '')
const currentApplication = computed(() => pageInfo.value.currentApplication || null)
const isApplyApproved = computed(() => {
if (distributionStatus.value === 'PASS') return true
if (submitApplicationStatus.value === 'APPROVED') return true
if (applySubmitted.value && submitAuditMode.value === 'AUTO_PASS') return true
return currentApplication.value?.applicationStatus === 'APPROVED'
})
const isApplyPending = computed(() => {
if (isApplyApproved.value) return false
if (distributionStatus.value === 'APPLY') return true
if (submitApplicationStatus.value === 'PENDING') return true
if (applySubmitted.value && submitAuditMode.value === 'MANUAL') return true
return currentApplication.value?.applicationStatus === 'PENDING'
})
const showApplyResult = computed(() => isApplyApproved.value || isApplyPending.value)
const isManualApply = computed(() => {
return (plan.value?.applyMode || 'MANUAL') === 'MANUAL'
})
const showManualApply = computed(() => {
return !showApplyResult.value && conditionsMet.value && isManualApply.value
})
const showAutoApplyTip = computed(() => {
return !showApplyResult.value && conditionsMet.value && plan.value?.applyMode === 'AUTO'
})
const showApplyForm = computed(() => {
return !!plan.value?.requireApplyInfo && fields.value.length > 0
})
const canApply = computed(() => {
return showManualApply.value && agreed.value
})
const selfPurchaseOk = computed(() => {
if (!plan.value?.requireSelfPurchaseAmount) return true
const current = Number(progress.value?.currentSelfPurchaseAmount || 0)
const min = Number(plan.value?.minSelfPurchaseAmount || 0)
return current >= min
})
const consumeCountOk = computed(() => {
if (!plan.value?.requireConsumeCount) return true
const current = Number(progress.value?.currentConsumeCount || 0)
const min = Number(plan.value?.minConsumeCount || 0)
return current >= min
})
onLoad((query) => {
recruitToken.value = query?.token || query?.distributionId || ''
})
onShow(() => {
loadPageData()
})
function loadPageData() {
loading.value = true
applySubmitted.value = false
submitAuditMode.value = ''
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
})
.finally(() => {
loading.value = false
})
}
function formatMoney(val: number | string | undefined) {
return Number(val || 0).toFixed(2)
}
function previewPoster() {
if (!plan.value?.posterBackground) return
uni.previewImage({
urls: [plan.value.posterBackground],
current: plan.value.posterBackground,
})
}
function goDistributionHome() {
uni.redirectTo({
url: '/pages/mine/distribution/home',
})
}
function openAgreement() {
agreementVisible.value = true
}
function clearFieldError(fieldId: string) {
if (fieldErrors[fieldId]) {
delete fieldErrors[fieldId]
}
}
function validateFormFields() {
let valid = true
Object.keys(fieldErrors).forEach((key) => delete fieldErrors[key])
fields.value.forEach((field: any) => {
if (!field.required) return
const value = formItems[field.id]
if (value == null || String(value).trim() === '') {
fieldErrors[field.id] = `请填写${field.fieldName}`
valid = false
}
})
return valid
}
function confirmApplyForm() {
if (!validateFormFields()) {
return
}
formInfoCompleted.value = true
applyFormVisible.value = false
uni.showToast({ title: '请点击申请成为分销员', icon: 'none' })
}
function doSubmit() {
const formItemList = showApplyForm.value
? fields.value.map((field: any) => ({
fieldId: field.id,
fieldValue: formItems[field.id] || '',
}))
: []
submitRecruitApplication({
recruitToken: recruitToken.value,
agreementAccepted: agreed.value,
formItems: formItemList,
}).then((res) => {
if (res.data?.success) {
handleApplySuccess(res.data.result)
if (isApplyApproved.value) {
uni.showToast({ title: '申请成功', icon: 'success' })
} else {
uni.showToast({ title: '已提交,等待审核', icon: 'none' })
}
} else {
uni.showToast({ title: res.data?.message || '提交失败', icon: 'none' })
}
})
}
function handleApplySuccess(application: any) {
const auditMode = application?.auditMode || plan.value?.auditMode || 'MANUAL'
const status = application?.applicationStatus || 'PENDING'
applySubmitted.value = true
submitAuditMode.value = auditMode
submitApplicationStatus.value = status
if (auditMode === 'AUTO_PASS' || status === 'APPROVED') {
pageInfo.value.distributionStatus = 'PASS'
if (pageInfo.value.currentApplication) {
pageInfo.value.currentApplication.applicationStatus = 'APPROVED'
}
} else if (pageInfo.value.currentApplication) {
pageInfo.value.currentApplication.applicationStatus = 'PENDING'
}
}
function submit() {
if (!tipsToLogin('normal')) {
return
}
if (!showManualApply.value) {
return
}
if (!agreed.value) {
uni.showToast({ title: '请先同意分销推广服务协议', icon: 'none' })
return
}
if (showApplyForm.value && !formInfoCompleted.value) {
applyFormVisible.value = true
return
}
doSubmit()
}
</script>
<style lang="scss" scoped>
.wrapper {
min-height: 100vh;
padding: 24rpx;
background: #f5f6f8;
}
.loading,
.empty {
text-align: center;
color: #999;
padding: 80rpx 0;
font-size: 28rpx;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 24rpx;
}
.poster-section {
padding: 0;
overflow: hidden;
}
.poster-image {
display: block;
width: 100%;
border-radius: 16rpx 16rpx 0 0;
background: #f5f5f5;
}
.poster-text {
padding: 24rpx 28rpx 0;
font-size: 34rpx;
font-weight: 600;
color: #222;
line-height: 1.4;
}
.poster-benefit {
padding: 12rpx 28rpx 0;
font-size: 28rpx;
color: #ff6b35;
line-height: 1.5;
}
.poster-rule {
padding: 12rpx 28rpx 0;
font-size: 26rpx;
color: #666;
line-height: 1.6;
white-space: pre-wrap;
}
.poster-contact {
padding: 12rpx 28rpx 24rpx;
font-size: 24rpx;
color: #999;
}
.title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 12rpx;
color: #222;
}
.condition-tip {
font-size: 24rpx;
color: #999;
margin-bottom: 20rpx;
}
.condition-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #f2f2f2;
}
.condition-item:last-of-type {
border-bottom: none;
}
.condition-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.condition-label {
font-size: 28rpx;
color: #333;
}
.condition-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.condition-status.ok {
color: #52c41a;
background: #f6ffed;
}
.condition-status.pending {
color: #999;
background: #f5f5f5;
}
.condition-value {
font-size: 26rpx;
color: #666;
}
.summary-status {
margin-top: 20rpx;
padding: 16rpx 20rpx;
border-radius: 12rpx;
font-size: 26rpx;
text-align: center;
}
.summary-status.ok {
color: #52c41a;
background: #f6ffed;
}
.summary-status.pending {
color: #ff6b35;
background: #fff7f0;
}
.goods-list {
margin-top: 12rpx;
}
.goods-item {
display: flex;
align-items: center;
padding: 16rpx 0;
}
.goods-thumb {
width: 96rpx;
height: 96rpx;
border-radius: 12rpx;
background: #f5f5f5;
margin-right: 16rpx;
}
.goods-meta {
flex: 1;
min-width: 0;
}
.goods-name {
font-size: 26rpx;
color: #333;
line-height: 1.4;
}
.goods-spec,
.goods-price {
margin-top: 6rpx;
font-size: 24rpx;
color: #999;
}
.field {
margin-bottom: 20rpx;
}
.label {
font-size: 26rpx;
margin-bottom: 8rpx;
color: #333;
}
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
.auto-tip {
text-align: center;
}
.auto-tip-text {
font-size: 28rpx;
color: #52c41a;
line-height: 1.6;
}
.result-section {
text-align: center;
padding: 48rpx 28rpx;
}
.result-icon {
width: 96rpx;
height: 96rpx;
margin: 0 auto 24rpx;
border-radius: 50%;
font-size: 48rpx;
line-height: 96rpx;
}
.result-icon.approved {
background: #f6ffed;
color: #52c41a;
}
.result-icon.pending {
background: #fff7e6;
color: #fa8c16;
}
.result-title {
font-size: 36rpx;
font-weight: 600;
color: #222;
margin-bottom: 16rpx;
}
.result-desc {
font-size: 26rpx;
color: #999;
line-height: 1.6;
margin-bottom: 32rpx;
}
.result-section .submit {
margin-top: 0;
}
.page-body.with-footer {
padding-bottom: calc(260rpx + env(safe-area-inset-bottom));
}
.apply-footer-fixed {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
}
.apply-footer-card {
background: #fff;
border-radius: 16rpx 16rpx 0 0;
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.06);
}
.apply-form-popup {
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.apply-form-header {
margin-bottom: 32rpx;
}
.apply-form-title {
font-size: 34rpx;
font-weight: 600;
color: #222;
margin-bottom: 12rpx;
}
.apply-form-subtitle {
font-size: 24rpx;
color: #999;
line-height: 1.5;
}
.popup-field {
margin-bottom: 28rpx;
}
.popup-label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.popup-input {
width: 100%;
height: 88rpx;
padding: 0 24rpx;
box-sizing: border-box;
background: #f7f7f7;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
}
.field-error {
margin-top: 10rpx;
font-size: 24rpx;
color: #ff4d4f;
}
.popup-next-btn {
margin-top: 16rpx;
background: linear-gradient(90deg, #ff8f3f 0%, #ff6b35 100%);
color: #fff;
text-align: center;
padding: 24rpx;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 600;
}
.agree {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 24rpx;
font-size: 24rpx;
color: #666;
}
.agreement-link {
color: $light-color;
}
.agreement-popup {
padding: 32rpx 28rpx 48rpx;
max-height: 70vh;
}
.agreement-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #222;
margin-bottom: 24rpx;
text-align: center;
}
.agreement-popup-body {
max-height: 56vh;
}
.agreement-empty {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 40rpx 0;
}
.submit {
background: $light-color;
color: #fff;
text-align: center;
padding: 24rpx;
border-radius: 40rpx;
font-size: 30rpx;
}
.submit.disabled {
opacity: 0.5;
}
</style>