mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-08-06 10:57:25 +08:00
feat(distribution): 实现分销员中心完整功能
- 新增分销业绩统计页面,包含时间范围筛选和自定义日期选择功能 - 重构分销认证页面,改为跳转至招募页面申请分销员身份 - 添加分销商品访问记录功能,支持分享追踪和佣金计算 - 更新分销历史记录页面,优化数据结构和显示字段 - 完全重写分销员首页,包含用户信息、收益统计、等级系统等功能
This commit is contained in:
@@ -1,10 +1,555 @@
|
||||
<template>
|
||||
<view></view>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="filter-bar">
|
||||
<scroll-view scroll-x class="filter-scroll" :show-scrollbar="false">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="item in rangeTabs"
|
||||
:key="item.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeRange === item.value }"
|
||||
@click="changeRange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="custom-time" :class="{ active: activeRange === 'CUSTOM' }" @click="openCustomPicker">
|
||||
<text>自定义时间</text>
|
||||
<u-icon name="arrow-down" size="12" :color="activeRange === 'CUSTOM' ? '#ff8f3f' : '#999'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="content">
|
||||
<view class="summary-card">
|
||||
<view class="summary-label-row">
|
||||
<text class="summary-label">累计收益</text>
|
||||
<view class="info-icon" @click="showGuide = true">
|
||||
<u-icon name="info-circle" size="14" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="summary-amount">
|
||||
<text class="amount-value">{{ formatMoney(stats.totalEarnings) }}</text>
|
||||
<text class="amount-unit">元</text>
|
||||
</view>
|
||||
<view class="summary-pending">含待结算 {{ formatMoney(stats.pendingEarnings) }}元</view>
|
||||
</view>
|
||||
|
||||
<view class="earn-grid">
|
||||
<view class="earn-item">
|
||||
<view class="earn-title">已结算收益</view>
|
||||
<view class="earn-value">{{ formatMoney(stats.settledEarnings) }}<text class="unit">元</text></view>
|
||||
</view>
|
||||
<view class="earn-item">
|
||||
<view class="earn-title">商品佣金</view>
|
||||
<view class="earn-value">{{ formatMoney(stats.directCommission) }}<text class="unit">元</text></view>
|
||||
<view class="earn-sub">含待结算: {{ formatMoney(stats.directPending) }}元</view>
|
||||
</view>
|
||||
<view class="earn-item">
|
||||
<view class="earn-title">邀请奖励</view>
|
||||
<view class="earn-value">{{ formatMoney(stats.inviteReward) }}<text class="unit">元</text></view>
|
||||
<view class="earn-sub">含待结算: {{ formatMoney(stats.invitePending) }}元</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计销售额(元)</view>
|
||||
<view class="stats-value">{{ formatMoney(stats.totalSales) }}</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计订单(笔)</view>
|
||||
<view class="stats-value">{{ stats.totalOrders || 0 }}</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计客户(人)</view>
|
||||
<view class="stats-value">{{ stats.totalCustomers || 0 }}</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计邀请(人)</view>
|
||||
<view class="stats-value">{{ stats.totalInvites || 0 }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-link" @click="showGuide = true">查看业绩指标说明</view>
|
||||
</view>
|
||||
|
||||
<u-popup v-model:show="customVisible" mode="bottom" round="16">
|
||||
<view class="custom-popup">
|
||||
<view class="popup-title">自定义时间</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">开始日期</text>
|
||||
<picker mode="date" :value="customStartDate" @change="onStartDateChange">
|
||||
<view class="picker-value">{{ customStartDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">结束日期</text>
|
||||
<picker mode="date" :value="customEndDate" @change="onEndDateChange">
|
||||
<view class="picker-value">{{ customEndDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="customVisible = false">取消</view>
|
||||
<view class="popup-btn primary" @click="applyCustomRange">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<u-popup v-model:show="showGuide" mode="center" round="16" :safe-area-inset-bottom="false">
|
||||
<view class="guide-popup">
|
||||
<view class="guide-title">业绩指标说明</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">销售额</view>
|
||||
<view class="guide-desc">仅统计推广订单的销售金额总和</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">客户</view>
|
||||
<view class="guide-desc">与分销员建立绑定关系的买家</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">邀请</view>
|
||||
<view class="guide-desc">分销员成功邀请的下级分销员</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">商品佣金</view>
|
||||
<view class="guide-desc">分销员推荐客户购买商品后获得的佣金</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">邀请奖励</view>
|
||||
<view class="guide-desc">下级分销员推广商品后,上级获得的奖励</view>
|
||||
</view>
|
||||
<view class="guide-btn" @click="showGuide = false">知道了</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 占位页,暂无业务逻辑
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionPerformance } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const rangeTabs = [
|
||||
{ label: '全部', value: 'ALL' },
|
||||
{ label: '今日', value: 'TODAY' },
|
||||
{ label: '昨日', value: 'YESTERDAY' },
|
||||
{ label: '近七日', value: 'LAST_7_DAYS' },
|
||||
]
|
||||
|
||||
const activeRange = ref('ALL')
|
||||
const loading = ref(false)
|
||||
const customVisible = ref(false)
|
||||
const showGuide = ref(false)
|
||||
const customStartDate = ref('')
|
||||
const customEndDate = ref('')
|
||||
|
||||
const stats = ref({
|
||||
totalEarnings: 0,
|
||||
pendingEarnings: 0,
|
||||
settledEarnings: 0,
|
||||
directCommission: 0,
|
||||
directPending: 0,
|
||||
inviteReward: 0,
|
||||
invitePending: 0,
|
||||
totalSales: 0,
|
||||
totalOrders: 0,
|
||||
totalCustomers: 0,
|
||||
totalInvites: 0,
|
||||
})
|
||||
|
||||
const RANGE_VALUES = ['ALL', 'TODAY', 'YESTERDAY', 'LAST_7_DAYS', 'CUSTOM']
|
||||
|
||||
onLoad((options: Record<string, string>) => {
|
||||
if (options?.rangeType && RANGE_VALUES.includes(options.rangeType)) {
|
||||
activeRange.value = options.rangeType
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadPerformance()
|
||||
})
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function buildParams() {
|
||||
const params: Record<string, string> = { rangeType: activeRange.value }
|
||||
if (activeRange.value === 'CUSTOM') {
|
||||
if (customStartDate.value) {
|
||||
params.startTime = `${customStartDate.value} 00:00:00`
|
||||
}
|
||||
if (customEndDate.value) {
|
||||
params.endTime = `${customEndDate.value} 23:59:59`
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function loadPerformance() {
|
||||
loading.value = true
|
||||
getDistributionPerformance(buildParams())
|
||||
.then((res) => {
|
||||
const data = res.data?.result || {}
|
||||
stats.value = {
|
||||
totalEarnings: Number(data.totalEarnings || 0),
|
||||
pendingEarnings: Number(data.pendingEarnings || 0),
|
||||
settledEarnings: Number(data.settledEarnings || 0),
|
||||
directCommission: Number(data.directCommission || 0),
|
||||
directPending: Number(data.directPending || 0),
|
||||
inviteReward: Number(data.inviteReward || 0),
|
||||
invitePending: Number(data.invitePending || 0),
|
||||
totalSales: Number(data.totalSales || 0),
|
||||
totalOrders: Number(data.totalOrders || 0),
|
||||
totalCustomers: Number(data.totalCustomers || 0),
|
||||
totalInvites: Number(data.totalInvites || 0),
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function changeRange(value: string) {
|
||||
if (activeRange.value === value) return
|
||||
activeRange.value = value
|
||||
loadPerformance()
|
||||
}
|
||||
|
||||
function openCustomPicker() {
|
||||
customVisible.value = true
|
||||
}
|
||||
|
||||
function onStartDateChange(e: { detail: { value: string } }) {
|
||||
customStartDate.value = e.detail.value
|
||||
}
|
||||
|
||||
function onEndDateChange(e: { detail: { value: string } }) {
|
||||
customEndDate.value = e.detail.value
|
||||
}
|
||||
|
||||
function applyCustomRange() {
|
||||
if (!customStartDate.value || !customEndDate.value) {
|
||||
uni.showToast({ title: '请选择开始和结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
activeRange.value = 'CUSTOM'
|
||||
customVisible.value = false
|
||||
loadPerformance()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 88rpx;
|
||||
height: 56rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #ff8f3f;
|
||||
background: #fff3ea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-left: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-time.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.summary-card,
|
||||
.earn-grid,
|
||||
.stats-grid {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 32rpx 28rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.summary-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.amount-unit {
|
||||
margin-left: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.summary-pending {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.earn-grid {
|
||||
display: flex;
|
||||
padding: 28rpx 0;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.earn-item {
|
||||
flex: 1;
|
||||
padding: 0 20rpx;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.earn-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.earn-title {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.earn-value {
|
||||
margin-top: 12rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.earn-value .unit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.earn-sub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
padding: 32rpx 28rpx;
|
||||
border-right: 1rpx solid #f0f0f0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stats-item:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.stats-item:nth-last-child(-n + 2) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
margin-top: 16rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.guide-link {
|
||||
margin-top: 40rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-btn.ghost {
|
||||
background: #f5f6f8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.popup-btn.primary {
|
||||
background: linear-gradient(135deg, #ff9f43, #ff7f27);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.guide-popup {
|
||||
width: 620rpx;
|
||||
padding: 40rpx 36rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #222;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.guide-term {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.guide-desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.guide-btn {
|
||||
margin-top: 12rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 42rpx;
|
||||
background: linear-gradient(135deg, #ff9f43, #ff7f27);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,99 +1,24 @@
|
||||
<template>
|
||||
<view class="wrapper">
|
||||
<view>
|
||||
<h4>实名认证(请上传真实的个人信息,认证通过后将无法修改)</h4>
|
||||
<view>
|
||||
<up-form
|
||||
:model="formData"
|
||||
label-position="left"
|
||||
label-width="180rpx"
|
||||
ref="uFormRef"
|
||||
>
|
||||
<up-form-item label="姓名" prop="name">
|
||||
<u-input v-model="formData.name" placeholder="请输入您的真实姓名" />
|
||||
</up-form-item>
|
||||
<up-form-item label="身份证" prop="idNumber">
|
||||
<u-input v-model="formData.idNumber" placeholder="请输入身份证号码" />
|
||||
</up-form-item>
|
||||
<up-form-item label="银行开户行" prop="settlementBankBranchName">
|
||||
<u-input v-model="formData.settlementBankBranchName" placeholder="请输入银行开户行" />
|
||||
</up-form-item>
|
||||
<up-form-item label="银行开户名" prop="settlementBankAccountName">
|
||||
<u-input v-model="formData.settlementBankAccountName" placeholder="请输入银行开户名" />
|
||||
</up-form-item>
|
||||
<up-form-item label="银行账号" prop="settlementBankAccountNum">
|
||||
<u-input v-model="formData.settlementBankAccountNum" placeholder="请输入银行账号" />
|
||||
</up-form-item>
|
||||
</up-form>
|
||||
<u-button :customStyle="{ background: lightColor, color: '#fff' }" @click="submitForm">提交</u-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tips">
|
||||
<view>您提交的信息正在审核</view>
|
||||
<view>提交认证申请后,工作人员将在三个工作日进行核对完成审核</view>
|
||||
<view>分销员申请已改为招募流程</view>
|
||||
<view>请前往招募页面提交申请</view>
|
||||
</view>
|
||||
<u-button :customStyle="{ background: lightColor, color: '#fff', marginTop: '40rpx' }" @click="goJoin">
|
||||
前往招募页
|
||||
</u-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, ref, getCurrentInstance } from 'vue'
|
||||
import { onReady } from '@dcloudio/uni-app'
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { applyDistribution } from '@/api/goods'
|
||||
|
||||
const store = useStore()
|
||||
const { proxy } = getCurrentInstance()!
|
||||
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const uFormRef = ref<any>(null)
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
idNumber: '',
|
||||
settlementBankBranchName: '',
|
||||
settlementBankAccountName: '',
|
||||
settlementBankAccountNum: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入姓名', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: unknown, value: string) => proxy.$u.test.chinese(value),
|
||||
message: '姓名输入不正确',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
settlementBankBranchName: [{ required: true, message: '请输入银行开户行', trigger: 'blur' }],
|
||||
settlementBankAccountName: [{ required: true, message: '银行开户名', trigger: 'blur' }],
|
||||
settlementBankAccountNum: [{ required: true, message: '请输入银行账号', trigger: 'blur' }],
|
||||
idNumber: [
|
||||
{ required: true, message: '请输入身份证', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: unknown, value: string) => proxy.$u.test.idCard(value),
|
||||
message: '身份证号码不正确',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
uFormRef.value?.setRules(rules)
|
||||
})
|
||||
|
||||
function submitForm() {
|
||||
uFormRef.value?.validate().then(() => {
|
||||
applyDistribution(formData).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({ title: '认证提交成功!', duration: 2000, icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 500)
|
||||
} else {
|
||||
uni.showToast({ title: res.data.message, duration: 2000, icon: 'none' })
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
uni.showToast({ title: '请填写有效信息', duration: 2000, icon: 'none' })
|
||||
})
|
||||
function goJoin() {
|
||||
uni.redirectTo({ url: '/pages/mine/distribution/join' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -102,8 +27,8 @@ function submitForm() {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.tips {
|
||||
margin-top: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
44
pages/mine/distribution/bind.vue
Normal file
44
pages/mine/distribution/bind.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<view class="bind-page"></view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import storage from '@/utils/storage'
|
||||
import { getGoodsDistribution } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
onLoad(async (options) => {
|
||||
let distributionId = (options?.distributionId as string) || ''
|
||||
|
||||
if (options?.scene) {
|
||||
const scene = decodeURIComponent(String(options.scene))
|
||||
const parts = scene.split(',')
|
||||
if (parts[0] === 'bind' && parts[1]) {
|
||||
distributionId = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if (distributionId) {
|
||||
store.state.distributionId = distributionId
|
||||
if (storage.getAccessToken()) {
|
||||
try {
|
||||
await getGoodsDistribution(distributionId)
|
||||
} catch (error) {
|
||||
console.warn('bind distribution failed', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bind-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
833
pages/mine/distribution/customer-list.vue
Normal file
833
pages/mine/distribution/customer-list.vue
Normal file
@@ -0,0 +1,833 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<u-icon name="arrow-left" color="#333" size="20" />
|
||||
</view>
|
||||
<view v-if="detailId" class="nav-title">客户详情</view>
|
||||
<view v-else class="search-box">
|
||||
<u-icon name="search" color="#bbb" size="16" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="请输入手机号或有效客户昵称搜索"
|
||||
placeholder-class="search-placeholder"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-if="detailId">
|
||||
<view v-if="detailLoading" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!detail.id" class="state-wrap">
|
||||
<text class="state-text">客户不存在</text>
|
||||
</view>
|
||||
<view v-else class="detail-wrap">
|
||||
<view class="detail-card">
|
||||
<view class="profile-row">
|
||||
<image class="detail-avatar" :src="resolveAvatar(detail.memberAvatar)" mode="aspectFill" />
|
||||
<view class="profile-info">
|
||||
<view class="detail-name-row">
|
||||
<text class="detail-name">{{ detail.memberNickname || '客户' }}</text>
|
||||
<text class="star-icon" :class="{ active: detail.starred }">★</text>
|
||||
</view>
|
||||
<view class="star-action" @click="toggleStar">
|
||||
{{ detail.starred ? '取消星标' : '设为星标' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<view class="stats-value">{{ detail.orderCount || 0 }}</view>
|
||||
<view class="stats-label">订单数量</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-value">{{ formatMoney(detail.tradeAmount) }}</view>
|
||||
<view class="stats-label">成交金额(元)</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-value">{{ formatMoney(detail.averageOrderAmount) }}</view>
|
||||
<view class="stats-label">客单价(元)</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-value stats-time">{{ formatTime(detail.lastTradeTime) }}</view>
|
||||
<view class="stats-label">最后成交时间</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="main-tabs">
|
||||
<view
|
||||
v-for="item in mainTabs"
|
||||
:key="item.value"
|
||||
class="main-tab"
|
||||
:class="{ active: activeFilter === item.value }"
|
||||
@click="changeFilter(item.value)"
|
||||
>
|
||||
<text>{{ item.label }}</text>
|
||||
<view v-if="activeFilter === item.value" class="tab-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-bar">
|
||||
<scroll-view scroll-x class="filter-scroll" :show-scrollbar="false">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="item in rangeTabs"
|
||||
:key="item.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeRange === item.value }"
|
||||
@click="changeRange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab custom-time"
|
||||
:class="{ active: activeRange === 'CUSTOM' }"
|
||||
@click="openCustomPicker"
|
||||
>
|
||||
<text>自定义时间</text>
|
||||
<u-icon name="arrow-down" size="12" :color="activeRange === 'CUSTOM' ? '#ff8f3f' : '#999'" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="summary-row">
|
||||
<text>共{{ customerTotal }}个客户</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !customerList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!customerList.length" class="state-wrap">
|
||||
<text class="state-text">暂无客户</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="list-wrap">
|
||||
<view class="customer-card" v-for="item in customerList" :key="item.id" @click="openDetail(item)">
|
||||
<image class="avatar" :src="resolveAvatar(item.memberAvatar)" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<view class="name-row">
|
||||
<text class="name">{{ item.memberNickname || '客户' }}</text>
|
||||
<u-icon
|
||||
v-if="item.starred"
|
||||
name="star-fill"
|
||||
color="#ff8f3f"
|
||||
size="16"
|
||||
/>
|
||||
</view>
|
||||
<view class="meta-line phone-status-row">
|
||||
<view class="phone-wrap">
|
||||
<text class="meta-label">手机号:</text>
|
||||
<text class="meta-value">{{ item.memberMobile || '-' }}</text>
|
||||
</view>
|
||||
<view class="status-wrap">
|
||||
<text class="meta-label">状态:</text>
|
||||
<text class="status-text" :class="{ effective: item.relationEffective }">
|
||||
{{ item.relationStatusText || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bind-time">绑定时间:{{ formatTime(item.bindTime) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="customerList.length" class="list-footer">
|
||||
<text v-if="finished" class="footer-text">没有更多数据了</text>
|
||||
<text v-else-if="loadingMore" class="footer-text">加载中...</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<u-popup v-model:show="customVisible" mode="bottom" round="16">
|
||||
<view class="custom-popup">
|
||||
<view class="popup-title">自定义时间</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">开始日期</text>
|
||||
<picker mode="date" :value="customStartDate" @change="onStartDateChange">
|
||||
<view class="picker-value">{{ customStartDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">结束日期</text>
|
||||
<picker mode="date" :value="customEndDate" @change="onEndDateChange">
|
||||
<view class="picker-value">{{ customEndDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="customVisible = false">取消</view>
|
||||
<view class="popup-btn primary" @click="applyCustomRange">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onReachBottom, onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import config from '@/config/config'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import {
|
||||
getDistributionCustomers,
|
||||
getDistributionCustomerDetail,
|
||||
updateDistributionCustomerStarred,
|
||||
} from '@/api/distribution'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const defaultAvatar = config.defaultUserPhoto
|
||||
const statusBarHeight = ref(20)
|
||||
|
||||
const mainTabs = [
|
||||
{ label: '全部客户', value: 'ALL' },
|
||||
{ label: '星标客户', value: 'STARRED' },
|
||||
{ label: '下单客户', value: 'ORDERED' },
|
||||
{ label: '即将失效客户', value: 'EXPIRING_SOON' },
|
||||
]
|
||||
|
||||
const rangeTabs = [
|
||||
{ label: '全部', value: 'ALL' },
|
||||
{ label: '今日', value: 'TODAY' },
|
||||
{ label: '昨日', value: 'YESTERDAY' },
|
||||
{ label: '近七日', value: 'LAST_7_DAYS' },
|
||||
]
|
||||
|
||||
const activeFilter = ref('ALL')
|
||||
const activeRange = ref('ALL')
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const finished = ref(false)
|
||||
const customerTotal = ref(0)
|
||||
const customerList = ref<any[]>([])
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const detailId = ref('')
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref<Record<string, any>>({})
|
||||
|
||||
const customVisible = ref(false)
|
||||
const customStartDate = ref('')
|
||||
const customEndDate = ref('')
|
||||
|
||||
const RANGE_VALUES = ['ALL', 'TODAY', 'YESTERDAY', 'LAST_7_DAYS', 'CUSTOM']
|
||||
|
||||
function applyRouteOptions(options: Record<string, string | undefined> = {}) {
|
||||
const rangeType = options.rangeType
|
||||
if (rangeType && RANGE_VALUES.includes(rangeType)) {
|
||||
activeRange.value = rangeType
|
||||
return
|
||||
}
|
||||
activeRange.value = 'ALL'
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
applyRouteOptions(options as Record<string, string>)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
statusBarHeight.value = uni.getWindowInfo?.()?.statusBarHeight || 20
|
||||
const pages = getCurrentPages()
|
||||
const current = pages[pages.length - 1] as { options?: Record<string, string> }
|
||||
if (current?.options) {
|
||||
applyRouteOptions(current.options)
|
||||
}
|
||||
if (!detailId.value) {
|
||||
resetAndLoad()
|
||||
}
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!detailId.value) {
|
||||
loadCustomers(false)
|
||||
}
|
||||
})
|
||||
|
||||
function buildQueryParams() {
|
||||
const params: Record<string, any> = {
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize,
|
||||
filterType: activeFilter.value,
|
||||
rangeType: activeRange.value,
|
||||
}
|
||||
const searchKeyword = keyword.value.trim()
|
||||
if (searchKeyword) {
|
||||
params.keyword = searchKeyword
|
||||
}
|
||||
if (activeRange.value === 'CUSTOM') {
|
||||
if (customStartDate.value) {
|
||||
params.startTime = `${customStartDate.value} 00:00:00`
|
||||
}
|
||||
if (customEndDate.value) {
|
||||
params.endTime = `${customEndDate.value} 23:59:59`
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function resetAndLoad() {
|
||||
pageNumber.value = 1
|
||||
finished.value = false
|
||||
customerList.value = []
|
||||
loadCustomers(true)
|
||||
}
|
||||
|
||||
function loadCustomers(reset = false) {
|
||||
if (reset) {
|
||||
loading.value = true
|
||||
pageNumber.value = 1
|
||||
finished.value = false
|
||||
} else if (loading.value || loadingMore.value || finished.value) {
|
||||
return
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
getDistributionCustomers(buildQueryParams())
|
||||
.then((res) => {
|
||||
const result = res.data?.result || {}
|
||||
const records = result.records || []
|
||||
customerTotal.value = Number(result.total || 0)
|
||||
if (reset) {
|
||||
customerList.value = records
|
||||
} else {
|
||||
customerList.value = customerList.value.concat(records)
|
||||
}
|
||||
if (records.length < pageSize || customerList.value.length >= customerTotal.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
pageNumber.value += 1
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadDetail() {
|
||||
if (!detailId.value) return
|
||||
detailLoading.value = true
|
||||
getDistributionCustomerDetail(detailId.value)
|
||||
.then((res) => {
|
||||
detail.value = res.data?.result || {}
|
||||
})
|
||||
.catch(() => {
|
||||
detail.value = {}
|
||||
})
|
||||
.finally(() => {
|
||||
detailLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function openDetail(item: any) {
|
||||
if (!item?.id) return
|
||||
detailId.value = item.id
|
||||
detail.value = {}
|
||||
loadDetail()
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
const currentId = detailId.value
|
||||
const starred = detail.value?.starred
|
||||
detailId.value = ''
|
||||
detail.value = {}
|
||||
if (currentId) {
|
||||
const target = customerList.value.find((item) => item.id === currentId)
|
||||
if (target) {
|
||||
target.starred = starred
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStar() {
|
||||
if (!detail.value?.id) return
|
||||
const next = !detail.value.starred
|
||||
updateDistributionCustomerStarred(detail.value.id, next).then(() => {
|
||||
detail.value.starred = next
|
||||
const target = customerList.value.find((item) => item.id === detail.value.id)
|
||||
if (target) {
|
||||
target.starred = next
|
||||
}
|
||||
uni.showToast({
|
||||
title: next ? '已设为星标' : '已取消星标',
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function changeFilter(value: string) {
|
||||
if (activeFilter.value === value) return
|
||||
activeFilter.value = value
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function changeRange(value: string) {
|
||||
if (activeRange.value === value) return
|
||||
activeRange.value = value
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function openCustomPicker() {
|
||||
customVisible.value = true
|
||||
}
|
||||
|
||||
function onStartDateChange(event: any) {
|
||||
customStartDate.value = event.detail.value
|
||||
}
|
||||
|
||||
function onEndDateChange(event: any) {
|
||||
customEndDate.value = event.detail.value
|
||||
}
|
||||
|
||||
function applyCustomRange() {
|
||||
if (!customStartDate.value || !customEndDate.value) {
|
||||
uni.showToast({ title: '请选择开始和结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (customStartDate.value > customEndDate.value) {
|
||||
uni.showToast({ title: '开始日期不能晚于结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
activeRange.value = 'CUSTOM'
|
||||
customVisible.value = false
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function resolveAvatar(avatar?: string) {
|
||||
return parseGoodsImageUrl(avatar) || defaultAvatar
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return '-'
|
||||
return String(value).replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (detailId.value) {
|
||||
closeDetail()
|
||||
return
|
||||
}
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages/mine/distribution/home' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.status-bar,
|
||||
.nav-bar {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 24rpx 16rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin-right: 56rpx;
|
||||
color: #222;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #bbb;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.main-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 12rpx 12rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.main-tab {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx 0 24rpx;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.main-tab.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 8rpx;
|
||||
width: 48rpx;
|
||||
height: 6rpx;
|
||||
margin-left: -24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
padding: 28rpx 0 16rpx;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
gap: 28rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
padding: 20rpx 24rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.list-wrap,
|
||||
.detail-wrap {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.customer-card,
|
||||
.detail-card {
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.customer-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 32rpx 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.detail-avatar {
|
||||
border-radius: 50%;
|
||||
background: #f2f2f2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.detail-avatar {
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
}
|
||||
|
||||
.info,
|
||||
.profile-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.name-row,
|
||||
.detail-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.name,
|
||||
.detail-name {
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.name {
|
||||
max-width: 420rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
max-width: 420rpx;
|
||||
margin-right: 10rpx;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
color: #ccc;
|
||||
font-size: 30rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.star-icon.active {
|
||||
color: #ff8f3f;
|
||||
}
|
||||
|
||||
.star-action {
|
||||
display: inline-flex;
|
||||
margin-top: 16rpx;
|
||||
padding: 8rpx 22rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 999rpx;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border-top: 1rpx solid #f2f2f2;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
width: 50%;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 8rpx 20rpx;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
color: #222;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.stats-time {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
margin-top: 10rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.phone-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.phone-wrap,
|
||||
.status-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.status-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.status-text.effective {
|
||||
color: #22c55e;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bind-time {
|
||||
margin-top: 12rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
padding: 12rpx 0 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
padding: 32rpx 28rpx 40rpx;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-btn.ghost {
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-btn.primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
}
|
||||
</style>
|
||||
611
pages/mine/distribution/grade.vue
Normal file
611
pages/mine/distribution/grade.vue
Normal file
@@ -0,0 +1,611 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!gradeList.length" class="empty-wrap">
|
||||
<text class="empty-text">暂无等级信息</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="content">
|
||||
<!-- 当前等级头部 -->
|
||||
<view class="hero-card">
|
||||
<view class="hero-top">
|
||||
<view class="hero-user">
|
||||
<image class="hero-avatar" :src="userInfo.face || defaultAvatar" mode="aspectFill" />
|
||||
<view class="hero-name">{{ currentGradeName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="progress-wrap">
|
||||
<view class="progress-line" v-if="gradeList.length > 1"></view>
|
||||
<view class="progress-steps">
|
||||
<view
|
||||
v-for="grade in gradeList"
|
||||
:key="grade.id || grade.sortOrder"
|
||||
class="progress-step"
|
||||
:class="{ active: Number(grade.sortOrder) === currentGradeValue }"
|
||||
>
|
||||
<view class="step-dot"></view>
|
||||
<view class="step-name">V{{ grade.sortOrder }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 佣金比例 -->
|
||||
<view class="card commission-card" v-if="currentGrade">
|
||||
<view class="commission-row">
|
||||
<view class="commission-item">
|
||||
<view class="commission-icon">推</view>
|
||||
<view class="commission-text">
|
||||
<text class="commission-rate">{{ formatRate(currentGrade.goodsCommissionRate) }}%</text>
|
||||
<text class="commission-label">商品佣金比</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="commission-divider"></view>
|
||||
<view class="commission-item">
|
||||
<view class="commission-icon">邀</view>
|
||||
<view class="commission-text">
|
||||
<text class="commission-rate">{{ formatRate(currentGrade.inviteCommissionRate) }}%</text>
|
||||
<text class="commission-label">邀请佣金比</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下一等级升级要求 -->
|
||||
<view class="card upgrade-card" v-if="distributionData.id && nextGrade">
|
||||
<view class="upgrade-title">
|
||||
满足以下规则可升级为<text class="upgrade-target">{{ nextGrade.gradeName }}</text>
|
||||
</view>
|
||||
<view class="upgrade-list" v-if="upgradeConditions.length">
|
||||
<view
|
||||
v-for="(item, index) in upgradeConditions"
|
||||
:key="index"
|
||||
class="upgrade-item"
|
||||
>
|
||||
<view class="upgrade-status" :class="item.achieved ? 'achieved' : 'pending'">
|
||||
{{ item.achieved ? '已达标' : '未达标' }}
|
||||
</view>
|
||||
<view class="upgrade-text">
|
||||
<text>{{ item.label }}</text>
|
||||
<text v-if="!item.achieved && item.gapText" class="upgrade-gap">{{ item.gapText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="upgrade-empty">暂无升级条件配置</view>
|
||||
</view>
|
||||
|
||||
<view class="card upgrade-card" v-else-if="distributionData.id">
|
||||
<view class="upgrade-title">当前已是最高等级</view>
|
||||
<view class="upgrade-empty">继续保持优秀业绩吧</view>
|
||||
</view>
|
||||
|
||||
<!-- 等级规则 -->
|
||||
<view class="card rules-card">
|
||||
<view class="rules-title">等级规则</view>
|
||||
<view class="rules-timeline">
|
||||
<view
|
||||
v-for="(grade, index) in gradeList"
|
||||
:key="'rule-' + (grade.id || grade.sortOrder)"
|
||||
class="timeline-item"
|
||||
>
|
||||
<view class="timeline-axis">
|
||||
<view class="timeline-dot"></view>
|
||||
<view v-if="index < gradeList.length - 1" class="timeline-line"></view>
|
||||
</view>
|
||||
<view class="timeline-content">
|
||||
<view class="grade-tag">{{ grade.gradeName }}</view>
|
||||
<view class="rule-section">
|
||||
<view class="rule-section-title">规则介绍</view>
|
||||
<view class="rule-section-text">{{ buildGradeRule(grade) }}</view>
|
||||
</view>
|
||||
<view class="rule-section">
|
||||
<view class="rule-section-title">权益介绍</view>
|
||||
<view class="rule-section-text">{{ buildGradeBenefit(grade) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import config from '@/config/config'
|
||||
import { distribution, getDistributionGrades } from '@/api/distribution'
|
||||
import { getUserInfo } from '@/api/members'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const defaultAvatar = config.defaultUserPhoto
|
||||
const loading = ref(true)
|
||||
const distributionData = ref<Record<string, any>>({})
|
||||
const gradeList = ref<any[]>([])
|
||||
|
||||
const userInfo = computed(() => store.state.userInfo || {})
|
||||
|
||||
const currentGradeValue = computed(() => {
|
||||
const val = Number(distributionData.value.gradeId)
|
||||
return Number.isFinite(val) && val > 0 ? val : 1
|
||||
})
|
||||
|
||||
const currentGrade = computed(() => {
|
||||
return gradeList.value.find((g) => Number(g.sortOrder) === currentGradeValue.value) || null
|
||||
})
|
||||
|
||||
const currentGradeName = computed(() => currentGrade.value?.gradeName || '普通分销员')
|
||||
|
||||
const nextGrade = computed(() => {
|
||||
return gradeList.value.find((g) => Number(g.sortOrder) === currentGradeValue.value + 1) || null
|
||||
})
|
||||
|
||||
const upgradeConditions = computed(() => {
|
||||
if (!nextGrade.value) return []
|
||||
return buildUpgradeConditions(nextGrade.value, distributionData.value)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
refreshUserInfo()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
function refreshUserInfo() {
|
||||
getUserInfo().then((res) => {
|
||||
if (res.data?.result) {
|
||||
store.commit('login', res.data.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadPageData() {
|
||||
loading.value = true
|
||||
Promise.all([fetchGrades(), fetchDistribution()]).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function fetchDistribution() {
|
||||
return distribution().then((res) => {
|
||||
if (!res.data?.result) {
|
||||
return
|
||||
}
|
||||
distributionData.value = res.data.result
|
||||
})
|
||||
}
|
||||
|
||||
function fetchGrades() {
|
||||
return getDistributionGrades()
|
||||
.then((res) => {
|
||||
const result = res.data?.result
|
||||
gradeList.value = (Array.isArray(result) ? result : []).sort(
|
||||
(a: any, b: any) => Number(a.sortOrder) - Number(b.sortOrder)
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
gradeList.value = []
|
||||
})
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatRate(val: number | string | undefined) {
|
||||
const num = Number(val || 0)
|
||||
return Number.isFinite(num) ? num.toFixed(2).replace(/\.?0+$/, '') || '0' : '0'
|
||||
}
|
||||
|
||||
function buildGradeRule(grade: any) {
|
||||
if (Number(grade.sortOrder) === 1) {
|
||||
return '成为分销员后即是该等级'
|
||||
}
|
||||
const parts: string[] = []
|
||||
if (grade.enableSalesCondition && Number(grade.salesThreshold || 0) > 0) {
|
||||
parts.push(`推广金额达 ${formatMoney(grade.salesThreshold)} 元`)
|
||||
}
|
||||
if (grade.enableCommissionCondition && Number(grade.commissionThreshold || 0) > 0) {
|
||||
parts.push(`收益额达 ${formatMoney(grade.commissionThreshold)} 元`)
|
||||
}
|
||||
if (grade.enableInviteCondition && Number(grade.inviteThreshold || 0) > 0) {
|
||||
parts.push(`邀请人数达 ${grade.inviteThreshold} 人`)
|
||||
}
|
||||
if (!parts.length) return '达到指定条件即可升级'
|
||||
return parts.join(',且')
|
||||
}
|
||||
|
||||
function buildGradeBenefit(grade: any) {
|
||||
const parts: string[] = []
|
||||
const goods = Number(grade.goodsCommissionRate || 0)
|
||||
const invite = Number(grade.inviteCommissionRate || 0)
|
||||
if (goods > 0) parts.push(`商品佣金比为 ${formatRate(goods)}%`)
|
||||
if (invite > 0) parts.push(`邀请佣金比为 ${formatRate(invite)}%`)
|
||||
return parts.length ? parts.join(';') : '暂无权益说明'
|
||||
}
|
||||
|
||||
function buildUpgradeConditions(grade: any, data: Record<string, any>) {
|
||||
const conditions: Array<{ label: string; achieved: boolean; gapText?: string }> = []
|
||||
const sales = Number(data.validSalesAmount || 0)
|
||||
const commission = Number(data.validCommissionAmount || data.rebateTotal || 0)
|
||||
const invites = Number(data.inviteCount || 0)
|
||||
|
||||
if (grade.enableSalesCondition) {
|
||||
const threshold = Number(grade.salesThreshold || 0)
|
||||
const achieved = sales >= threshold
|
||||
conditions.push({
|
||||
label: `推广金额达${formatMoney(threshold)}元`,
|
||||
achieved,
|
||||
gapText: achieved ? undefined : `(还差 ${formatMoney(Math.max(threshold - sales, 0))} 元)`,
|
||||
})
|
||||
}
|
||||
|
||||
if (grade.enableCommissionCondition) {
|
||||
const threshold = Number(grade.commissionThreshold || 0)
|
||||
const achieved = commission >= threshold
|
||||
conditions.push({
|
||||
label: `收益额达${formatMoney(threshold)}元`,
|
||||
achieved,
|
||||
gapText: achieved ? undefined : `(还差 ${formatMoney(Math.max(threshold - commission, 0))} 元)`,
|
||||
})
|
||||
}
|
||||
|
||||
if (grade.enableInviteCondition) {
|
||||
const threshold = Number(grade.inviteThreshold || 0)
|
||||
const achieved = invites >= threshold
|
||||
conditions.push({
|
||||
label: `邀请人数达${threshold}人`,
|
||||
achieved,
|
||||
gapText: achieved ? undefined : `(还差 ${Math.max(threshold - invites, 0)} 人)`,
|
||||
})
|
||||
}
|
||||
|
||||
return conditions
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.loading-wrap,
|
||||
.empty-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 40rpx 32rpx 48rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #d4a574 0%, #c9956a 45%, #b8845a 100%);
|
||||
box-shadow: 0 12rpx 32rpx rgba(184, 132, 90, 0.25);
|
||||
}
|
||||
|
||||
.hero-top {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.hero-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.5);
|
||||
margin-right: 24rpx;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
right: 12rpx;
|
||||
top: 12rpx;
|
||||
height: 4rpx;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.progress-step:first-child {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.progress-step:last-child {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.progress-step:first-child:last-child {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.progress-step.active .step-dot {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 6rpx rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.step-name {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
padding: 0 6rpx;
|
||||
}
|
||||
|
||||
.progress-step.active .step-name {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.commission-card {
|
||||
padding: 36rpx 28rpx 28rpx;
|
||||
}
|
||||
|
||||
.commission-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.commission-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.commission-divider {
|
||||
flex-shrink: 0;
|
||||
width: 1rpx;
|
||||
height: 72rpx;
|
||||
margin: 0 16rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.commission-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: #f8efe6;
|
||||
color: #b8845a;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.commission-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.commission-rate {
|
||||
color: #222;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.commission-label {
|
||||
margin-top: 8rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.upgrade-title {
|
||||
color: #5c3b00;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.upgrade-target {
|
||||
color: #ff6b35;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upgrade-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.upgrade-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.upgrade-status {
|
||||
flex-shrink: 0;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.upgrade-status.achieved {
|
||||
color: #52c41a;
|
||||
background: #f6ffed;
|
||||
}
|
||||
|
||||
.upgrade-status.pending {
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.upgrade-text {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.upgrade-gap {
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.upgrade-empty {
|
||||
margin-top: 16rpx;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.rules-title {
|
||||
margin-bottom: 28rpx;
|
||||
color: #5c3b00;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rules-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.timeline-axis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
flex-shrink: 0;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
margin-top: 18rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
flex: 1;
|
||||
width: 2rpx;
|
||||
min-height: 40rpx;
|
||||
margin: 8rpx 0;
|
||||
background: #f0d4b8;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.timeline-item:last-child .timeline-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.grade-tag {
|
||||
display: inline-block;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f8efe6;
|
||||
color: #5c3b00;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.rule-section {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.rule-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rule-section-title {
|
||||
margin-bottom: 8rpx;
|
||||
color: #5c3b00;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rule-section-text {
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -32,11 +32,10 @@
|
||||
<view class="log-item">
|
||||
<view class="log-item-view">
|
||||
<view class="title">{{ item.goodsName }}</view>
|
||||
<view class="price">提成金额:+{{ unitPrice(item.rebate) }}</view>
|
||||
<view class="price">提成金额:+{{ unitPrice(item.commissionAmount) }}</view>
|
||||
</view>
|
||||
<view class="log-item-view">
|
||||
<view>创建时间:{{ item.createTime }}</view>
|
||||
<view>店铺:{{ item.storeName }}</view>
|
||||
</view>
|
||||
<view class="log-item-footer">
|
||||
<view>会员名称:{{ item.memberName }}</view>
|
||||
@@ -57,7 +56,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { cashLog, distributionOrderList } from '@/api/goods'
|
||||
import { cashLog, getDistributionOrders } from '@/api/distribution'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
|
||||
const store = useStore()
|
||||
@@ -71,7 +70,7 @@ const listType = ref(0)
|
||||
const routeQuery = ref<Record<string, string>>({})
|
||||
|
||||
const withdrawParams = ref({ pageNumber: 1, pageSize: 10 })
|
||||
const achievementParams = ref({ pageNumber: 1, pageSize: 10 })
|
||||
const achievementParams = ref({ pageNumber: 1, pageSize: 10, orderType: 'PROMOTION' })
|
||||
|
||||
onLoad((option) => {
|
||||
const type = Number(option.type != null ? option.type : 0)
|
||||
@@ -100,12 +99,16 @@ function hideLoadingIfNeeded() {
|
||||
|
||||
function fetchAchievementList() {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
distributionOrderList(achievementParams.value).then((res) => {
|
||||
if (res.data.success && res.data.result.records.length >= 1) {
|
||||
achievementList.value.push(...res.data.result.records)
|
||||
getDistributionOrders(achievementParams.value).then((res) => {
|
||||
const records = res.data.success ? res.data.result?.records || [] : []
|
||||
if (records.length) {
|
||||
achievementList.value.push(...records)
|
||||
if (records.length < achievementParams.value.pageSize) {
|
||||
loadStatus.value = 'nomore'
|
||||
}
|
||||
} else {
|
||||
loadStatus.value = 'nomore'
|
||||
isEmpty.value = true
|
||||
isEmpty.value = achievementList.value.length === 0
|
||||
}
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
329
pages/mine/distribution/invite-friends.vue
Normal file
329
pages/mine/distribution/invite-friends.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="hero">
|
||||
<view class="hero-title">邀好友 赚奖励</view>
|
||||
</view>
|
||||
|
||||
<view class="flow-card">
|
||||
<view class="flow-list">
|
||||
<view class="flow-item" v-for="(item, index) in flowSteps" :key="item.title">
|
||||
<view class="flow-icon-wrap" :style="{ background: item.bg }">
|
||||
<u-icon :name="item.icon" color="#fff" size="22" />
|
||||
</view>
|
||||
<view class="flow-text">
|
||||
<text class="flow-title">{{ item.title }}</text>
|
||||
<text class="flow-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
<view v-if="index < flowSteps.length - 1" class="flow-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-btn" @click="goInviteCard">图文邀请卡</view>
|
||||
</view>
|
||||
|
||||
<view class="invite-card">
|
||||
<view class="invite-title">
|
||||
<text class="invite-title-line">—</text>
|
||||
<text class="invite-title-text">我已邀请{{ inviteTotal }}人</text>
|
||||
<text class="invite-title-line">—</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !inviteList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!inviteList.length" class="state-wrap">
|
||||
<text class="state-text">还没有邀请好友,快去分享邀请卡吧</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="invite-list">
|
||||
<view class="invite-item" v-for="item in inviteList" :key="item.childDistributionId">
|
||||
<image
|
||||
class="invite-avatar"
|
||||
:src="resolveAvatar(item.memberAvatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="invite-name">{{ displayInviteName(item) }}</view>
|
||||
<view class="invite-reward">
|
||||
获得
|
||||
<text class="reward-value">{{ formatMoney(item.inviteRewardAmount) }}</text>
|
||||
元
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="view-all" @click="goInviteList">查看我的邀请</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import config from '@/config/config'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionInvitees } from '@/api/distribution'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const defaultAvatar = config.defaultUserPhoto
|
||||
|
||||
const flowSteps = [
|
||||
{
|
||||
icon: 'account-fill',
|
||||
bg: 'linear-gradient(135deg, #ff9a8b, #ff6a88)',
|
||||
title: '邀请好友',
|
||||
desc: '成为分销员',
|
||||
},
|
||||
{
|
||||
icon: 'bag-fill',
|
||||
bg: 'linear-gradient(135deg, #84fab0, #8fd3f4)',
|
||||
title: '好友客户',
|
||||
desc: '下单购物',
|
||||
},
|
||||
{
|
||||
icon: 'red-packet-fill',
|
||||
bg: 'linear-gradient(135deg, #fccb90, #ff8f3f)',
|
||||
title: '邀请者获得',
|
||||
desc: '邀请奖励',
|
||||
},
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const inviteTotal = ref(0)
|
||||
const inviteList = ref<any[]>([])
|
||||
|
||||
onShow(() => {
|
||||
loadPreview()
|
||||
})
|
||||
|
||||
function loadPreview() {
|
||||
loading.value = true
|
||||
getDistributionInvitees({
|
||||
pageNumber: 1,
|
||||
pageSize: 5,
|
||||
rangeType: 'ALL',
|
||||
})
|
||||
.then((res) => {
|
||||
const result = res.data?.result || {}
|
||||
inviteTotal.value = Number(result.total || 0)
|
||||
inviteList.value = result.records || []
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function resolveAvatar(avatar?: string) {
|
||||
return parseGoodsImageUrl(avatar) || defaultAvatar
|
||||
}
|
||||
|
||||
function displayInviteName(item: any) {
|
||||
const gradeName = (item.gradeName || '').trim()
|
||||
const memberName = (item.memberName || '分销员').trim()
|
||||
if (gradeName && !memberName.startsWith(gradeName)) {
|
||||
return `${gradeName}${memberName}`
|
||||
}
|
||||
return memberName
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function goInviteCard() {
|
||||
uni.navigateTo({ url: '/pages/mine/distribution/invite' })
|
||||
}
|
||||
|
||||
function goInviteList() {
|
||||
uni.navigateTo({ url: '/pages/mine/distribution/invite-list' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #fff7f0;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 56rpx 40rpx 96rpx;
|
||||
background: linear-gradient(135deg, #ffb347 0%, #ff8f3f 45%, #ff6b35 100%);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #fff;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.flow-card {
|
||||
margin: -64rpx 24rpx 0;
|
||||
padding: 36rpx 28rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 53, 0.12);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.flow-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flow-icon-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flow-text {
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
display: block;
|
||||
color: #333;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.flow-desc {
|
||||
display: block;
|
||||
color: #999;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
position: absolute;
|
||||
top: 44rpx;
|
||||
right: -20rpx;
|
||||
width: 40rpx;
|
||||
height: 2rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
margin-top: 36rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.invite-card {
|
||||
margin: 24rpx;
|
||||
padding: 28rpx 24rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.invite-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.invite-title-line,
|
||||
.invite-title-text {
|
||||
color: #ff8f3f;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 48rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.invite-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.invite-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.invite-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: #f2f2f2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.invite-name {
|
||||
flex: 1;
|
||||
margin: 0 20rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.invite-reward {
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
color: #ff6b35;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
margin-top: 28rpx;
|
||||
text-align: center;
|
||||
color: #ff8f3f;
|
||||
font-size: 28rpx;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
486
pages/mine/distribution/invite-list.vue
Normal file
486
pages/mine/distribution/invite-list.vue
Normal file
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="filter-bar">
|
||||
<scroll-view scroll-x class="filter-scroll" :show-scrollbar="false">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="item in rangeTabs"
|
||||
:key="item.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeRange === item.value }"
|
||||
@click="changeRange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="custom-time" :class="{ active: activeRange === 'CUSTOM' }" @click="openCustomPicker">
|
||||
<text>自定义时间</text>
|
||||
<u-icon name="arrow-down" size="12" :color="activeRange === 'CUSTOM' ? '#ff8f3f' : '#999'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="summary-row">
|
||||
<text>共 {{ inviteTotal }} 个邀请</text>
|
||||
<text>获得邀请奖励 {{ formatMoney(totalInviteReward) }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !inviteList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!inviteList.length" class="state-wrap">
|
||||
<text class="state-text">暂无邀请记录</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="list-wrap">
|
||||
<view class="invite-card" v-for="item in inviteList" :key="item.childDistributionId">
|
||||
<view class="card-top">
|
||||
<image class="avatar" :src="resolveAvatar(item.memberAvatar)" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<view class="name">{{ displayInviteName(item) }}</view>
|
||||
<view class="time">邀请时间:{{ formatTime(item.effectiveTime) }}</view>
|
||||
</view>
|
||||
<view class="reward">
|
||||
<text class="reward-label">奖励</text>
|
||||
<text class="reward-value">{{ formatMoney(item.inviteRewardAmount) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-stats">
|
||||
<text>销售额:{{ formatMoney(item.salesAmount) }}</text>
|
||||
<text>绑客数:{{ item.customerCount || 0 }}</text>
|
||||
<text>订单数:{{ item.orderCount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="inviteList.length" class="list-footer">
|
||||
<text v-if="finished" class="footer-text">没有更多数据了</text>
|
||||
<text v-else-if="loadingMore" class="footer-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<u-popup v-model:show="customVisible" mode="bottom" round="16">
|
||||
<view class="custom-popup">
|
||||
<view class="popup-title">自定义时间</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">开始日期</text>
|
||||
<picker mode="date" :value="customStartDate" @change="onStartDateChange">
|
||||
<view class="picker-value">{{ customStartDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">结束日期</text>
|
||||
<picker mode="date" :value="customEndDate" @change="onEndDateChange">
|
||||
<view class="picker-value">{{ customEndDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="customVisible = false">取消</view>
|
||||
<view class="popup-btn primary" @click="applyCustomRange">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onReachBottom, onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import config from '@/config/config'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionInvitees } from '@/api/distribution'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const defaultAvatar = config.defaultUserPhoto
|
||||
|
||||
const rangeTabs = [
|
||||
{ label: '全部', value: 'ALL' },
|
||||
{ label: '今日', value: 'TODAY' },
|
||||
{ label: '昨日', value: 'YESTERDAY' },
|
||||
{ label: '近七日', value: 'LAST_7_DAYS' },
|
||||
]
|
||||
|
||||
const activeRange = ref('ALL')
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const finished = ref(false)
|
||||
const inviteTotal = ref(0)
|
||||
const totalInviteReward = ref(0)
|
||||
const inviteList = ref<any[]>([])
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const customVisible = ref(false)
|
||||
const customStartDate = ref('')
|
||||
const customEndDate = ref('')
|
||||
|
||||
const RANGE_VALUES = ['ALL', 'TODAY', 'YESTERDAY', 'LAST_7_DAYS', 'CUSTOM']
|
||||
|
||||
function applyRouteOptions(options: Record<string, string | undefined> = {}) {
|
||||
const rangeType = options.rangeType
|
||||
if (rangeType && RANGE_VALUES.includes(rangeType)) {
|
||||
activeRange.value = rangeType
|
||||
return
|
||||
}
|
||||
activeRange.value = 'ALL'
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
applyRouteOptions(options as Record<string, string>)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
const current = pages[pages.length - 1] as { options?: Record<string, string> }
|
||||
if (current?.options) {
|
||||
applyRouteOptions(current.options)
|
||||
}
|
||||
resetAndLoad()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
loadInvitees(false)
|
||||
})
|
||||
|
||||
function resetAndLoad() {
|
||||
pageNumber.value = 1
|
||||
finished.value = false
|
||||
inviteList.value = []
|
||||
loadInvitees(true)
|
||||
}
|
||||
|
||||
function buildQueryParams() {
|
||||
const params: Record<string, any> = {
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize,
|
||||
rangeType: activeRange.value,
|
||||
}
|
||||
if (activeRange.value === 'CUSTOM') {
|
||||
if (customStartDate.value) {
|
||||
params.startTime = `${customStartDate.value} 00:00:00`
|
||||
}
|
||||
if (customEndDate.value) {
|
||||
params.endTime = `${customEndDate.value} 23:59:59`
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function loadInvitees(reset = false) {
|
||||
if (reset) {
|
||||
loading.value = true
|
||||
pageNumber.value = 1
|
||||
finished.value = false
|
||||
} else if (loading.value || loadingMore.value || finished.value) {
|
||||
return
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
getDistributionInvitees(buildQueryParams())
|
||||
.then((res) => {
|
||||
const result = res.data?.result || {}
|
||||
const records = result.records || []
|
||||
inviteTotal.value = Number(result.total || 0)
|
||||
totalInviteReward.value = Number(result.totalInviteReward || 0)
|
||||
if (reset) {
|
||||
inviteList.value = records
|
||||
} else {
|
||||
inviteList.value = inviteList.value.concat(records)
|
||||
}
|
||||
if (records.length < pageSize || inviteList.value.length >= inviteTotal.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
pageNumber.value += 1
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function changeRange(value: string) {
|
||||
if (activeRange.value === value) return
|
||||
activeRange.value = value
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function openCustomPicker() {
|
||||
customVisible.value = true
|
||||
}
|
||||
|
||||
function onStartDateChange(event: any) {
|
||||
customStartDate.value = event.detail.value
|
||||
}
|
||||
|
||||
function onEndDateChange(event: any) {
|
||||
customEndDate.value = event.detail.value
|
||||
}
|
||||
|
||||
function applyCustomRange() {
|
||||
if (!customStartDate.value || !customEndDate.value) {
|
||||
uni.showToast({ title: '请选择开始和结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (customStartDate.value > customEndDate.value) {
|
||||
uni.showToast({ title: '开始日期不能晚于结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
activeRange.value = 'CUSTOM'
|
||||
customVisible.value = false
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function resolveAvatar(avatar?: string) {
|
||||
return parseGoodsImageUrl(avatar) || defaultAvatar
|
||||
}
|
||||
|
||||
function displayInviteName(item: any) {
|
||||
const gradeName = (item.gradeName || '').trim()
|
||||
const memberName = (item.memberName || '分销员').trim()
|
||||
if (gradeName && !memberName.startsWith(gradeName)) {
|
||||
return `${gradeName}${memberName}`
|
||||
}
|
||||
return memberName
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return '-'
|
||||
return String(value).replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
background: #f5f5f5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #ff8f3f;
|
||||
background: #fff2e8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-left: 16rpx;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.custom-time.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 24rpx 20rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.invite-card {
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: #f2f2f2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #333;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-top: 8rpx;
|
||||
color: #999;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.reward {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reward-label {
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
color: #ff6b35;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx 24rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.card-stats text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-stats text:nth-child(2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-stats text:nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
padding: 12rpx 0 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
padding: 32rpx 28rpx 40rpx;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-btn.ghost {
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-btn.primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
}
|
||||
</style>
|
||||
425
pages/mine/distribution/invite.vue
Normal file
425
pages/mine/distribution/invite.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<view class="wrapper" :style="themeStyle">
|
||||
<view v-if="pageLoading" class="state-wrap">
|
||||
<text class="state-text">{{ pageLoadingText }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="pageError" class="state-wrap">
|
||||
<text class="state-text">{{ pageError }}</text>
|
||||
<view class="retry-btn" @click="loadInviteCardSetting">重新加载</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="card" :class="{ 'card--has-bg': hasBackground }">
|
||||
<image
|
||||
v-if="hasBackground"
|
||||
class="card-bg"
|
||||
:src="backgroundUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="card-content">
|
||||
<view v-if="showMemberInfo" class="member-info">
|
||||
<image
|
||||
v-if="inviteContext.memberAvatar"
|
||||
class="member-avatar"
|
||||
:src="avatarUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="member-name" :style="textStyle">{{ inviteContext.memberName || '分销员' }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="inviteContext.slogan" class="slogan" :style="textStyle">{{ inviteContext.slogan }}</view>
|
||||
|
||||
<view class="qr-wrap" @click="previewQrCode">
|
||||
<view v-if="qrLoading" class="qr-state">
|
||||
<text class="qr-state-text">{{ qrLoadingText }}</text>
|
||||
</view>
|
||||
<view v-else-if="qrError" class="qr-state">
|
||||
<text class="qr-state-text">{{ qrError }}</text>
|
||||
<view class="retry-btn small" @click.stop="loadQrCode">重新生成</view>
|
||||
</view>
|
||||
<image
|
||||
v-else-if="qrImage"
|
||||
class="qr-image"
|
||||
:src="qrImage"
|
||||
mode="aspectFit"
|
||||
show-menu-by-longpress
|
||||
@error="onQrImageError"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!pageLoading && !pageError" class="footer-actions">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="btn primary share-btn" open-type="share" :disabled="!qrImage">
|
||||
<text class="btn-text">微信分享</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<view class="hint-text">长按图片保存海报</view>
|
||||
</view>
|
||||
|
||||
<canvas canvas-id="inviteQrCanvas" class="qr-canvas" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getInviteCard } from '@/api/distribution'
|
||||
import { getMpCode } from '@/api/goods'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const DEFAULT_JOIN_PAGE = 'pages/mine/distribution/join'
|
||||
const QR_CANVAS_SIZE = 280
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const pageLoading = ref(true)
|
||||
const pageLoadingText = ref('加载邀请卡设置...')
|
||||
const pageError = ref('')
|
||||
const qrLoading = ref(false)
|
||||
const qrLoadingText = ref('葵花码生成中...')
|
||||
const qrError = ref('')
|
||||
const qrImage = ref('')
|
||||
const inviteContext = ref<Record<string, any>>({})
|
||||
|
||||
const showMemberInfo = computed(() => inviteContext.value.memberInfoVisible !== false)
|
||||
const textStyle = computed(() => ({
|
||||
color: inviteContext.value.textColor || '#1f2a44',
|
||||
}))
|
||||
const avatarUrl = computed(() => parseGoodsImageUrl(inviteContext.value.memberAvatar))
|
||||
const backgroundUrl = computed(() => parseGoodsImageUrl(inviteContext.value.backgroundImage))
|
||||
const hasBackground = computed(() => !!backgroundUrl.value)
|
||||
|
||||
onLoad(() => {
|
||||
loadInviteCardSetting()
|
||||
})
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const ctx = inviteContext.value
|
||||
const sharePage = normalizeSharePage(ctx.sharePage)
|
||||
const query = ctx.distributionId ? `?distributionId=${ctx.distributionId}` : ''
|
||||
return {
|
||||
title: ctx.slogan || '邀请您成为我的下级分销员',
|
||||
path: `/${sharePage}${query}`,
|
||||
imageUrl: qrImage.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function normalizeSharePage(page?: string) {
|
||||
const value = String(page || DEFAULT_JOIN_PAGE).trim()
|
||||
return value.replace(/^\//, '')
|
||||
}
|
||||
|
||||
function writeBase64ToTemp(base64: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const raw = String(base64).trim()
|
||||
if (!raw || raw.startsWith('{')) {
|
||||
reject(new Error('葵花码数据无效'))
|
||||
return
|
||||
}
|
||||
const data = raw.replace(/^data:image\/\w+;base64,/, '')
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/invite_qr_${Date.now()}.png`
|
||||
wx.getFileSystemManager().writeFile({
|
||||
filePath,
|
||||
data,
|
||||
encoding: 'base64',
|
||||
success: () => resolve(filePath),
|
||||
fail: () => reject(new Error('葵花码写入失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function exportQrByCanvas(qrPath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ctx = uni.createCanvasContext('inviteQrCanvas', instance?.proxy)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.fillRect(0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
ctx.drawImage(qrPath, 0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
ctx.draw(false, () => {
|
||||
setTimeout(() => {
|
||||
uni.canvasToTempFilePath(
|
||||
{
|
||||
canvasId: 'inviteQrCanvas',
|
||||
width: QR_CANVAS_SIZE,
|
||||
height: QR_CANVAS_SIZE,
|
||||
destWidth: QR_CANVAS_SIZE,
|
||||
destHeight: QR_CANVAS_SIZE,
|
||||
success: (file) => resolve(file.tempFilePath),
|
||||
fail: () => reject(new Error('葵花码导出失败')),
|
||||
},
|
||||
instance?.proxy
|
||||
)
|
||||
}, 300)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveQrDisplay(result: string) {
|
||||
const tempPath = await writeBase64ToTemp(result)
|
||||
qrImage.value = await exportQrByCanvas(tempPath)
|
||||
}
|
||||
|
||||
async function loadInviteCardSetting() {
|
||||
pageLoading.value = true
|
||||
pageLoadingText.value = '加载邀请卡设置...'
|
||||
pageError.value = ''
|
||||
qrImage.value = ''
|
||||
qrError.value = ''
|
||||
|
||||
try {
|
||||
const res = await getInviteCard()
|
||||
if (!res.data?.success || !res.data?.result) {
|
||||
throw new Error(res.data?.message || '获取邀请卡设置失败')
|
||||
}
|
||||
|
||||
const result = res.data.result
|
||||
inviteContext.value = {
|
||||
distributionId: result.distributionId || '',
|
||||
memberName: result.memberName || '',
|
||||
memberAvatar: result.memberAvatar || '',
|
||||
backgroundImage: result.backgroundImage || '',
|
||||
slogan: result.slogan || '邀请您成为我的下级分销员',
|
||||
memberInfoVisible: result.memberInfoVisible !== false,
|
||||
textColor: result.textColor || '#1f2a44',
|
||||
sharePage: result.sharePage || DEFAULT_JOIN_PAGE,
|
||||
shareScene: result.shareScene || '',
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('load invite card setting failed', error)
|
||||
pageError.value = error?.message || '获取邀请卡设置失败,请稍后重试'
|
||||
return
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
}
|
||||
|
||||
loadQrCode()
|
||||
}
|
||||
|
||||
async function loadQrCode() {
|
||||
if (!inviteContext.value.distributionId && !inviteContext.value.shareScene) {
|
||||
qrError.value = '分销员信息异常,无法生成葵花码'
|
||||
return
|
||||
}
|
||||
|
||||
qrLoading.value = true
|
||||
qrLoadingText.value = '葵花码生成中...'
|
||||
qrError.value = ''
|
||||
qrImage.value = ''
|
||||
|
||||
try {
|
||||
const ctx = inviteContext.value
|
||||
const codeRes = await getMpCode({
|
||||
page: normalizeSharePage(ctx.sharePage),
|
||||
scene: ctx.shareScene || `recruit_${ctx.distributionId}`,
|
||||
})
|
||||
if (!codeRes.data?.success || !codeRes.data?.result) {
|
||||
throw new Error(codeRes.data?.message || '葵花码生成失败')
|
||||
}
|
||||
|
||||
await resolveQrDisplay(codeRes.data.result)
|
||||
} catch (error: any) {
|
||||
console.error('load invite qr code failed', error)
|
||||
qrError.value = error?.message || '葵花码生成失败,请稍后重试'
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onQrImageError() {
|
||||
qrError.value = '葵花码图片加载失败'
|
||||
qrImage.value = ''
|
||||
}
|
||||
|
||||
function previewQrCode() {
|
||||
if (!qrImage.value) return
|
||||
uni.previewImage({
|
||||
current: qrImage.value,
|
||||
urls: [qrImage.value],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 160rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
display: inline-block;
|
||||
margin-top: 24rpx;
|
||||
padding: 12rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.retry-btn.small {
|
||||
margin-top: 16rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
min-height: 720rpx;
|
||||
}
|
||||
|
||||
.card--has-bg {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 32rpx 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card--has-bg .card-content {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.member-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.qr-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.qr-state {
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.qr-state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
padding: 0 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.qr-canvas {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 40rpx;
|
||||
border: 1px solid $light-color;
|
||||
color: $light-color;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
border-color: $light-color;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: block;
|
||||
padding: 20rpx 0;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin-top: 24rpx;
|
||||
color: #b3b3b3;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,496 +1,402 @@
|
||||
<template>
|
||||
<view class="wrapper">
|
||||
<!-- 筛选弹出层 TODO后续版本更新 -->
|
||||
<!-- <u-popup width="90%" v-model:show="popup" mode="right">
|
||||
<view class="screen-title">商品筛选</view>
|
||||
|
||||
<view class="screen-view">
|
||||
<view class="screen-item">
|
||||
<h4>价格区间</h4>
|
||||
<view class="flex">
|
||||
<u-input class="u-bg" placeholder-style="font-size:22rpx;" type="number" input-align="center" placeholder="最低价"></u-input>
|
||||
<view class="line"></view>
|
||||
<u-input class="u-bg" placeholder-style="font-size:22rpx;" type="number" input-align="center" placeholder="最高价"></u-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="screen-item">
|
||||
<h4>销量</h4>
|
||||
<view class="flex">
|
||||
<u-input class="u-bg w200 flex1" placeholder-style="font-size:22rpx;" type="number" input-align="center" placeholder="销量"></u-input>
|
||||
<view class="flex1">笔以上</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="screen-item">
|
||||
<h4>收入比率</h4>
|
||||
<view class="flex">
|
||||
<u-input class="u-bg" placeholder-style="font-size:22rpx;" type="number" input-align="center" placeholder="最低%"></u-input>
|
||||
<view class="line"></view>
|
||||
<u-input class="u-bg" placeholder-style="font-size:22rpx;" type="number" input-align="center" placeholder="最高%"></u-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="screen-item">
|
||||
<h4>包邮</h4>
|
||||
<view class="flex">
|
||||
<u-tag class="u-tag" shape="circle" text="包邮" mode="plain" type="info" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="screen-item">
|
||||
<h4>促销活动</h4>
|
||||
<view class="flex">
|
||||
<u-tag class="u-tag" shape="circle" text="限时抢购" mode="plain" type="info" />
|
||||
<u-tag class="u-tag" shape="circle" text="拼团秒杀" mode="plain" type="info" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="screen-item">
|
||||
<h4>经营类型</h4>
|
||||
<view class="flex">
|
||||
<u-tag class="u-tag" shape="circle" text="平台自营" mode="plain" type="info" />
|
||||
<u-tag class="u-tag" shape="circle" text="三方店铺" mode="plain" type="info" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="screen-btn">
|
||||
<view class="screen-clear"> 重置 </view>
|
||||
<view class="screen-submit"> 确定 </view>
|
||||
</view>
|
||||
</u-popup> -->
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav">
|
||||
<view class="nav-item" @click="toggleGoodsTab(true)" :class="{ checked: queryParams.checked }">已选择</view>
|
||||
<view class="nav-item" @click="toggleGoodsTab(false)" :class="{ checked: !queryParams.checked }">未选择</view>
|
||||
|
||||
<!-- <view class="nav-item" @click="popup = !popup">筛选</view> -->
|
||||
</view>
|
||||
<!-- 商品列表 -->
|
||||
|
||||
<view class="goods-list">
|
||||
<scroll-view class="body-view" scroll-y @scrolltolower="loadMore">
|
||||
<block v-for="(item, index) in goodsList" :key="item.id">
|
||||
<u-swipe-action v-if="queryParams.checked" class="distribution-swipe">
|
||||
<u-swipe-action-item
|
||||
:show="item.___selected"
|
||||
@open="openSwipeAction(item)"
|
||||
:name="index"
|
||||
:options="swipeOptions"
|
||||
@click="confirmUnbindPrompt(item)"
|
||||
>
|
||||
<view class="goods-item">
|
||||
<view class="goods-item-img" @click="navigateToGoods(item)">
|
||||
<u-image width="176rpx" height="176rpx" :src="item.thumbnail"></u-image>
|
||||
</view>
|
||||
<view class="goods-item-desc">
|
||||
<view class="-item-title" @click="navigateToGoods(item)">
|
||||
{{ item.goodsName }}
|
||||
</view>
|
||||
<view class="-item-price" @click="navigateToGoods(item)">
|
||||
佣金:
|
||||
<span> ¥{{ unitPrice(item.commission) }}</span>
|
||||
</view>
|
||||
<view class="-item-bottom">
|
||||
<view class="-item-bootom-money" @click="navigateToGoods(item)">
|
||||
<view class="-item-yj">
|
||||
<span>¥{{ unitPrice(item.price) }}</span>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view class="click" @click="shareDistributionGoods(item)">分销商品</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-swipe-action-item>
|
||||
</u-swipe-action>
|
||||
|
||||
<view v-else class="goods-item">
|
||||
<view class="goods-item-img" @click="navigateToGoods(item)">
|
||||
<u-image width="176rpx" height="176rpx" :src="item.thumbnail"></u-image>
|
||||
</view>
|
||||
<view class="goods-item-desc">
|
||||
<view class="-item-title" @click="navigateToGoods(item)">
|
||||
{{ item.goodsName }}
|
||||
</view>
|
||||
<view class="-item-price" @click="navigateToGoods(item)">
|
||||
佣金:
|
||||
<span> ¥{{ unitPrice(item.commission) }}</span>
|
||||
</view>
|
||||
<view class="-item-bottom">
|
||||
<view class="-item-bootom-money" @click="navigateToGoods(item)">
|
||||
<view class="-item-yj">
|
||||
<span>¥{{ unitPrice(item.price) }}</span>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view class="click" @click="selectGoods(item)">立即选取</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
<view class="empty">
|
||||
<!-- <u-empty v-if="empty" text="没有分销商品了" mode="list"></u-empty> -->
|
||||
<view class="page">
|
||||
<view class="search-bar">
|
||||
<view class="search-inner">
|
||||
<u-icon name="search" color="#bbb" size="18" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="商品搜索: 请输入商品关键字"
|
||||
placeholder-class="search-placeholder"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<canvas class="canvas-hide" canvas-id="qrcode" />
|
||||
<drawCanvas ref="drawCanvasRef" v-if="showPoster" :res="posterData" />
|
||||
<u-modal
|
||||
v-model:show="showUnbindModal"
|
||||
:confirm-style="{ color: lightColor }"
|
||||
@confirm="confirmUnbind"
|
||||
show-cancel-button
|
||||
:content="unbindModalContent"
|
||||
:async-close="true"
|
||||
></u-modal>
|
||||
|
||||
<view class="sort-bar">
|
||||
<view
|
||||
v-for="item in sortTabs"
|
||||
:key="item.field"
|
||||
class="sort-item"
|
||||
:class="{ active: sortField === item.field }"
|
||||
@click="onSortTab(item.field)"
|
||||
>
|
||||
<text>{{ item.label }}</text>
|
||||
<view class="sort-arrows">
|
||||
<image
|
||||
class="arrow-img"
|
||||
:src="sortField === item.field && sortOrder === 'ASC' ? '/static/index/arrow-up-1.png' : '/static/index/arrow-up.png'"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
class="arrow-img"
|
||||
:src="sortField === item.field && sortOrder === 'DESC' ? '/static/index/arrow-down-1.png' : '/static/index/arrow-down.png'"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="goods-scroll"
|
||||
scroll-y
|
||||
:lower-threshold="80"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-if="loading && !goodsList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!goodsList.length" class="state-wrap">
|
||||
<text class="state-text">暂无推广商品</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="goods-list">
|
||||
<view
|
||||
v-for="item in goodsList"
|
||||
:key="item.id"
|
||||
class="goods-card"
|
||||
>
|
||||
<image
|
||||
class="goods-image"
|
||||
:src="item.thumbnail"
|
||||
mode="aspectFill"
|
||||
@click="goGoodsDetail(item)"
|
||||
/>
|
||||
<view class="goods-body">
|
||||
<view class="goods-name" @click="goGoodsDetail(item)">{{ item.goodsName }}</view>
|
||||
<view class="goods-detail-row">
|
||||
<view class="goods-info" @click="goGoodsDetail(item)">
|
||||
<view class="goods-earn">赚 ¥{{ formatMoney(item.commission) }}</view>
|
||||
<view class="goods-price">¥{{ formatMoney(item.price) }}</view>
|
||||
</view>
|
||||
<view class="share-btn" @click.stop="shareGoods(item)">立即分享</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="goodsList.length" class="load-more">
|
||||
<text>{{ finished ? '没有更多了' : (loadingMore ? '加载中...' : '上拉加载更多') }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import {
|
||||
distributionGoods,
|
||||
checkedDistributionGoods,
|
||||
getMpCode,
|
||||
} from '@/api/goods'
|
||||
import drawCanvas from '@/components/m-canvas'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { distributionGoods, getDistributionGoodsSetting } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const swipeOptions = computed(() => [
|
||||
{
|
||||
text: '解绑',
|
||||
style: { backgroundColor: lightColor.value },
|
||||
},
|
||||
])
|
||||
const DEFAULT_SORT_MAP: Record<string, { field: string; order: string }> = {
|
||||
HIGHEST_COMMISSION: { field: 'COMMISSION', order: 'DESC' },
|
||||
HIGHEST_PRICE: { field: 'PRICE', order: 'DESC' },
|
||||
HIGHEST_SALES: { field: 'SALES', order: 'DESC' },
|
||||
LATEST_LISTING: { field: 'LATEST', order: 'DESC' },
|
||||
}
|
||||
|
||||
const sortTabs = [
|
||||
{ field: 'COMMISSION', label: '佣金' },
|
||||
{ field: 'LATEST', label: '最新' },
|
||||
{ field: 'SALES', label: '销量' },
|
||||
{ field: 'PRICE', label: '价格' },
|
||||
]
|
||||
|
||||
const unbindModalContent = '解绑该商品?'
|
||||
const showPoster = ref(false)
|
||||
const showUnbindModal = ref(false)
|
||||
const routeQuery = ref<Record<string, string>>({})
|
||||
const selectedGoods = ref<any>(null)
|
||||
const drawCanvasRef = ref<any>(null)
|
||||
const keyword = ref('')
|
||||
const sortField = ref('COMMISSION')
|
||||
const sortOrder = ref<'ASC' | 'DESC'>('DESC')
|
||||
const goodsList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const finished = ref(false)
|
||||
|
||||
const queryParams = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
checked: true,
|
||||
goodsName: '',
|
||||
sortField: 'COMMISSION',
|
||||
sortOrder: 'DESC',
|
||||
})
|
||||
const goodsList = ref<any[]>([])
|
||||
|
||||
const posterData = ref({
|
||||
container: {
|
||||
width: 600,
|
||||
height: 960,
|
||||
background: '#fff',
|
||||
title: '分享背景',
|
||||
},
|
||||
bottom: {
|
||||
img: '',
|
||||
code: '',
|
||||
price: 0,
|
||||
desc: '',
|
||||
},
|
||||
})
|
||||
const distributionId = computed(() => routeQuery.value.id || '')
|
||||
|
||||
onLoad((options) => {
|
||||
routeQuery.value = options || {}
|
||||
routeQuery.value = (options || {}) as Record<string, string>
|
||||
initPage()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
goodsList.value = []
|
||||
queryParams.value.pageNumber = 1
|
||||
fetchGoodsList()
|
||||
})
|
||||
|
||||
function confirmUnbindPrompt(item: any) {
|
||||
showUnbindModal.value = true
|
||||
selectedGoods.value = item
|
||||
async function initPage() {
|
||||
await loadGoodsSetting()
|
||||
resetAndFetch()
|
||||
}
|
||||
|
||||
function confirmUnbind() {
|
||||
checkedDistributionGoods({ id: selectedGoods.value.id, checked: false }).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({ title: '此商品解绑成功', duration: 2000 })
|
||||
showUnbindModal.value = false
|
||||
goodsList.value = []
|
||||
queryParams.value.pageNumber = 1
|
||||
fetchGoodsList()
|
||||
async function loadGoodsSetting() {
|
||||
try {
|
||||
const res = await getDistributionGoodsSetting()
|
||||
if (res.data?.success && res.data.result) {
|
||||
const setting = res.data.result
|
||||
const mapped = DEFAULT_SORT_MAP[setting.defaultSort] || DEFAULT_SORT_MAP.HIGHEST_COMMISSION
|
||||
sortField.value = mapped.field
|
||||
sortOrder.value = mapped.order as 'ASC' | 'DESC'
|
||||
queryParams.value.sortField = mapped.field
|
||||
queryParams.value.sortOrder = mapped.order
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('load distribution goods setting failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openSwipeAction(item: any) {
|
||||
goodsList.value.forEach((row) => {
|
||||
row.___selected = false
|
||||
})
|
||||
item.___selected = true
|
||||
function formatMoney(val: number | string) {
|
||||
const num = Number(val || 0)
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
function navigateToGoods(item: any) {
|
||||
function resetAndFetch() {
|
||||
goodsList.value = []
|
||||
finished.value = false
|
||||
queryParams.value.pageNumber = 1
|
||||
queryParams.value.goodsName = keyword.value.trim()
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
resetAndFetch()
|
||||
}
|
||||
|
||||
function onSortTab(field: string) {
|
||||
if (sortField.value === field) {
|
||||
sortOrder.value = sortOrder.value === 'DESC' ? 'ASC' : 'DESC'
|
||||
} else {
|
||||
sortField.value = field
|
||||
sortOrder.value = 'DESC'
|
||||
}
|
||||
queryParams.value.sortField = sortField.value
|
||||
queryParams.value.sortOrder = sortOrder.value
|
||||
resetAndFetch()
|
||||
}
|
||||
|
||||
function fetchGoodsList() {
|
||||
if (queryParams.value.pageNumber > 1) {
|
||||
if (loadingMore.value || finished.value) return
|
||||
loadingMore.value = true
|
||||
} else {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
distributionGoods(queryParams.value)
|
||||
.then((res) => {
|
||||
const records = res.data?.result?.records || []
|
||||
const total = Number(res.data?.result?.total || 0)
|
||||
if (res.data?.success) {
|
||||
goodsList.value.push(...records)
|
||||
finished.value = goodsList.value.length >= total || records.length < queryParams.value.pageSize
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!goodsList.value.length) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (loading.value || loadingMore.value || finished.value) return
|
||||
queryParams.value.pageNumber += 1
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
function goGoodsDetail(item: any) {
|
||||
if (!item?.skuId || !item?.goodsId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.skuId}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
|
||||
async function shareDistributionGoods(goods: any) {
|
||||
uni.showToast({ title: '请请按住保存图片', duration: 2000, icon: 'none' })
|
||||
const page = 'pages/product/goods'
|
||||
const scene = `${goods.skuId},${goods.goodsId},${routeQuery.value.id}`
|
||||
const result = await getMpCode({ page, scene })
|
||||
if (result.data.success) {
|
||||
const callback = result.data.result
|
||||
posterData.value.container.title = `${goods.goodsName}`
|
||||
posterData.value.bottom.code = `data:image/png;base64,${callback}`
|
||||
posterData.value.bottom.price = unitPrice(goods.price, '¥')
|
||||
posterData.value.bottom.desc = `${goods.goodsName}`
|
||||
posterData.value.bottom.img = `${goods.thumbnail}`
|
||||
if (showPoster.value) {
|
||||
drawCanvasRef.value?.init()
|
||||
}
|
||||
showPoster.value = true
|
||||
} else {
|
||||
uni.showToast({ title: '制作二维码失败!请稍后重试', duration: 2000, icon: 'none' })
|
||||
function shareGoods(item: any) {
|
||||
const distId = distributionId.value
|
||||
if (!item?.skuId || !item?.goodsId || !distId) {
|
||||
uni.showToast({ title: '分享信息不完整', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function toggleGoodsTab(isSelected: boolean) {
|
||||
goodsList.value = []
|
||||
queryParams.value.checked = isSelected
|
||||
queryParams.value.pageNumber = 1
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
function selectGoods(item: any) {
|
||||
checkedDistributionGoods({ id: item.id, checked: true }).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({ title: '已添加到我的选品库', duration: 2000, icon: 'none' })
|
||||
setTimeout(() => {
|
||||
goodsList.value = []
|
||||
queryParams.value.pageNumber = 1
|
||||
fetchGoodsList()
|
||||
}, 500)
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/distribution/share?skuId=${item.skuId}&goodsId=${item.goodsId}&distributionId=${distId}`,
|
||||
})
|
||||
}
|
||||
|
||||
function fetchGoodsList() {
|
||||
distributionGoods(queryParams.value).then((res) => {
|
||||
if (res.data.success && res.data.result.records.length >= 1) {
|
||||
res.data.result.records.forEach((item: any) => {
|
||||
item.___selected = false
|
||||
})
|
||||
goodsList.value.push(...res.data.result.records)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
queryParams.value.pageNumber += 1
|
||||
fetchGoodsList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.body-view {
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 44px - 80rpx - 104rpx);
|
||||
}
|
||||
.canvas-hide {
|
||||
/* 1 */
|
||||
position: fixed;
|
||||
right: 100vw;
|
||||
bottom: 100vh;
|
||||
/* 2 */
|
||||
z-index: -9999;
|
||||
/* 3 */
|
||||
opacity: 0;
|
||||
}
|
||||
.empty {
|
||||
margin: 40rpx 0;
|
||||
}
|
||||
.checked {
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
.screen-btn {
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 36rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin-left: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #bbb;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.sort-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
> .screen-clear,
|
||||
.screen-submit {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
.screen-submit {
|
||||
background: $main-color;
|
||||
color: #fff;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
|
||||
&.active {
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.screen-item {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.sort-arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
.flex1 {
|
||||
padding-left: 10rpx;
|
||||
|
||||
.arrow-img {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
}
|
||||
.u-tag {
|
||||
margin-right: 20rpx;
|
||||
|
||||
.goods-scroll {
|
||||
flex: 1;
|
||||
height: calc(100vh - 200rpx);
|
||||
}
|
||||
.line {
|
||||
width: 40rpx;
|
||||
height: 2rpx;
|
||||
background: #999;
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
.u-bg {
|
||||
background: #eff1f4;
|
||||
border-radius: 0.4em;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.screen-title {
|
||||
height: 88rpx;
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
font-size: 28upz;
|
||||
line-height: 88rpx;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
margin: 20rpx 0;
|
||||
align-items: center;
|
||||
}
|
||||
.screen-view {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.bar {
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
z-index: 8;
|
||||
> .bar-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 88rpx;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid #ededed;
|
||||
border-bottom: 1px solid #ededed;
|
||||
> .nav-item {
|
||||
line-height: 88rpx;
|
||||
height: 88rpx;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
color: #666;
|
||||
}
|
||||
> .nav-item.checked {
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
width: 80rpx;
|
||||
height: 4rpx;
|
||||
background: $main-color;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-swipe,
|
||||
:deep(.u-swipe-action),
|
||||
:deep(.u-swipe-action-item),
|
||||
:deep(.u-swipe-action-item__content) {
|
||||
width: 100%;
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
:deep(.u-swipe-action-item) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.u-swipe-action-item__content) {
|
||||
overflow: hidden;
|
||||
}
|
||||
.click {
|
||||
background: $main-color;
|
||||
color: #fff;
|
||||
margin: 0 4rpx;
|
||||
font-size: 22rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 100px;
|
||||
}
|
||||
.goods-list {
|
||||
// #ifdef H5
|
||||
height: calc(100vh - 176rpx);
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
height: calc(100vh - 88rpx);
|
||||
// #endif
|
||||
overflow: auto;
|
||||
padding: 16rpx 0 24rpx;
|
||||
}
|
||||
.goods-item {
|
||||
|
||||
.goods-card {
|
||||
display: flex;
|
||||
margin: 0 20rpx 20rpx;
|
||||
padding: 22rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
flex-shrink: 0;
|
||||
width: 176rpx;
|
||||
height: 176rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
.goods-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-left: 16rpx;
|
||||
display: flex;
|
||||
padding: 22rpx;
|
||||
margin: 20rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> .goods-item-desc {
|
||||
flex: 2;
|
||||
padding: 0 16rpx;
|
||||
line-height: 1.7;
|
||||
> .-item-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding-bottom: 20rpx;
|
||||
> .-item-bootom-money {
|
||||
> .-item-bl,
|
||||
.-item-yj {
|
||||
margin-right: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: $font-color-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .-item-title {
|
||||
display: -webkit-box;
|
||||
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
> .-item-price {
|
||||
color: $price-color;
|
||||
> span {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
margin-top: auto;
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
.goods-earn {
|
||||
display: inline-block;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #fff0f0;
|
||||
color: #ff4d4f;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
margin-top: 10rpx;
|
||||
color: #222;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #ff8f3f 0%, #ff6b35 100%);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
padding: 24rpx 0 40rpx;
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
512
pages/mine/distribution/order-detail.vue
Normal file
512
pages/mine/distribution/order-detail.vue
Normal file
@@ -0,0 +1,512 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<u-icon name="arrow-left" color="#fff" size="20" />
|
||||
</view>
|
||||
<view class="nav-title">分销订单详情</view>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="hero">
|
||||
<view class="hero-status">{{ detail.settlementStatusText || '待结算' }}</view>
|
||||
<view class="hero-sub">分销订单详情</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!detail.orderItemSn" class="state-wrap">
|
||||
<text class="state-text">订单不存在</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="content-card">
|
||||
<view class="steps">
|
||||
<view
|
||||
v-for="(step, index) in progressSteps"
|
||||
:key="step.key"
|
||||
class="step-item"
|
||||
:class="{ active: step.done, current: step.current }"
|
||||
>
|
||||
<view class="step-node-wrap">
|
||||
<view class="step-node"></view>
|
||||
<view v-if="index < progressSteps.length - 1" class="step-line"></view>
|
||||
</view>
|
||||
<text class="step-label">{{ step.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<u-icon name="bag-fill" color="#c58b4e" size="16" />
|
||||
<text>物流信息</text>
|
||||
</view>
|
||||
<view class="logistics-card">
|
||||
<view class="logistics-row">
|
||||
<text class="logistics-tag">{{ deliveryTypeText }}</text>
|
||||
<text class="logistics-text">{{ logisticsSummary }}</text>
|
||||
</view>
|
||||
<view class="logistics-trace">{{ logisticsTraceText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="buyer-row">买家:{{ detail.memberName || '匿名用户' }}</view>
|
||||
|
||||
<view class="goods-card">
|
||||
<image class="goods-image" :src="resolveImage(detail.image)" mode="aspectFill" />
|
||||
<view class="goods-info">
|
||||
<view class="goods-title-row">
|
||||
<text class="goods-title">{{ detail.goodsName || '商品' }}</text>
|
||||
<text class="goods-num">x{{ detail.num || 1 }}</text>
|
||||
</view>
|
||||
<view class="goods-spec">{{ formatGoodsSpecs(detail.specs) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="amount-block">
|
||||
<view class="amount-line">商品总数 {{ totalGoodsNum }} 件</view>
|
||||
<view class="amount-line">
|
||||
订单总价
|
||||
<text class="amount-value">¥{{ formatMoney(detail.totalOrderPrice) }}</text>
|
||||
</view>
|
||||
<view v-if="showCommission" class="amount-line commission-line">
|
||||
{{ detail.commissionLabel || '商品佣金' }}
|
||||
<text class="amount-value">¥{{ formatMoney(detail.commissionAmount) }}</text>
|
||||
</view>
|
||||
<view v-if="detail.settleTime && detail.settlementStatusType === 'SETTLED'" class="settle-time">
|
||||
结算时间:{{ formatTime(detail.settleTime) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section order-info-section">
|
||||
<view class="section-title with-bar">
|
||||
<view class="title-bar"></view>
|
||||
<text>订单信息</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">订单编号</text>
|
||||
<text class="info-value">{{ detail.orderSn || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">下单时间</text>
|
||||
<text class="info-value">{{ formatTime(detail.createTime) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">支付时间</text>
|
||||
<text class="info-value">{{ formatTime(detail.paymentTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionOrderDetail } from '@/api/distribution'
|
||||
import { parseGoodsImageUrl, formatGoodsSpecs } from '@/utils/filters.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const statusBarHeight = ref(20)
|
||||
const loading = ref(false)
|
||||
const detail = ref<Record<string, any>>({})
|
||||
const orderItemSn = ref('')
|
||||
const orderType = ref('PROMOTION')
|
||||
|
||||
onLoad((options) => {
|
||||
statusBarHeight.value = uni.getWindowInfo?.()?.statusBarHeight || 20
|
||||
orderItemSn.value = options?.orderItemSn || ''
|
||||
orderType.value = options?.orderType || 'PROMOTION'
|
||||
loadDetail()
|
||||
})
|
||||
|
||||
const totalGoodsNum = computed(() => Number(detail.value.totalGoodsNum || detail.value.num || 0))
|
||||
|
||||
const showCommission = computed(() => {
|
||||
const refunded = detail.value.refunded === true || detail.value.refunded === 1
|
||||
return !refunded && Number(detail.value.commissionAmount || 0) > 0
|
||||
})
|
||||
|
||||
const deliveryTypeText = computed(() => {
|
||||
const method = detail.value.deliveryMethod
|
||||
if (method === 'SELF_PICK_UP') return '自提'
|
||||
if (method === 'LOCAL_TOWN_DELIVERY') return '同城配送'
|
||||
return '快递'
|
||||
})
|
||||
|
||||
const logisticsSummary = computed(() => {
|
||||
const name = detail.value.logisticsName
|
||||
const no = detail.value.logisticsNo
|
||||
if (name && no) return `${name}:${no}`
|
||||
if (name) return name
|
||||
if (no) return no
|
||||
return '暂无物流信息'
|
||||
})
|
||||
|
||||
const logisticsTraceText = computed(() => {
|
||||
if (!detail.value.logisticsNo) return '暂无物流轨迹'
|
||||
return '暂无物流轨迹'
|
||||
})
|
||||
|
||||
const progressSteps = computed(() => {
|
||||
const paid = detail.value.payStatus === 'PAID'
|
||||
const orderStatus = detail.value.orderStatus || ''
|
||||
const delivered = ['DELIVERED', 'TAKE', 'COMPLETED', 'COMPLETE'].includes(orderStatus)
|
||||
|| detail.value.deliverStatus === 'DELIVERED'
|
||||
const completed = ['COMPLETED', 'COMPLETE'].includes(orderStatus)
|
||||
const settled = detail.value.settlementStatusType === 'SETTLED'
|
||||
const steps = [
|
||||
{ key: 'paid', label: '买家付款', done: paid },
|
||||
{ key: 'deliver', label: '商家发货', done: delivered },
|
||||
{ key: 'complete', label: '交易完成', done: completed },
|
||||
{ key: 'settle', label: '结算佣金', done: settled },
|
||||
]
|
||||
let currentMarked = false
|
||||
return steps.map((step) => {
|
||||
if (!step.done && !currentMarked) {
|
||||
currentMarked = true
|
||||
return { ...step, current: true }
|
||||
}
|
||||
return { ...step, current: false }
|
||||
})
|
||||
})
|
||||
|
||||
function loadDetail() {
|
||||
if (!orderItemSn.value) return
|
||||
loading.value = true
|
||||
getDistributionOrderDetail({
|
||||
orderItemSn: orderItemSn.value,
|
||||
orderType: orderType.value,
|
||||
})
|
||||
.then((res) => {
|
||||
detail.value = res.data?.result || {}
|
||||
})
|
||||
.catch(() => {
|
||||
detail.value = {}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function resolveImage(image?: string) {
|
||||
return parseGoodsImageUrl(image) || '/static/nodata.png'
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return '-'
|
||||
return String(value).replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages/mine/distribution/order-list' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: linear-gradient(180deg, #ff9f3f 0%, #ff7a2f 100%);
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
background: linear-gradient(180deg, #ff7a2f 0%, #ff6b35 100%);
|
||||
}
|
||||
|
||||
.nav-back,
|
||||
.nav-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 12rpx 32rpx 80rpx;
|
||||
background: linear-gradient(180deg, #ff6b35 0%, #ff8f3f 100%);
|
||||
}
|
||||
|
||||
.hero-status {
|
||||
color: #fff;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
margin-top: 8rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
margin: -56rpx 24rpx 32rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-node-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #e8e8e8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 4rpx;
|
||||
margin-top: -2rpx;
|
||||
background: #f0f0f0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.step-item.active .step-node {
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.step-item.active .step-line {
|
||||
background: #ffd8bf;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
margin-top: 12rpx;
|
||||
color: #bbb;
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-item.active .step-label {
|
||||
color: #ff8f3f;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-title.with-bar {
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.logistics-card {
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.logistics-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.logistics-tag {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #fff2e8;
|
||||
color: #ff8f3f;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.logistics-text {
|
||||
flex: 1;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.logistics-trace {
|
||||
margin-top: 12rpx;
|
||||
color: #ccc;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.buyer-row {
|
||||
margin-bottom: 20rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
display: flex;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f4f4f4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.goods-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.goods-num {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-spec {
|
||||
margin-top: 8rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.amount-block {
|
||||
text-align: right;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.amount-line {
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.commission-line {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
color: #ff4d4f;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.settle-time {
|
||||
margin-top: 4rpx;
|
||||
color: #ccc;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.order-info-section {
|
||||
padding-top: 8rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
padding: 18rpx 0;
|
||||
border-bottom: 1rpx solid #f8f8f8;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
710
pages/mine/distribution/order-list.vue
Normal file
710
pages/mine/distribution/order-list.vue
Normal file
@@ -0,0 +1,710 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="search-bar">
|
||||
<view class="search-inner">
|
||||
<u-icon name="search" color="#bbb" size="18" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="输入用户手机号、订单号或商品名"
|
||||
placeholder-class="search-placeholder"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-tabs">
|
||||
<view
|
||||
v-for="item in mainTabs"
|
||||
:key="item.value"
|
||||
class="main-tab"
|
||||
:class="{ active: activeTab === item.value }"
|
||||
@click="changeTab(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-bar">
|
||||
<scroll-view scroll-x class="filter-scroll" :show-scrollbar="false">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="item in rangeTabs"
|
||||
:key="item.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeRange === item.value }"
|
||||
@click="changeRange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="custom-time" :class="{ active: activeRange === 'CUSTOM' }" @click="openCustomPicker">
|
||||
<text>自定义时间</text>
|
||||
<u-icon name="arrow-down" size="12" :color="activeRange === 'CUSTOM' ? '#ff8f3f' : '#999'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="summary-row">
|
||||
<text>
|
||||
共
|
||||
<text class="summary-highlight">{{ orderTotal }}</text>
|
||||
笔订单,获得{{ commissionLabel }}
|
||||
<text class="summary-highlight">{{ formatMoney(totalCommission) }}</text>
|
||||
元
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !orderList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!orderList.length" class="state-wrap">
|
||||
<text class="state-text">暂无订单记录</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="list-wrap">
|
||||
<view class="order-card" v-for="item in orderList" :key="item.orderItemSn || item.orderSn">
|
||||
<view class="card-header">
|
||||
<text class="buyer-name">买家:{{ item.memberName || '匿名用户' }}</text>
|
||||
<text class="status-text" :class="statusClass(item)">{{ item.settlementStatusText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="order-time">下单时间:{{ formatTime(item.createTime) }}</view>
|
||||
<view v-if="isRefunded(item)" class="refund-tag">已退款</view>
|
||||
|
||||
<view class="goods-row">
|
||||
<image class="goods-image" :src="resolveImage(item.image)" mode="aspectFill" />
|
||||
<view class="goods-info">
|
||||
<view class="goods-title-row">
|
||||
<text class="goods-title">{{ item.goodsName || '商品' }}</text>
|
||||
<text class="goods-num">x{{ item.num || 1 }}</text>
|
||||
</view>
|
||||
<view class="goods-spec">{{ formatGoodsSpecs(item.specs) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="amount-block">
|
||||
<view class="amount-line">实付金额:¥ {{ formatMoney(item.finalPrice) }}</view>
|
||||
<view
|
||||
v-if="showCommission(item)"
|
||||
class="amount-line commission-line"
|
||||
>
|
||||
{{ commissionLabel }}:
|
||||
<text class="commission-value">{{ formatMoney(item.commissionAmount) }} 元</text>
|
||||
</view>
|
||||
<view v-if="item.settleTime && item.settlementStatusType === 'SETTLED'" class="settle-time">
|
||||
结算时间:{{ formatTime(item.settleTime) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<text class="order-sn">单号:{{ item.orderSn }}</text>
|
||||
<text class="detail-btn" @click="goOrderDetail(item)">订单详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="orderList.length" class="list-footer">
|
||||
<text v-if="finished" class="footer-text">没有更多数据了</text>
|
||||
<text v-else-if="loadingMore" class="footer-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<u-popup v-model:show="customVisible" mode="bottom" round="16">
|
||||
<view class="custom-popup">
|
||||
<view class="popup-title">自定义时间</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">开始日期</text>
|
||||
<picker mode="date" :value="customStartDate" @change="onStartDateChange">
|
||||
<view class="picker-value">{{ customStartDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">结束日期</text>
|
||||
<picker mode="date" :value="customEndDate" @change="onEndDateChange">
|
||||
<view class="picker-value">{{ customEndDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="customVisible = false">取消</view>
|
||||
<view class="popup-btn primary" @click="applyCustomRange">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onReachBottom, onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionOrders } from '@/api/distribution'
|
||||
import { parseGoodsImageUrl, formatGoodsSpecs } from '@/utils/filters.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const mainTabs = [
|
||||
{ label: '推广订单', value: 'PROMOTION' },
|
||||
{ label: '邀请订单', value: 'INVITE' },
|
||||
]
|
||||
|
||||
const rangeTabs = [
|
||||
{ label: '全部', value: 'ALL' },
|
||||
{ label: '今日', value: 'TODAY' },
|
||||
{ label: '昨日', value: 'YESTERDAY' },
|
||||
{ label: '近七日', value: 'LAST_7_DAYS' },
|
||||
]
|
||||
|
||||
const activeTab = ref('PROMOTION')
|
||||
const activeRange = ref('ALL')
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const finished = ref(false)
|
||||
const orderTotal = ref(0)
|
||||
const totalCommission = ref(0)
|
||||
const orderList = ref<any[]>([])
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const customVisible = ref(false)
|
||||
const customStartDate = ref('')
|
||||
const customEndDate = ref('')
|
||||
|
||||
const RANGE_VALUES = ['ALL', 'TODAY', 'YESTERDAY', 'LAST_7_DAYS', 'CUSTOM']
|
||||
const ORDER_TYPE_VALUES = ['PROMOTION', 'INVITE']
|
||||
|
||||
function applyRouteOptions(options: Record<string, string | undefined> = {}) {
|
||||
const orderType = options.orderType
|
||||
if (orderType && ORDER_TYPE_VALUES.includes(orderType)) {
|
||||
activeTab.value = orderType
|
||||
} else {
|
||||
activeTab.value = 'PROMOTION'
|
||||
}
|
||||
const rangeType = options.rangeType
|
||||
if (rangeType && RANGE_VALUES.includes(rangeType)) {
|
||||
activeRange.value = rangeType
|
||||
} else {
|
||||
activeRange.value = 'ALL'
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
applyRouteOptions(options as Record<string, string>)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
const current = pages[pages.length - 1] as { options?: Record<string, string> }
|
||||
if (current?.options) {
|
||||
applyRouteOptions(current.options)
|
||||
}
|
||||
resetAndLoad()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
loadOrders(false)
|
||||
})
|
||||
|
||||
function buildQueryParams() {
|
||||
const params: Record<string, any> = {
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize,
|
||||
rangeType: activeRange.value,
|
||||
orderType: activeTab.value,
|
||||
}
|
||||
const searchKeyword = keyword.value.trim()
|
||||
if (searchKeyword) {
|
||||
params.keyword = searchKeyword
|
||||
}
|
||||
if (activeRange.value === 'CUSTOM') {
|
||||
if (customStartDate.value) {
|
||||
params.startTime = `${customStartDate.value} 00:00:00`
|
||||
}
|
||||
if (customEndDate.value) {
|
||||
params.endTime = `${customEndDate.value} 23:59:59`
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const commissionLabel = computed(() => (activeTab.value === 'INVITE' ? '邀请佣金' : '商品佣金'))
|
||||
|
||||
function resetAndLoad() {
|
||||
pageNumber.value = 1
|
||||
finished.value = false
|
||||
orderList.value = []
|
||||
loadOrders(true)
|
||||
}
|
||||
|
||||
function loadOrders(reset = false) {
|
||||
if (reset) {
|
||||
loading.value = true
|
||||
pageNumber.value = 1
|
||||
finished.value = false
|
||||
} else if (loading.value || loadingMore.value || finished.value) {
|
||||
return
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
getDistributionOrders(buildQueryParams())
|
||||
.then((res) => {
|
||||
const result = res.data?.result || {}
|
||||
const records = result.records || []
|
||||
orderTotal.value = Number(result.total || 0)
|
||||
totalCommission.value = Number(result.totalCommission || 0)
|
||||
if (reset) {
|
||||
orderList.value = records
|
||||
} else {
|
||||
orderList.value = orderList.value.concat(records)
|
||||
}
|
||||
if (records.length < pageSize || orderList.value.length >= orderTotal.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
pageNumber.value += 1
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function changeTab(value: string) {
|
||||
if (activeTab.value === value) return
|
||||
activeTab.value = value
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function changeRange(value: string) {
|
||||
if (activeRange.value === value) return
|
||||
activeRange.value = value
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function openCustomPicker() {
|
||||
customVisible.value = true
|
||||
}
|
||||
|
||||
function onStartDateChange(event: any) {
|
||||
customStartDate.value = event.detail.value
|
||||
}
|
||||
|
||||
function onEndDateChange(event: any) {
|
||||
customEndDate.value = event.detail.value
|
||||
}
|
||||
|
||||
function applyCustomRange() {
|
||||
if (!customStartDate.value || !customEndDate.value) {
|
||||
uni.showToast({ title: '请选择开始和结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (customStartDate.value > customEndDate.value) {
|
||||
uni.showToast({ title: '开始日期不能晚于结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
activeRange.value = 'CUSTOM'
|
||||
customVisible.value = false
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
function resolveImage(image?: string) {
|
||||
return parseGoodsImageUrl(image) || '/static/nodata.png'
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return '-'
|
||||
return String(value).replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
function statusClass(item: any) {
|
||||
if (item.settlementStatusType === 'SETTLED') return 'status-settled'
|
||||
if (item.settlementStatusType === 'PENDING') return 'status-pending'
|
||||
return 'status-not-settle'
|
||||
}
|
||||
|
||||
function isRefunded(item: any) {
|
||||
return item.refunded === true || item.refunded === 1 || item.refunded === '1'
|
||||
}
|
||||
|
||||
function showCommission(item: any) {
|
||||
return !isRefunded(item) && Number(item.commissionAmount || 0) > 0
|
||||
}
|
||||
|
||||
function goOrderDetail(item: any) {
|
||||
if (!item?.orderItemSn) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/distribution/order-detail?orderItemSn=${item.orderItemSn}&orderType=${activeTab.value}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin-left: 12rpx;
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #bbb;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.main-tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.main-tab {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.main-tab.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.main-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
width: 56rpx;
|
||||
height: 6rpx;
|
||||
margin-left: -28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #ff8f3f;
|
||||
background: #fff2e8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-left: 16rpx;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.custom-time.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
padding: 16rpx 24rpx 20rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.summary-highlight {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.buyer-name {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.status-settled {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ff8f3f;
|
||||
}
|
||||
|
||||
.status-not-settle {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
margin-top: 12rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.refund-tag {
|
||||
display: inline-block;
|
||||
margin-top: 12rpx;
|
||||
color: #ff4d4f;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.goods-row {
|
||||
display: flex;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f4f4f4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.goods-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-num {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-spec {
|
||||
margin-top: 8rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.amount-block {
|
||||
margin-top: 20rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.amount-line {
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.commission-line {
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.commission-value {
|
||||
color: #ff8f3f;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settle-time {
|
||||
margin-top: 4rpx;
|
||||
color: #bbb;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-sn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #ccc;
|
||||
font-size: 22rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
margin-left: 16rpx;
|
||||
color: #ff8f3f;
|
||||
font-size: 26rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
padding: 12rpx 0 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
padding: 32rpx 28rpx 40rpx;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-btn.ghost {
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-btn.primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
}
|
||||
</style>
|
||||
428
pages/mine/distribution/poster.vue
Normal file
428
pages/mine/distribution/poster.vue
Normal file
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<view class="wrapper" :style="themeStyle">
|
||||
<view v-if="pageLoading" class="state-wrap">
|
||||
<text class="state-text">{{ pageLoadingText }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="pageError" class="state-wrap">
|
||||
<text class="state-text">{{ pageError }}</text>
|
||||
<view class="retry-btn" @click="loadPosterSetting">重新加载</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="card" :class="{ 'card--has-bg': hasBackground }">
|
||||
<image
|
||||
v-if="hasBackground"
|
||||
class="card-bg"
|
||||
:src="backgroundUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="card-content">
|
||||
<view v-if="showMemberInfo" class="member-info">
|
||||
<image
|
||||
v-if="posterContext.memberAvatar"
|
||||
class="member-avatar"
|
||||
:src="avatarUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="member-name" :style="textStyle">{{ posterContext.memberName || '分销员' }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="posterContext.slogan" class="slogan" :style="textStyle">{{ posterContext.slogan }}</view>
|
||||
|
||||
<view class="qr-wrap" @click="previewQrCode">
|
||||
<view v-if="qrLoading" class="qr-state">
|
||||
<text class="qr-state-text">{{ qrLoadingText }}</text>
|
||||
</view>
|
||||
<view v-else-if="qrError" class="qr-state">
|
||||
<text class="qr-state-text">{{ qrError }}</text>
|
||||
<view class="retry-btn small" @click.stop="loadQrCode">重新生成</view>
|
||||
</view>
|
||||
<image
|
||||
v-else-if="qrImage"
|
||||
class="qr-image"
|
||||
:src="qrImage"
|
||||
mode="aspectFit"
|
||||
show-menu-by-longpress
|
||||
@error="onQrImageError"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!pageLoading && !pageError" class="footer-actions">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="btn primary share-btn" open-type="share" :disabled="!qrImage">
|
||||
<text class="btn-text">微信分享</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<view class="hint-text">长按图片保存海报</view>
|
||||
</view>
|
||||
|
||||
<canvas canvas-id="qrCanvas" class="qr-canvas" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getPromotionPoster } from '@/api/distribution'
|
||||
import { getMpCode } from '@/api/goods'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const DEFAULT_HOME_PAGE = 'pages/tabbar/home/index'
|
||||
const QR_CANVAS_SIZE = 280
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const pageLoading = ref(true)
|
||||
const pageLoadingText = ref('加载海报设置...')
|
||||
const pageError = ref('')
|
||||
const qrLoading = ref(false)
|
||||
const qrLoadingText = ref('葵花码生成中...')
|
||||
const qrError = ref('')
|
||||
const qrImage = ref('')
|
||||
const posterContext = ref<Record<string, any>>({})
|
||||
|
||||
const showMemberInfo = computed(() => posterContext.value.memberInfoVisible !== false)
|
||||
const textStyle = computed(() => ({
|
||||
color: posterContext.value.textColor || '#1f2a44',
|
||||
}))
|
||||
const avatarUrl = computed(() => parseGoodsImageUrl(posterContext.value.memberAvatar))
|
||||
const backgroundUrl = computed(() => parseGoodsImageUrl(posterContext.value.backgroundImage))
|
||||
const hasBackground = computed(() => !!backgroundUrl.value)
|
||||
|
||||
onLoad(() => {
|
||||
loadPosterSetting()
|
||||
})
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const ctx = posterContext.value
|
||||
const sharePage = normalizeSharePage(ctx.sharePage)
|
||||
return {
|
||||
title: ctx.slogan || '邀请你一起逛逛',
|
||||
path: `/${sharePage}`,
|
||||
imageUrl: qrImage.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function normalizeSharePage(page?: string) {
|
||||
const value = String(page || DEFAULT_HOME_PAGE).trim()
|
||||
return value.replace(/^\//, '')
|
||||
}
|
||||
|
||||
function writeBase64ToTemp(base64: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const raw = String(base64).trim()
|
||||
if (!raw || raw.startsWith('{')) {
|
||||
reject(new Error('葵花码数据无效'))
|
||||
return
|
||||
}
|
||||
const data = raw.replace(/^data:image\/\w+;base64,/, '')
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/poster_qr_${Date.now()}.png`
|
||||
wx.getFileSystemManager().writeFile({
|
||||
filePath,
|
||||
data,
|
||||
encoding: 'base64',
|
||||
success: () => resolve(filePath),
|
||||
fail: () => reject(new Error('葵花码写入失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function exportQrByCanvas(qrPath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ctx = uni.createCanvasContext('qrCanvas', instance?.proxy)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.fillRect(0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
ctx.drawImage(qrPath, 0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
ctx.draw(false, () => {
|
||||
setTimeout(() => {
|
||||
uni.canvasToTempFilePath(
|
||||
{
|
||||
canvasId: 'qrCanvas',
|
||||
width: QR_CANVAS_SIZE,
|
||||
height: QR_CANVAS_SIZE,
|
||||
destWidth: QR_CANVAS_SIZE,
|
||||
destHeight: QR_CANVAS_SIZE,
|
||||
success: (file) => resolve(file.tempFilePath),
|
||||
fail: () => reject(new Error('葵花码导出失败')),
|
||||
},
|
||||
instance?.proxy
|
||||
)
|
||||
}, 300)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveQrDisplay(result: string) {
|
||||
const tempPath = await writeBase64ToTemp(result)
|
||||
qrImage.value = await exportQrByCanvas(tempPath)
|
||||
}
|
||||
|
||||
async function loadPosterSetting() {
|
||||
pageLoading.value = true
|
||||
pageLoadingText.value = '加载海报设置...'
|
||||
pageError.value = ''
|
||||
qrImage.value = ''
|
||||
qrError.value = ''
|
||||
|
||||
try {
|
||||
const res = await getPromotionPoster()
|
||||
if (!res.data?.success || !res.data?.result) {
|
||||
throw new Error(res.data?.message || '获取海报设置失败')
|
||||
}
|
||||
|
||||
const result = res.data.result
|
||||
posterContext.value = {
|
||||
distributionId: result.distributionId || '',
|
||||
memberName: result.memberName || '',
|
||||
memberAvatar: result.memberAvatar || '',
|
||||
backgroundImage: result.backgroundImage || '',
|
||||
slogan: result.slogan || '发现好物,邀请你一起逛逛',
|
||||
memberInfoVisible: result.memberInfoVisible !== false,
|
||||
textColor: result.textColor || '#1f2a44',
|
||||
sharePage: result.sharePage || DEFAULT_HOME_PAGE,
|
||||
shareScene: result.shareScene || '',
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('load poster setting failed', error)
|
||||
pageError.value = error?.message || '获取海报设置失败,请稍后重试'
|
||||
return
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
}
|
||||
|
||||
loadQrCode()
|
||||
}
|
||||
|
||||
async function loadQrCode() {
|
||||
if (!posterContext.value.distributionId && !posterContext.value.shareScene) {
|
||||
qrError.value = '分销员信息异常,无法生成葵花码'
|
||||
return
|
||||
}
|
||||
|
||||
qrLoading.value = true
|
||||
qrLoadingText.value = '葵花码生成中...'
|
||||
qrError.value = ''
|
||||
qrImage.value = ''
|
||||
|
||||
try {
|
||||
const ctx = posterContext.value
|
||||
const codeRes = await getMpCode({
|
||||
page: normalizeSharePage(ctx.sharePage),
|
||||
scene: ctx.shareScene || `bind,${ctx.distributionId}`,
|
||||
})
|
||||
if (!codeRes.data?.success || !codeRes.data?.result) {
|
||||
throw new Error(codeRes.data?.message || '葵花码生成失败')
|
||||
}
|
||||
|
||||
await resolveQrDisplay(codeRes.data.result)
|
||||
} catch (error: any) {
|
||||
console.error('load qr code failed', error)
|
||||
qrError.value = error?.message || '葵花码生成失败,请稍后重试'
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onQrImageError() {
|
||||
qrError.value = '葵花码图片加载失败'
|
||||
qrImage.value = ''
|
||||
}
|
||||
|
||||
function previewQrCode() {
|
||||
if (!qrImage.value) return
|
||||
uni.previewImage({
|
||||
current: qrImage.value,
|
||||
urls: [qrImage.value],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 160rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
display: inline-block;
|
||||
margin-top: 24rpx;
|
||||
padding: 12rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.retry-btn.small {
|
||||
margin-top: 16rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
min-height: 720rpx;
|
||||
}
|
||||
|
||||
.card--has-bg {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 32rpx 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card--has-bg .card-content {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.member-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.qr-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.qr-state {
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.qr-state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
padding: 0 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.qr-canvas {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 40rpx;
|
||||
border: 1px solid $light-color;
|
||||
color: $light-color;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
border-color: $light-color;
|
||||
}
|
||||
|
||||
.btn.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: block;
|
||||
padding: 20rpx 0;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin-top: 24rpx;
|
||||
color: #b3b3b3;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
28
pages/mine/distribution/share.vue
Normal file
28
pages/mine/distribution/share.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<distribution-goods-share
|
||||
:sku-id="query.skuId"
|
||||
:goods-id="query.goodsId"
|
||||
:distribution-id="query.distributionId"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import DistributionGoodsShare from '@/components/distribution-goods-share/index.vue'
|
||||
|
||||
const query = ref<Record<string, string>>({})
|
||||
|
||||
onLoad((options) => {
|
||||
query.value = (options || {}) as Record<string, string>
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -29,7 +29,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance, onMounted } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { distribution, cash } from '@/api/goods'
|
||||
import { distribution, cash } from '@/api/distribution'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user