mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-08-06 02:47:25 +08:00
feat(distribution): 实现分销员中心完整功能
- 新增分销业绩统计页面,包含时间范围筛选和自定义日期选择功能 - 重构分销认证页面,改为跳转至招募页面申请分销员身份 - 添加分销商品访问记录功能,支持分享追踪和佣金计算 - 更新分销历史记录页面,优化数据结构和显示字段 - 完全重写分销员首页,包含用户信息、收益统计、等级系统等功能
This commit is contained in:
215
api/distribution.js
Normal file
215
api/distribution.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 分销员相关 API
|
||||
*/
|
||||
|
||||
import { http, Method } from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* 绑定分销员
|
||||
* @param distributionId 分销员ID
|
||||
*/
|
||||
export function getGoodsDistribution(distributionId) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/bindingDistribution/${distributionId}`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会员的分销员信息(含待提现、冻结金额等)
|
||||
*/
|
||||
export function distribution() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 推广海报上下文(邀请好友锁客) */
|
||||
export function getPromotionPoster() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/promotion-poster`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 邀请卡上下文(招募下级分销员) */
|
||||
export function getInviteCard() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/invite-card`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 我邀请的下级分销员列表 */
|
||||
export function getDistributionInvitees(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/invite/invitees`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销员业绩统计 */
|
||||
export function getDistributionPerformance(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/performance`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 我的客户列表 */
|
||||
export function getDistributionCustomers(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/customers`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 客户详情 */
|
||||
export function getDistributionCustomerDetail(id) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/customers/${id}`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 设置/取消客户星标 */
|
||||
export function updateDistributionCustomerStarred(id, starred) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/customers/${id}/starred`,
|
||||
method: Method.PUT,
|
||||
params: { starred },
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销等级列表(买家端可见时) */
|
||||
export function getDistributionGrades() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/grades`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销员提现 */
|
||||
export function cash(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.POST,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销员提现历史 */
|
||||
export function cashLog(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 推广/邀请订单分页列表 */
|
||||
export function getDistributionOrders(params) {
|
||||
return http.request({
|
||||
url: `/distribution/order/orders`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 推广/邀请订单详情 */
|
||||
export function getDistributionOrderDetail(params) {
|
||||
return http.request({
|
||||
url: `/distribution/order/orders/detail`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销商品设置 */
|
||||
export function getDistributionGoodsSetting() {
|
||||
return http.request({
|
||||
url: `/distribution/goods/setting`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销员商品列表 */
|
||||
export function distributionGoods(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销员中心推荐商品 */
|
||||
export function getDistributionFeaturedGoods() {
|
||||
return http.request({
|
||||
url: `/distribution/goods/featured`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销商品分享上下文(推广图文) */
|
||||
export function getDistributionGoodsShareContext(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods/share-context`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销商品海报上下文(生成葵花码,服务端记录分享) */
|
||||
export function getDistributionGoodsPosterContext(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods/poster-context`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分享商品访问(记录访问并绑定客户关系) */
|
||||
export function recordDistributionGoodsVisit(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/share-visit`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 招募页信息 */
|
||||
export function getRecruitPage() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/page`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交招募申请 */
|
||||
export function submitRecruitApplication(data) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/apply`,
|
||||
method: Method.POST,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 招募条件进度 */
|
||||
export function getRecruitProgress() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/progress`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 上级分销员信息 */
|
||||
export function getDistributionParent() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/parent`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
91
api/goods.js
91
api/goods.js
@@ -37,18 +37,6 @@ export function getGoodsRelated(params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品分销
|
||||
* @param distributionId 商品分销ID
|
||||
*/
|
||||
export function getGoodsDistribution(distributionId) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/bindingDistribution/${distributionId}`,
|
||||
method: Method.GET,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param params
|
||||
@@ -118,85 +106,6 @@ export function getCategoryList(id) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前会员的分销商信息 可根据分销商信息查询待提现金额以及冻结金额等信息
|
||||
*/
|
||||
export function distribution() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution`,
|
||||
method: Method.GET,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请分销商
|
||||
*/
|
||||
export function applyDistribution(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution`,
|
||||
method: Method.POST,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现
|
||||
*/
|
||||
export function cash(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.POST,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现历史
|
||||
*/
|
||||
export function cashLog(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.GET,
|
||||
params
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商分页订单列表
|
||||
*/
|
||||
export function distributionOrderList(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/distributionOrder`,
|
||||
method: Method.GET,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商商品列表
|
||||
*/
|
||||
export function distributionGoods(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 选择分销商品 分销商品id
|
||||
*/
|
||||
export function checkedDistributionGoods(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods/checked/${params.id}`,
|
||||
method: Method.GET,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 小程序码
|
||||
*/
|
||||
|
||||
459
components/distribution-goods-share/index.vue
Normal file
459
components/distribution-goods-share/index.vue
Normal file
@@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<view class="share-panel" :style="themeStyle">
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'text' }"
|
||||
@click="activeTab = 'text'"
|
||||
>
|
||||
<text>推广图文</text>
|
||||
<view v-if="activeTab === 'text'" class="tab-line"></view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'poster' }"
|
||||
@click="activeTab = 'poster'"
|
||||
>
|
||||
<text>生成海报</text>
|
||||
<view v-if="activeTab === 'poster'" class="tab-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="activeTab === 'text'" class="content">
|
||||
<view class="text-card">
|
||||
<text v-if="shareContext.defaultShareText" class="share-text" :user-select="true">
|
||||
{{ shareContext.defaultShareText }}
|
||||
</text>
|
||||
<view class="share-divider"></view>
|
||||
<text class="share-text" :user-select="true">{{ shareBodyText }}</text>
|
||||
<view class="share-divider"></view>
|
||||
</view>
|
||||
|
||||
<view class="media-section">
|
||||
<scroll-view scroll-x class="image-scroll" v-if="images.length">
|
||||
<view class="image-list">
|
||||
<image
|
||||
v-for="(img, index) in images"
|
||||
:key="index"
|
||||
class="goods-image"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
@click="previewImage(index)"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="save-btn-wrap">
|
||||
<view class="save-btn" @click="saveShareContent">保存图文</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="content">
|
||||
<poster-panel
|
||||
:images="images"
|
||||
:goods-name="shareContext.goodsName"
|
||||
:goods-desc="posterGoodsDesc"
|
||||
:price="shareContext.price"
|
||||
:sku-id="skuId"
|
||||
:goods-id="goodsId"
|
||||
:distribution-id="distributionId"
|
||||
:active="activeTab === 'poster'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import config from '@/config/config'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionGoodsShareContext } from '@/api/distribution'
|
||||
import { unitPrice, parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
import PosterPanel from './poster-panel.vue'
|
||||
// #ifdef H5
|
||||
import { h5Copy } from '@/js_sdk/h5-copy/h5-copy.js'
|
||||
// #endif
|
||||
|
||||
const props = defineProps<{
|
||||
skuId?: string
|
||||
goodsId?: string
|
||||
distributionId?: string
|
||||
}>()
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const activeTab = ref<'text' | 'poster'>('text')
|
||||
const loading = ref(true)
|
||||
const shareContext = ref<Record<string, any>>({})
|
||||
|
||||
const posterGoodsDesc = computed(() => {
|
||||
const ctx = shareContext.value
|
||||
return ctx.goodsSellingPoint || ctx.defaultShareText || ''
|
||||
})
|
||||
|
||||
const images = computed(() => {
|
||||
const raw: string[] = []
|
||||
const list = shareContext.value.images
|
||||
if (Array.isArray(list) && list.length) {
|
||||
raw.push(...list)
|
||||
} else if (shareContext.value.thumbnail) {
|
||||
raw.push(shareContext.value.thumbnail)
|
||||
}
|
||||
const parsed = raw
|
||||
.map((item) => parseGoodsImageUrl(item))
|
||||
.filter((url) => !!url)
|
||||
return [...new Set(parsed)]
|
||||
})
|
||||
|
||||
const shareLink = computed(() => {
|
||||
const link = shareContext.value.miniProgramLink || shareContext.value.sharePath || ''
|
||||
if (!link) return ''
|
||||
if (link.startsWith('http')) return link
|
||||
const base = (config.shareLink || '').replace(/\/$/, '')
|
||||
return base + link
|
||||
})
|
||||
|
||||
const SHARE_DIVIDER = '- '.repeat(36).trim()
|
||||
|
||||
const shareBodyText = computed(() => {
|
||||
const ctx = shareContext.value
|
||||
const lines: string[] = []
|
||||
if (ctx.goodsName) {
|
||||
lines.push(`🔴 ${ctx.goodsName}`)
|
||||
}
|
||||
if (ctx.price !== null && ctx.price !== undefined) {
|
||||
lines.push(`【到手价】💰 ${unitPrice(ctx.price, '¥')}元`)
|
||||
}
|
||||
if (shareLink.value) {
|
||||
lines.push(`点击立即参与:${shareLink.value}`)
|
||||
}
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
const shareText = computed(() => {
|
||||
const ctx = shareContext.value
|
||||
const lines: string[] = []
|
||||
if (ctx.defaultShareText) {
|
||||
lines.push(ctx.defaultShareText)
|
||||
lines.push('')
|
||||
}
|
||||
lines.push(SHARE_DIVIDER)
|
||||
if (shareBodyText.value) {
|
||||
lines.push(shareBodyText.value)
|
||||
}
|
||||
lines.push(SHARE_DIVIDER)
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.skuId, props.goodsId, props.distributionId],
|
||||
() => {
|
||||
loadShareContext()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function loadShareContext() {
|
||||
const { skuId, goodsId, distributionId } = props
|
||||
if (!skuId || !goodsId || !distributionId) {
|
||||
loading.value = false
|
||||
shareContext.value = {}
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
getDistributionGoodsShareContext({ skuId, goodsId, distributionId })
|
||||
.then((res) => {
|
||||
if (res.data?.success) {
|
||||
shareContext.value = res.data.result || {}
|
||||
} else {
|
||||
uni.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载分享内容失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function previewImage(index: number) {
|
||||
if (!images.value.length) return
|
||||
uni.previewImage({
|
||||
current: images.value[index],
|
||||
urls: images.value,
|
||||
})
|
||||
}
|
||||
|
||||
function isClipboardBlocked(errMsg = '') {
|
||||
return (
|
||||
errMsg.includes('privacy agreement') ||
|
||||
errMsg.includes('no permission') ||
|
||||
errMsg.includes('auth deny')
|
||||
)
|
||||
}
|
||||
|
||||
function saveShareContent() {
|
||||
const text = shareText.value
|
||||
if (!text.trim()) {
|
||||
uni.showToast({ title: '暂无可保存内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
if (h5Copy(text) === false) {
|
||||
uni.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
continueAfterCopy()
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
const runCopy = () => {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
showToast: false,
|
||||
success: () => continueAfterCopy(),
|
||||
fail: (err: any) => {
|
||||
console.error('setClipboardData fail', err)
|
||||
const errMsg = err?.errMsg || ''
|
||||
if (isClipboardBlocked(errMsg)) {
|
||||
continueAfterCopy()
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const wxApi = (globalThis as any).wx
|
||||
if (wxApi?.requirePrivacyAuthorize) {
|
||||
wxApi.requirePrivacyAuthorize({
|
||||
success: () => runCopy(),
|
||||
fail: () => continueAfterCopy(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// #endif
|
||||
runCopy()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function continueAfterCopy() {
|
||||
const imageUrls = images.value
|
||||
if (!imageUrls.length) {
|
||||
uni.showToast({ title: '内容已复制', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: '保存中', mask: true })
|
||||
saveImages(imageUrls)
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '内容已复制', icon: 'none' })
|
||||
})
|
||||
.catch((error) => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '图片保存失败', icon: 'none' })
|
||||
console.error('save images failed', error)
|
||||
})
|
||||
}
|
||||
|
||||
function saveToAlbum(filePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => resolve(),
|
||||
fail: (err: any) => {
|
||||
const errMsg = err?.errMsg || ''
|
||||
const needAuth =
|
||||
errMsg.includes('auth deny') ||
|
||||
errMsg.includes('authorize') ||
|
||||
errMsg.includes('permission')
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!needAuth) {
|
||||
reject(new Error('save failed'))
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (!modalRes.confirm) {
|
||||
reject(new Error('auth denied'))
|
||||
return
|
||||
}
|
||||
uni.openSetting({
|
||||
success: (settingRes) => {
|
||||
if (settingRes.authSetting['scope.writePhotosAlbum']) {
|
||||
saveToAlbum(filePath).then(resolve).catch(reject)
|
||||
} else {
|
||||
reject(new Error('auth denied'))
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error('auth denied')),
|
||||
})
|
||||
},
|
||||
fail: () => reject(new Error('auth denied')),
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
reject(new Error('save failed'))
|
||||
// #endif
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveImages(urls: string[]) {
|
||||
return urls.reduce<Promise<void>>((chain, url) => {
|
||||
return chain.then(() => downloadAndSaveImage(url))
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
function downloadAndSaveImage(url: string) {
|
||||
if (!url || !/^https?:\/\//.test(url)) {
|
||||
return Promise.reject(new Error('invalid url'))
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||
reject(new Error('download failed'))
|
||||
return
|
||||
}
|
||||
saveToAlbum(res.tempFilePath).then(resolve).catch(reject)
|
||||
},
|
||||
fail: () => reject(new Error('download failed')),
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.share-panel {
|
||||
min-height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 32rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-bottom: 20rpx;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
|
||||
&.active {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
width: 56rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx 24rpx 48rpx;
|
||||
}
|
||||
|
||||
.text-card {
|
||||
background: #f7f7f7;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.share-divider {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
margin: 20rpx 0;
|
||||
border-top: 2rpx dashed #c8c8c8;
|
||||
}
|
||||
|
||||
.share-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.media-section {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.image-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.save-btn-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
min-width: 140rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
</style>
|
||||
539
components/distribution-goods-share/poster-panel.vue
Normal file
539
components/distribution-goods-share/poster-panel.vue
Normal file
@@ -0,0 +1,539 @@
|
||||
<template>
|
||||
<view class="poster-panel">
|
||||
<view class="poster-hint">点击海报预览后可保存到相册</view>
|
||||
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<text class="loading-text">海报生成中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="loadError" class="loading-wrap">
|
||||
<text class="loading-text">{{ loadError }}</text>
|
||||
<view class="retry-btn" @click="generatePosters">重新生成</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!images.length" class="loading-wrap">
|
||||
<text class="loading-text">暂无商品图片</text>
|
||||
</view>
|
||||
|
||||
<template v-else-if="posterList.length">
|
||||
<view class="poster-content">
|
||||
<swiper
|
||||
class="poster-swiper"
|
||||
:current="current"
|
||||
circular
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="(item, index) in posterList" :key="index">
|
||||
<view class="poster-card-wrap" @click="previewPoster(index)">
|
||||
<image class="poster-image" :src="item" mode="widthFix" />
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view v-if="posterList.length > 1" class="poster-dots">
|
||||
<view
|
||||
v-for="(_, index) in posterList"
|
||||
:key="index"
|
||||
:class="['dot', current === index && 'active']"
|
||||
/>
|
||||
</view>
|
||||
<view class="save-btn-wrap">
|
||||
<view class="save-btn" @click="saveCurrentPoster">保存海报</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas id="distributionPosterCanvas" type="2d" style="width: 600px; height: 840px" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas
|
||||
canvas-id="distributionPosterCanvas"
|
||||
id="distributionPosterCanvas"
|
||||
style="width: 600px; height: 840px"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, getCurrentInstance, nextTick } from 'vue'
|
||||
import DrawPoster from '@/js_sdk/u-draw-poster'
|
||||
import { getDistributionGoodsPosterContext } from '@/api/distribution'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
|
||||
const CANVAS_SELECTOR = 'distributionPosterCanvas'
|
||||
const POSTER_WIDTH = 600
|
||||
const IMAGE_HEIGHT = POSTER_WIDTH
|
||||
const BOTTOM_HEIGHT = 240
|
||||
const POSTER_HEIGHT = IMAGE_HEIGHT + BOTTOM_HEIGHT
|
||||
const QR_SIZE = 120
|
||||
const QR_MARGIN_RIGHT = 28
|
||||
const QR_MARGIN_BOTTOM = 28
|
||||
const GENERATE_TIMEOUT = 45000
|
||||
|
||||
const props = defineProps<{
|
||||
images?: string[]
|
||||
goodsName?: string
|
||||
goodsDesc?: string
|
||||
price?: number | null
|
||||
skuId?: string
|
||||
goodsId?: string
|
||||
distributionId?: string
|
||||
active?: boolean
|
||||
}>()
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const posterList = ref<string[]>([])
|
||||
const current = ref(0)
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const hasGenerated = ref(false)
|
||||
let dp: any = null
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const st2 = (size: number) => size * 2
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
const st2 = (size: number) => size
|
||||
// #endif
|
||||
|
||||
watch(
|
||||
() => [props.images?.length, props.skuId, props.goodsId, props.distributionId],
|
||||
() => {
|
||||
resetPosters()
|
||||
if (props.active && props.images?.length) {
|
||||
generatePosters()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(active) => {
|
||||
if (active && !hasGenerated.value && props.images?.length && !loading.value) {
|
||||
generatePosters()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function resetPosters() {
|
||||
hasGenerated.value = false
|
||||
posterList.value = []
|
||||
loadError.value = ''
|
||||
clearDrawPosterCache()
|
||||
}
|
||||
|
||||
function clearDrawPosterCache() {
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1] as any
|
||||
if (!page) return
|
||||
delete page[`#${CANVAS_SELECTOR}__dp`]
|
||||
delete page[`${CANVAS_SELECTOR}__dp`]
|
||||
}
|
||||
|
||||
function onSwiperChange(e: any) {
|
||||
current.value = e.detail?.current ?? 0
|
||||
}
|
||||
|
||||
function previewPoster(index: number) {
|
||||
if (!posterList.value.length) return
|
||||
uni.previewImage({
|
||||
current: posterList.value[index],
|
||||
urls: posterList.value,
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number, message: string) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(message)), ms)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
function getComponentThis() {
|
||||
return instance?.proxy || instance
|
||||
}
|
||||
|
||||
async function waitCanvasReady() {
|
||||
await nextTick()
|
||||
await sleep(150)
|
||||
}
|
||||
|
||||
async function resolveImagePath(url: string) {
|
||||
if (!url) {
|
||||
throw new Error('商品图片地址无效')
|
||||
}
|
||||
if (
|
||||
url.startsWith('data:') ||
|
||||
url.startsWith('wxfile://') ||
|
||||
url.startsWith('http://tmp/') ||
|
||||
url.startsWith('https://tmp/')
|
||||
) {
|
||||
return url
|
||||
}
|
||||
const res = await uni.downloadFile({ url })
|
||||
if (res.statusCode === 200 && res.tempFilePath) {
|
||||
return res.tempFilePath
|
||||
}
|
||||
throw new Error('商品图片下载失败,请检查图片域名配置')
|
||||
}
|
||||
|
||||
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: (res) => resolve({ width: res.width, height: res.height }),
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function calcContainBox(
|
||||
imgW: number,
|
||||
imgH: number,
|
||||
boxX: number,
|
||||
boxY: number,
|
||||
boxW: number,
|
||||
boxH: number
|
||||
) {
|
||||
const scale = Math.min(boxW / imgW, boxH / imgH)
|
||||
const width = imgW * scale
|
||||
const height = imgH * scale
|
||||
return {
|
||||
x: boxX + (boxW - width) / 2,
|
||||
y: boxY + (boxH - height) / 2,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePosters() {
|
||||
const { skuId, goodsId, distributionId, images } = props
|
||||
if (!skuId || !goodsId || !distributionId || !images?.length) {
|
||||
return
|
||||
}
|
||||
if (loading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
loadError.value = '请在微信小程序中生成海报'
|
||||
return
|
||||
// #endif
|
||||
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
posterList.value = []
|
||||
clearDrawPosterCache()
|
||||
|
||||
try {
|
||||
await withTimeout(runGeneratePosters(images, skuId, goodsId, distributionId), GENERATE_TIMEOUT, '海报生成超时,请重试')
|
||||
hasGenerated.value = true
|
||||
} catch (error: any) {
|
||||
console.error('generate posters failed', error)
|
||||
loadError.value = error?.message || '海报生成失败,请稍后重试'
|
||||
clearDrawPosterCache()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runGeneratePosters(
|
||||
images: string[],
|
||||
skuId: string,
|
||||
goodsId: string,
|
||||
distributionId: string
|
||||
) {
|
||||
const posterRes = await getDistributionGoodsPosterContext({ skuId, goodsId, distributionId })
|
||||
if (!posterRes.data?.success || !posterRes.data?.result?.qrCode) {
|
||||
throw new Error(posterRes.data?.message || '葵花码生成失败')
|
||||
}
|
||||
const qrCode = `data:image/png;base64,${posterRes.data.result.qrCode}`
|
||||
|
||||
await waitCanvasReady()
|
||||
|
||||
dp = await DrawPoster.build({
|
||||
selector: CANVAS_SELECTOR,
|
||||
componentThis: getComponentThis(),
|
||||
loading: false,
|
||||
debugging: false,
|
||||
})
|
||||
|
||||
if (!dp?.canvas || !dp?.ctx) {
|
||||
throw new Error('画布初始化失败,请重试')
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
dp.canvas.width = st2(POSTER_WIDTH)
|
||||
dp.canvas.height = st2(POSTER_HEIGHT)
|
||||
// #endif
|
||||
|
||||
const results: string[] = []
|
||||
for (const image of images) {
|
||||
const localImage = await resolveImagePath(image)
|
||||
results.push(await drawPoster(dp, localImage, qrCode))
|
||||
}
|
||||
posterList.value = results
|
||||
}
|
||||
|
||||
async function drawPoster(dpInst: any, productImg: string, qrCode: string) {
|
||||
const goodsName = props.goodsName || '商品推荐'
|
||||
const goodsDesc = resolveGoodsDesc()
|
||||
const priceText =
|
||||
props.price !== null && props.price !== undefined ? unitPrice(props.price, '¥') : ''
|
||||
|
||||
await dpInst.draw((ctx: any) => {
|
||||
ctx.clearRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT))
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRoundRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT), st2(20))
|
||||
ctx.clip()
|
||||
})
|
||||
|
||||
await dpInst.draw(async (ctx: any) => {
|
||||
ctx.fillStyle = '#f7f7f7'
|
||||
ctx.fillRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(IMAGE_HEIGHT))
|
||||
const { width: imgW, height: imgH } = await getImageInfo(productImg)
|
||||
const fit = calcContainBox(imgW, imgH, 0, 0, POSTER_WIDTH, IMAGE_HEIGHT)
|
||||
await ctx.drawImage(
|
||||
productImg,
|
||||
st2(fit.x),
|
||||
st2(fit.y),
|
||||
st2(fit.width),
|
||||
st2(fit.height)
|
||||
)
|
||||
})
|
||||
|
||||
await dpInst.draw(async (ctx: any) => {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(st2(0), st2(IMAGE_HEIGHT), st2(POSTER_WIDTH), st2(POSTER_HEIGHT - IMAGE_HEIGHT))
|
||||
|
||||
const qrX = POSTER_WIDTH - QR_SIZE - QR_MARGIN_RIGHT
|
||||
const qrY = POSTER_HEIGHT - QR_SIZE - QR_MARGIN_BOTTOM
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.arc(
|
||||
st2(qrX + QR_SIZE / 2),
|
||||
st2(qrY + QR_SIZE / 2),
|
||||
st2(QR_SIZE / 2),
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
ctx.clip()
|
||||
await ctx.drawImage(qrCode, st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE))
|
||||
ctx.restore()
|
||||
})
|
||||
|
||||
await dpInst.draw((ctx: any) => {
|
||||
const textLeft = 24
|
||||
const textMaxWidth = POSTER_WIDTH - QR_SIZE - QR_MARGIN_RIGHT - textLeft - 20
|
||||
const bottomTop = IMAGE_HEIGHT
|
||||
const bottomBottom = POSTER_HEIGHT - 16
|
||||
const qrY = POSTER_HEIGHT - QR_SIZE - QR_MARGIN_BOTTOM
|
||||
|
||||
const nameLineHeight = 36
|
||||
const descLineHeight = 28
|
||||
const priceLineHeight = 36
|
||||
const textGap = 16
|
||||
const nameLineCount = Math.min(2, Math.max(1, Math.ceil(goodsName.length / 16)))
|
||||
const descLineCount = goodsDesc ? Math.min(2, Math.max(1, Math.ceil(goodsDesc.length / 14))) : 0
|
||||
|
||||
let textBlockHeight = nameLineCount * nameLineHeight
|
||||
if (goodsDesc) {
|
||||
textBlockHeight += textGap + descLineCount * descLineHeight
|
||||
}
|
||||
if (priceText) {
|
||||
textBlockHeight += textGap + priceLineHeight
|
||||
}
|
||||
|
||||
const maxTextHeight = bottomBottom - bottomTop - 12
|
||||
if (textBlockHeight > maxTextHeight) {
|
||||
textBlockHeight = maxTextHeight
|
||||
}
|
||||
|
||||
let textY = qrY + (QR_SIZE - textBlockHeight) / 2
|
||||
textY = Math.max(bottomTop + 12, textY)
|
||||
if (textY + textBlockHeight > bottomBottom) {
|
||||
textY = bottomBottom - textBlockHeight
|
||||
}
|
||||
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillStyle = '#333333'
|
||||
ctx.font = `${st2(28)}px PingFang SC`
|
||||
ctx.fillWarpText({
|
||||
text: goodsName,
|
||||
maxWidth: st2(textMaxWidth),
|
||||
x: st2(textLeft),
|
||||
y: st2(textY),
|
||||
layer: 2,
|
||||
lineHeight: st2(nameLineHeight),
|
||||
})
|
||||
textY += nameLineCount * nameLineHeight
|
||||
if (goodsDesc) {
|
||||
textY += textGap
|
||||
ctx.fillStyle = '#999999'
|
||||
ctx.font = `${st2(22)}px PingFang SC`
|
||||
ctx.fillWarpText({
|
||||
text: goodsDesc,
|
||||
maxWidth: st2(textMaxWidth),
|
||||
x: st2(textLeft),
|
||||
y: st2(textY),
|
||||
layer: 2,
|
||||
lineHeight: st2(descLineHeight),
|
||||
})
|
||||
textY += descLineCount * descLineHeight
|
||||
}
|
||||
|
||||
if (priceText) {
|
||||
textY += textGap
|
||||
const priceY = Math.min(textY, bottomBottom - priceLineHeight)
|
||||
ctx.fillStyle = '#ff3c2a'
|
||||
ctx.font = `bold ${st2(36)}px PingFang SC`
|
||||
ctx.fillText(priceText, st2(textLeft), st2(priceY))
|
||||
}
|
||||
})
|
||||
|
||||
const path = await dpInst.createImagePath()
|
||||
if (!path || path === '---stop createImagePath---') {
|
||||
throw new Error('海报图片导出失败')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
function resolveGoodsDesc() {
|
||||
const desc = (props.goodsDesc || '').trim()
|
||||
const name = (props.goodsName || '').trim()
|
||||
if (!desc || desc === name) return ''
|
||||
return desc.length > 28 ? `${desc.slice(0, 28)}...` : desc
|
||||
}
|
||||
|
||||
function saveCurrentPoster() {
|
||||
const filePath = posterList.value[current.value]
|
||||
if (!filePath) return
|
||||
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已保存到相册', icon: 'none' })
|
||||
},
|
||||
fail: (err: any) => {
|
||||
const errMsg = err?.errMsg || ''
|
||||
if (errMsg.includes('auth deny') || errMsg.includes('authorize')) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
uni.openSetting({})
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.poster-panel {
|
||||
padding: 8rpx 0 48rpx;
|
||||
}
|
||||
|
||||
.poster-hint {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #b3b3b3;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.poster-content {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.retry-btn,
|
||||
.save-btn {
|
||||
display: inline-block;
|
||||
padding: 12rpx 40rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.poster-swiper {
|
||||
height: 1000rpx;
|
||||
}
|
||||
|
||||
.poster-card-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 0 48rpx;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.poster-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
|
||||
&.active {
|
||||
width: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.save-btn-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.canvas-hide {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
width: 600px;
|
||||
height: 840px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,13 @@
|
||||
*/
|
||||
// 开发环境
|
||||
const dev = {
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
buyer: "http://127.0.0.1:8888",
|
||||
im: "https://im-api.pickmall.cn",
|
||||
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
||||
};
|
||||
// 生产环境
|
||||
const prod = {
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
buyer: "http://127.0.0.1:8888",
|
||||
im: "https://im-api.pickmall.cn",
|
||||
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
var _a;
|
||||
export const PLATFORM = typeof process !== 'undefined' ? (_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.VUE_APP_PLATFORM : undefined;
|
||||
function detectPlatform() {
|
||||
if (typeof process !== 'undefined' && (_a = process === null || process === void 0 ? void 0 : process.env) !== null && _a !== void 0 && _a.VUE_APP_PLATFORM) {
|
||||
return process.env.VUE_APP_PLATFORM;
|
||||
}
|
||||
if (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function') {
|
||||
return 'mp-weixin';
|
||||
}
|
||||
if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
|
||||
const info = uni.getSystemInfoSync();
|
||||
if (info && info.uniPlatform === 'mp-weixin') {
|
||||
return 'mp-weixin';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const PLATFORM = detectPlatform();
|
||||
/** 全局对象 */
|
||||
const _uni = (function () {
|
||||
if (typeof uni != "undefined")
|
||||
|
||||
@@ -37,7 +37,9 @@ export const handleBuildOpts = (options) => {
|
||||
}
|
||||
const oldSelector = defaultOpts.selector;
|
||||
if (PLATFORM === 'mp-weixin' && defaultOpts.type2d) {
|
||||
defaultOpts.selector = '#' + defaultOpts.selector;
|
||||
defaultOpts.selector = oldSelector.startsWith('#')
|
||||
? oldSelector
|
||||
: '#' + oldSelector;
|
||||
}
|
||||
if (!PLATFORM) {
|
||||
console.error('注意! draw-poster未开启uni条件编译! 当环境是微信小程序将不会动态切换为type2d模式');
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
},
|
||||
"plugins" : {},
|
||||
"libVersion" : "3.5.8",
|
||||
"__usePrivacyCheck__" : true,
|
||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
|
||||
},
|
||||
"h5" : {
|
||||
|
||||
99
pages.json
99
pages.json
@@ -136,7 +136,7 @@
|
||||
{
|
||||
"path": "distribution/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "推广分佣",
|
||||
"navigationBarTitleText": "推广商品",
|
||||
"app-plus": {
|
||||
//app页面不显示滚动条
|
||||
"scrollIndicator": "none"
|
||||
@@ -181,15 +181,104 @@
|
||||
{
|
||||
"path": "distribution/achievement",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的分销业绩"
|
||||
|
||||
"navigationBarTitleText": "业绩统计",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/grade",
|
||||
"style": {
|
||||
"navigationBarTitleText": "分销等级"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/home",
|
||||
"style": {
|
||||
"navigationBarTitleText": "推广分佣"
|
||||
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "分销员中心"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/invite-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的邀请",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/customer-list",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "客户列表",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/invite-friends",
|
||||
"style": {
|
||||
"navigationBarTitleText": "邀请卡",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#fff7f0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/invite",
|
||||
"style": {
|
||||
"navigationBarTitleText": "图文邀请卡",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/order-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "推广订单",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/order-detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "分销订单详情",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/poster",
|
||||
"style": {
|
||||
"navigationBarTitleText": "平台海报",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/bind",
|
||||
"style": {
|
||||
"navigationBarTitleText": "加载中",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "distribution/share",
|
||||
"style": {
|
||||
"navigationBarTitleText": "分享给好友",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
maxlength="100"
|
||||
height="150"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="街道楼牌号等"
|
||||
/>
|
||||
</up-form-item>
|
||||
@@ -348,9 +349,19 @@ page {
|
||||
}
|
||||
|
||||
:deep(.u-form-item__body__right__content__slot) {
|
||||
justify-content: flex-start;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.u-textarea__field),
|
||||
:deep(.u-input__content__field-wrapper__field) {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
:deep(.uni-textarea-placeholder),
|
||||
:deep(.input-placeholder) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.default-row {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -1,107 +1,908 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="nav-list">
|
||||
<view class="total">可提现金额</view>
|
||||
<view class="price">{{ unitPrice(distributionData.canRebate) }}</view>
|
||||
<view class="frozen">冻结金额{{ unitPrice(distributionData.commissionFrozen) }}</view>
|
||||
<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="header">
|
||||
<view class="user-row" @click="goGrade">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar" :src="userInfo.face || defaultAvatar" mode="aspectFill" />
|
||||
<view class="level-badge">V{{ currentGradeValue }}</view>
|
||||
</view>
|
||||
<view class="user-meta">
|
||||
<view class="user-name">{{ displayName }}</view>
|
||||
<view class="user-sub" v-if="userInfo.mobile">手机号:{{ userInfo.mobile }}</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="rgba(255,255,255,0.7)" size="16"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 等级升级条 -->
|
||||
<view class="level-bar" v-if="gradeVisible" @click="goGrade">
|
||||
<view class="level-bar-inner">
|
||||
<view class="level-info">
|
||||
<view class="level-title">
|
||||
<text>{{ currentGradeName }}</text>
|
||||
<text class="level-tag">V{{ currentGradeValue }}</text>
|
||||
<u-icon name="arrow-right" color="#8a5a28" size="12"></u-icon>
|
||||
</view>
|
||||
<view class="level-tip">{{ upgradeTip }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="body">
|
||||
<!-- 收益卡片 -->
|
||||
<view class="earn-card" @click="goAchievement">
|
||||
<view class="earn-row">
|
||||
<view class="earn-item">
|
||||
<view class="earn-label">今日收益</view>
|
||||
<view class="earn-value">{{ formatMoney(todayEarnings) }}<text class="unit">元</text></view>
|
||||
</view>
|
||||
<view class="earn-item earn-item-right">
|
||||
<view class="earn-label">总收益</view>
|
||||
<view class="earn-value">{{ formatMoney(totalEarnings) }}<text class="unit">元</text></view>
|
||||
<view class="earn-sub" @click.stop="goAchievement">
|
||||
含待结算{{ formatMoney(pendingAmount) }}元
|
||||
<u-icon name="arrow-right" color="rgba(255,255,255,0.85)" size="12"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 可提现 -->
|
||||
<view class="withdraw-card" @click="goWithdraw">
|
||||
<view class="withdraw-left">
|
||||
<text>可提现金额(元)</text>
|
||||
</view>
|
||||
<view class="withdraw-right">
|
||||
<text class="withdraw-amount">{{ formatMoney(withdrawableAmount) }}</text>
|
||||
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展开更多 -->
|
||||
<view class="expand-toggle" v-if="!expanded" @click="expanded = true">
|
||||
<text>展开更多</text>
|
||||
<u-icon name="arrow-down" color="#999" size="14"></u-icon>
|
||||
</view>
|
||||
|
||||
<view class="stats-card" v-if="expanded">
|
||||
<view class="stats-row">
|
||||
<view class="stats-item" @click="goTodayOrders">
|
||||
<view class="stats-label">今日推广订单</view>
|
||||
<view class="stats-value">{{ todayOrderCount }}</view>
|
||||
</view>
|
||||
<view class="stats-item" @click="goOrders">
|
||||
<view class="stats-label">累计推广订单</view>
|
||||
<view class="stats-value">{{ totalOrderCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-row">
|
||||
<view class="stats-item" @click="goTodayCustomers">
|
||||
<view class="stats-label">今日新增客户</view>
|
||||
<view class="stats-value">{{ todayCustomerCount }}</view>
|
||||
</view>
|
||||
<view class="stats-item" @click="goCustomers">
|
||||
<view class="stats-label">累计客户</view>
|
||||
<view class="stats-value">{{ totalCustomerCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-row stats-row-last">
|
||||
<view class="stats-item" @click="goTodayInvites">
|
||||
<view class="stats-label">今日新增邀请</view>
|
||||
<view class="stats-value">{{ todayInviteCount }}</view>
|
||||
</view>
|
||||
<view class="stats-item" @click="goInvites">
|
||||
<view class="stats-label">累计邀请</view>
|
||||
<view class="stats-value">{{ totalInviteCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收起 -->
|
||||
<view class="expand-toggle" v-if="expanded" @click="expanded = false">
|
||||
<text>收起</text>
|
||||
<u-icon name="arrow-up" color="#999" size="14"></u-icon>
|
||||
</view>
|
||||
|
||||
<!-- 主推商品:由后台分销商品设置决定是否展示及商品规则 -->
|
||||
<view class="featured-section" v-if="featuredGoods.length">
|
||||
<view class="section-title featured-section-title">主推商品</view>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
v-for="item in featuredGoods"
|
||||
:key="item.skuId || item.id"
|
||||
class="featured-item"
|
||||
>
|
||||
<image
|
||||
class="featured-image"
|
||||
:src="item.thumbnail"
|
||||
mode="aspectFill"
|
||||
@click="goFeaturedGoods(item)"
|
||||
></image>
|
||||
<view class="featured-body">
|
||||
<view class="featured-name" @click="goFeaturedGoods(item)">{{ item.goodsName }}</view>
|
||||
<view class="featured-detail-row">
|
||||
<view class="featured-info" @click="goFeaturedGoods(item)">
|
||||
<view class="featured-earn">赚 ¥{{ formatMoney(item.commission) }}</view>
|
||||
<view class="featured-price">¥{{ formatMoney(item.price) }}</view>
|
||||
</view>
|
||||
<view class="featured-share-btn" @click.stop="shareFeaturedGoods(item)">
|
||||
立即分享
|
||||
</view>
|
||||
<view class="nav">
|
||||
<view class="nav-item">
|
||||
<u-icon
|
||||
size="50"
|
||||
@click="navigateTo(`/pages/mine/distribution/list?id=${distributionData.id}&name=${distributionData.memberName}`)"
|
||||
color="#ff6b35"
|
||||
name="bag-fill"
|
||||
></u-icon>
|
||||
<view>分销商品</view>
|
||||
</view>
|
||||
<view
|
||||
class="nav-item"
|
||||
@click="navigateTo(`/pages/mine/distribution/history?type=0&id=${distributionData.id}&name=${distributionData.memberName}`)"
|
||||
class="featured-sales"
|
||||
v-if="item.salesCount !== null && item.salesCount !== undefined"
|
||||
@click="goFeaturedGoods(item)"
|
||||
>
|
||||
<u-icon size="50" color="#ff6b35" name="order"></u-icon>
|
||||
<view>分销业绩</view>
|
||||
已售 {{ item.salesCount }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推广卖货 -->
|
||||
<view class="promote-section">
|
||||
<view class="promote-title">推广卖货</view>
|
||||
<view class="tool-grid">
|
||||
<view class="tool-row">
|
||||
<view class="tool-item" @click="goGoodsList">
|
||||
<view class="tool-text">
|
||||
<view class="tool-name">推广商品</view>
|
||||
<view class="tool-desc">佣金赚不够</view>
|
||||
</view>
|
||||
<view class="tool-icon">
|
||||
<u-icon name="bag-fill" color="#ff8f3f" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="goPoster">
|
||||
<view class="tool-text">
|
||||
<view class="tool-name">推广海报</view>
|
||||
<view class="tool-desc">发圈快速获客</view>
|
||||
</view>
|
||||
<view class="tool-icon">
|
||||
<u-icon name="photo-fill" color="#ff6b6b" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tool-row">
|
||||
<view class="tool-item" @click="goInvite">
|
||||
<view class="tool-text">
|
||||
<view class="tool-name">邀请好友</view>
|
||||
<view class="tool-desc">可获得邀请佣金</view>
|
||||
</view>
|
||||
<view class="tool-icon">
|
||||
<u-icon name="email-fill" color="#9b7bff" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="goOrder">
|
||||
<view class="tool-text">
|
||||
<view class="tool-name">推广订单</view>
|
||||
<view class="tool-desc">查看推广订单</view>
|
||||
</view>
|
||||
<view class="tool-icon">
|
||||
<u-icon name="order" color="#ff8f3f" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-item" @click="navigateTo('/pages/mine/distribution/history?type=1')">
|
||||
<u-icon size="50" color="#ff6b35" name="red-packet-fill"></u-icon>
|
||||
<view>提现记录</view>
|
||||
</view>
|
||||
<view class="nav-item" @click="navigateTo('/pages/mine/distribution/withdrawal')">
|
||||
<u-icon size="50" color="#ffc71c" name="rmb-circle-fill"></u-icon>
|
||||
<view>提现</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { distribution } from '@/api/goods'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import config from '@/config/config'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import {
|
||||
distribution,
|
||||
getDistributionFeaturedGoods,
|
||||
getDistributionGrades,
|
||||
getDistributionCustomers,
|
||||
getDistributionInvitees,
|
||||
getDistributionOrders,
|
||||
getDistributionPerformance,
|
||||
} from '@/api/distribution'
|
||||
import { getUserInfo } from '@/api/members'
|
||||
|
||||
const store = useStore()
|
||||
const distributionData = ref<Record<string, any>>({})
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const defaultAvatar = config.defaultUserPhoto
|
||||
const statusBarHeight = ref(20)
|
||||
const expanded = ref(false)
|
||||
|
||||
onShow(() => {
|
||||
fetchDistributionInfo()
|
||||
const distributionData = ref<Record<string, any>>({})
|
||||
const gradeList = ref<any[]>([])
|
||||
const featuredGoods = ref<any[]>([])
|
||||
const todayOrderCount = ref(0)
|
||||
const todayEarnings = ref(0)
|
||||
const todayCustomerCount = ref(0)
|
||||
const todayInviteCount = ref(0)
|
||||
|
||||
const userInfo = computed(() => store.state.userInfo || {})
|
||||
const displayName = computed(() => userInfo.value.nickName || distributionData.value.memberName || '分销员')
|
||||
|
||||
const withdrawableAmount = computed(() => Number(distributionData.value.canRebate || 0))
|
||||
const pendingAmount = computed(() => Number(distributionData.value.commissionFrozen || 0))
|
||||
const totalEarnings = computed(() => Number(distributionData.value.rebateTotal || 0))
|
||||
const totalOrderCount = computed(() => Number(distributionData.value.distributionOrderCount || 0))
|
||||
const totalCustomerCount = computed(() => Number(distributionData.value.validCustomerCount || 0))
|
||||
const totalInviteCount = computed(() => Number(distributionData.value.inviteCount || 0))
|
||||
|
||||
const currentGradeValue = computed(() => {
|
||||
const val = Number(distributionData.value.gradeId)
|
||||
return Number.isFinite(val) && val > 0 ? val : 1
|
||||
})
|
||||
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
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 gradeVisible = computed(() => gradeList.value.length > 0)
|
||||
|
||||
const upgradeTip = computed(() => {
|
||||
if (!nextGrade.value) {
|
||||
return '当前已是最高等级,继续保持优秀业绩'
|
||||
}
|
||||
const sales = Number(distributionData.value.validSalesAmount || 0)
|
||||
const threshold = Number(nextGrade.value.salesThreshold || 0)
|
||||
if (threshold > 0 && sales < threshold) {
|
||||
const gap = (threshold - sales).toFixed(2)
|
||||
return `推广金额再增加${gap}元即可升级为${nextGrade.value.gradeName || '下一等级'}`
|
||||
}
|
||||
return `继续提升业绩即可升级为${nextGrade.value.gradeName || '下一等级'}`
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
statusBarHeight.value = uni.getWindowInfo?.()?.statusBarHeight || 20
|
||||
refreshUserInfo()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
function refreshUserInfo() {
|
||||
getUserInfo().then((res) => {
|
||||
if (res.data?.result) {
|
||||
store.commit('login', res.data.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadPageData() {
|
||||
Promise.all([
|
||||
fetchDistribution(),
|
||||
fetchGrades(),
|
||||
fetchTodayOrders(),
|
||||
fetchTodayEarnings(),
|
||||
fetchTodayCustomers(),
|
||||
fetchTodayInvites(),
|
||||
fetchFeaturedGoods(),
|
||||
])
|
||||
}
|
||||
|
||||
function fetchDistribution() {
|
||||
return distribution().then((res) => {
|
||||
if (!res.data?.result) {
|
||||
uni.showToast({ title: '您还不是分销员', icon: 'none' })
|
||||
setTimeout(() => goBack(), 1500)
|
||||
return
|
||||
}
|
||||
distributionData.value = res.data.result
|
||||
})
|
||||
}
|
||||
|
||||
function fetchGrades() {
|
||||
return getDistributionGrades().then((res) => {
|
||||
gradeList.value = res.data?.result || []
|
||||
})
|
||||
}
|
||||
|
||||
function fetchTodayOrders() {
|
||||
return getDistributionOrders({
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
orderType: 'PROMOTION',
|
||||
rangeType: 'TODAY',
|
||||
}).then((res) => {
|
||||
todayOrderCount.value = Number(res.data?.result?.total || 0)
|
||||
})
|
||||
}
|
||||
|
||||
function fetchTodayEarnings() {
|
||||
return getDistributionPerformance({ rangeType: 'TODAY' }).then((res) => {
|
||||
todayEarnings.value = Number(res.data?.result?.totalEarnings || 0)
|
||||
})
|
||||
}
|
||||
|
||||
function fetchTodayCustomers() {
|
||||
return getDistributionCustomers({
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
filterType: 'ALL',
|
||||
rangeType: 'TODAY',
|
||||
}).then((res) => {
|
||||
todayCustomerCount.value = Number(res.data?.result?.total || 0)
|
||||
})
|
||||
}
|
||||
|
||||
function fetchTodayInvites() {
|
||||
return getDistributionInvitees({
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
rangeType: 'TODAY',
|
||||
}).then((res) => {
|
||||
todayInviteCount.value = Number(res.data?.result?.total || 0)
|
||||
})
|
||||
}
|
||||
|
||||
function fetchFeaturedGoods() {
|
||||
featuredGoods.value = []
|
||||
return getDistributionFeaturedGoods().then((res) => {
|
||||
const result = res.data?.result
|
||||
featuredGoods.value = Array.isArray(result) ? result : []
|
||||
})
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
const num = Number(val || 0)
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
function fetchDistributionInfo() {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
distribution().then((res) => {
|
||||
if (res.data.result) {
|
||||
distributionData.value = res.data.result
|
||||
function goBack() {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||
}
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
function distQuery() {
|
||||
const d = distributionData.value
|
||||
return `id=${d.id || ''}&name=${encodeURIComponent(d.memberName || '')}`
|
||||
}
|
||||
|
||||
function goGrade() {
|
||||
navigateTo('/pages/mine/distribution/grade')
|
||||
}
|
||||
|
||||
function goAchievement() {
|
||||
navigateTo('/pages/mine/distribution/achievement')
|
||||
}
|
||||
|
||||
function goHistory() {
|
||||
navigateTo(`/pages/mine/distribution/history?type=0&${distQuery()}`)
|
||||
}
|
||||
|
||||
function goWithdraw() {
|
||||
navigateTo('/pages/mine/distribution/withdrawal')
|
||||
}
|
||||
|
||||
function goGoodsList() {
|
||||
navigateTo(`/pages/mine/distribution/list?${distQuery()}`)
|
||||
}
|
||||
|
||||
function goFeaturedGoods(item: any) {
|
||||
if (!item?.skuId || !item?.goodsId) return
|
||||
navigateTo(`/pages/product/goods?id=${item.skuId}&goodsId=${item.goodsId}`)
|
||||
}
|
||||
|
||||
function shareFeaturedGoods(item: any) {
|
||||
const distributionId = distributionData.value.id
|
||||
if (!item?.skuId || !item?.goodsId || !distributionId) {
|
||||
uni.showToast({ title: '分享信息不完整', icon: 'none' })
|
||||
return
|
||||
}
|
||||
navigateTo(
|
||||
`/pages/mine/distribution/share?skuId=${item.skuId}&goodsId=${item.goodsId}&distributionId=${distributionId}`
|
||||
)
|
||||
}
|
||||
|
||||
function goPoster() {
|
||||
navigateTo('/pages/mine/distribution/poster')
|
||||
}
|
||||
|
||||
function goInvite() {
|
||||
navigateTo('/pages/mine/distribution/invite-friends')
|
||||
}
|
||||
|
||||
function goCustomers() {
|
||||
navigateTo('/pages/mine/distribution/customer-list')
|
||||
}
|
||||
|
||||
function goTodayCustomers() {
|
||||
navigateTo('/pages/mine/distribution/customer-list?rangeType=TODAY')
|
||||
}
|
||||
|
||||
function goInvites() {
|
||||
navigateTo('/pages/mine/distribution/invite-list')
|
||||
}
|
||||
|
||||
function goTodayInvites() {
|
||||
navigateTo('/pages/mine/distribution/invite-list?rangeType=TODAY')
|
||||
}
|
||||
|
||||
function goOrders() {
|
||||
navigateTo('/pages/mine/distribution/order-list?orderType=PROMOTION')
|
||||
}
|
||||
|
||||
function goTodayOrders() {
|
||||
navigateTo('/pages/mine/distribution/order-list?orderType=PROMOTION&rangeType=TODAY')
|
||||
}
|
||||
|
||||
function goOrder() {
|
||||
goOrders()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav {
|
||||
background: #fff;
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: #1f2228;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 16rpx;
|
||||
background: #1f2228;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nav-list {
|
||||
color: #fff;
|
||||
padding: 40rpx 0;
|
||||
background: linear-gradient(91deg, $light-color 1%, $aider-light-color 99%);
|
||||
}
|
||||
.total {
|
||||
padding: 10rpx 0;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.frozen {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.price {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 50rpx;
|
||||
}
|
||||
.nav-item {
|
||||
height: 240rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(180deg, #1f2228 0%, #2a2e36 100%);
|
||||
padding: 24rpx 32rpx 40rpx;
|
||||
}
|
||||
|
||||
.user-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
position: absolute;
|
||||
right: -4rpx;
|
||||
bottom: -4rpx;
|
||||
min-width: 40rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #f6d365, #fda085);
|
||||
color: #5c3b00;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: #fff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.user-sub {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.level-bar {
|
||||
margin: -24rpx 24rpx 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.level-bar-inner {
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #fdebd3 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.level-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.level-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
color: #5c3b12;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.level-tag {
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
background: rgba(201, 122, 43, 0.12);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.level-tip {
|
||||
color: #8a6a45;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.earn-card {
|
||||
margin-top: 16rpx;
|
||||
padding: 36rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #ff8f3f 0%, #ff6b35 55%, #ff5a2f 100%);
|
||||
box-shadow: 0 12rpx 32rpx rgba(255, 107, 53, 0.28);
|
||||
}
|
||||
|
||||
.earn-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.earn-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.earn-item-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.earn-label {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.earn-value {
|
||||
color: #fff;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.earn-sub {
|
||||
margin-top: 12rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.expand-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx 0 8rpx;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.stats-row-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
color: #222;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.withdraw-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.withdraw-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.withdraw-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.withdraw-amount {
|
||||
color: #ff6b35;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 36rpx 0 20rpx;
|
||||
color: #222;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.promote-section {
|
||||
margin-top: 24rpx;
|
||||
margin-bottom: 40rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.promote-title {
|
||||
margin-bottom: 20rpx;
|
||||
color: #222;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tool-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tool-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 48%;
|
||||
padding: 24rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12rpx;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 12rpx;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
color: #222;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
color: #999;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.featured-section {
|
||||
margin-top: 36rpx;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.featured-section-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.featured-list {
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.featured-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.featured-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.featured-image {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 176rpx;
|
||||
height: 176rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
.featured-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
|
||||
.featured-name {
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.featured-detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.featured-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.featured-earn {
|
||||
display: inline-block;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #fff0f0;
|
||||
color: #ff4d4f;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.featured-price {
|
||||
margin-top: 10rpx;
|
||||
color: #222;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.featured-sales {
|
||||
margin-top: 8rpx;
|
||||
color: #b3b3b3;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.featured-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;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
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"
|
||||
<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"
|
||||
/>
|
||||
</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 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="submit" @click="submitForm">提交</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 { onReady } from '@dcloudio/uni-app'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { fieldInputStyle } from '@/utils/form-style.js'
|
||||
import { getRecruitPage, getRecruitProgress, submitRecruitApplication } from '@/api/distribution'
|
||||
import { tipsToLogin } from '@/utils/filters.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 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 stepList = [
|
||||
{ name: '推广人资料' },
|
||||
{ name: '平台审核' },
|
||||
{ name: '完成' },
|
||||
]
|
||||
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 formData = reactive({
|
||||
name: '',
|
||||
radio: '',
|
||||
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 rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
uFormRef.value?.setRules(rules)
|
||||
})
|
||||
function formatMoney(val: number | string | undefined) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
uFormRef.value?.validate().catch(() => {
|
||||
uni.showToast({ title: '请填写有效信息', icon: 'none' })
|
||||
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">
|
||||
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;
|
||||
padding: 24rpx;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.feedBack-box {
|
||||
.loading,
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 80rpx 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
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 {
|
||||
@include seller-entry-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 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>
|
||||
|
||||
<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="sort-bar">
|
||||
<view
|
||||
v-for="item in sortTabs"
|
||||
:key="item.field"
|
||||
class="sort-item"
|
||||
:class="{ active: sortField === item.field }"
|
||||
@click="onSortTab(item.field)"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<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 class="goods-item-desc">
|
||||
<view class="-item-title" @click="navigateToGoods(item)">
|
||||
{{ item.goodsName }}
|
||||
|
||||
<view v-else-if="!goodsList.length" class="state-wrap">
|
||||
<text class="state-text">暂无推广商品</text>
|
||||
</view>
|
||||
<view class="-item-price" @click="navigateToGoods(item)">
|
||||
佣金:
|
||||
<span> ¥{{ unitPrice(item.commission) }}</span>
|
||||
|
||||
<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="-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 class="share-btn" @click.stop="shareGoods(item)">立即分享</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view v-if="goodsList.length" class="load-more">
|
||||
<text>{{ finished ? '没有更多了' : (loadingMore ? '加载中...' : '上拉加载更多') }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
<view class="empty">
|
||||
<!-- <u-empty v-if="empty" text="没有分销商品了" mode="list"></u-empty> -->
|
||||
</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>
|
||||
</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
|
||||
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 formatMoney(val: number | string) {
|
||||
const num = Number(val || 0)
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
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 openSwipeAction(item: any) {
|
||||
goodsList.value.forEach((row) => {
|
||||
row.___selected = false
|
||||
})
|
||||
item.___selected = true
|
||||
function loadMore() {
|
||||
if (loading.value || loadingMore.value || finished.value) return
|
||||
queryParams.value.pageNumber += 1
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
function navigateToGoods(item: any) {
|
||||
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 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)
|
||||
function shareGoods(item: any) {
|
||||
const distId = distributionId.value
|
||||
if (!item?.skuId || !item?.goodsId || !distId) {
|
||||
uni.showToast({ title: '分享信息不完整', icon: 'none' })
|
||||
return
|
||||
}
|
||||
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>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.screen-item {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.flex1 {
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
.u-tag {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.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;
|
||||
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%;
|
||||
.search-bar {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fff;
|
||||
z-index: 8;
|
||||
> .bar-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
|
||||
.search-inner {
|
||||
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;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 36rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-left: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #bbb;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.sort-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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;
|
||||
}
|
||||
font-size: 28rpx;
|
||||
|
||||
&.active {
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-swipe,
|
||||
:deep(.u-swipe-action),
|
||||
:deep(.u-swipe-action-item),
|
||||
:deep(.u-swipe-action-item__content) {
|
||||
width: 100%;
|
||||
.sort-arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
:deep(.u-swipe-action-item) {
|
||||
overflow: hidden;
|
||||
.arrow-img {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
}
|
||||
|
||||
:deep(.u-swipe-action-item__content) {
|
||||
overflow: hidden;
|
||||
.goods-scroll {
|
||||
flex: 1;
|
||||
height: calc(100vh - 200rpx);
|
||||
}
|
||||
.click {
|
||||
background: $main-color;
|
||||
color: #fff;
|
||||
margin: 0 4rpx;
|
||||
font-size: 22rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 100px;
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
padding: 22rpx;
|
||||
margin: 20rpx;
|
||||
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;
|
||||
.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;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
> .-item-title {
|
||||
|
||||
.goods-name {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
display: -webkit-box;
|
||||
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
> .-item-price {
|
||||
color: $price-color;
|
||||
> span {
|
||||
font-size: 36rpx;
|
||||
|
||||
.goods-detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
|
||||
.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'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div v-if="!wechatLogin" class="login-body">
|
||||
<u-navbar :auto-back="showBack" :border="false"></u-navbar>
|
||||
<div>
|
||||
<u-navbar :auto-back="showBack" :border="false" :fixed="true" :placeholder="true"></u-navbar>
|
||||
<div class="login-header">
|
||||
<div class="title">{{ loginTitleWay[current].title }}</div>
|
||||
<div :class="current == 1 ? 'desc-light' : 'desc'">
|
||||
{{ loginTitleWay[current].desc
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 手机号 -->
|
||||
<div v-show="!enableUserPwdBox">
|
||||
<div v-show="!enableUserPwdBox" class="login-form-block">
|
||||
<div v-show="current == 0">
|
||||
<u-input
|
||||
border="none"
|
||||
@@ -51,7 +51,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 帐号密码登录 -->
|
||||
<!-- 帐号密码登录(小程序不展示) -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<div v-show="enableUserPwdBox">
|
||||
<u-input
|
||||
border="none"
|
||||
@@ -81,6 +82,7 @@
|
||||
帐号密码登录
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endif -->
|
||||
|
||||
<div class="privacy-row" v-show="current != 1">
|
||||
<u-checkbox
|
||||
@@ -99,11 +101,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<div v-if="current != 1" class="user-password-tips" @click="enableUserPwdBox = !enableUserPwdBox">
|
||||
{{ !enableUserPwdBox ? "帐号密码" : "手机号" }}登录
|
||||
</div>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 循环出当前可使用的第三方登录模式 -->
|
||||
<!-- 第三方登录(小程序不展示微信登录入口) -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<div class="flex login-list">
|
||||
<template v-for="(item, index) in loginList" :key="index">
|
||||
<div v-if="item.code" :style="{ background: item.color }" class="login-item">
|
||||
@@ -115,6 +120,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- #endif -->
|
||||
<myVerification v-if="codeFlag" @send="handleVerification" class="verification" ref="verification"
|
||||
business="LOGIN" />
|
||||
</div>
|
||||
@@ -182,7 +188,6 @@ const inputStyle = {
|
||||
const placeholderStyle = 'font-size: 32rpx;line-height: 32rpx;color: #999999;'
|
||||
const loginList = ref<LoginListItem[]>([
|
||||
{ icon: 'weixin-fill', color: '#00a327', title: '微信', code: 'WECHAT' },
|
||||
{ icon: 'qq-fill', color: '#38ace9', title: 'QQ', code: 'QQ' },
|
||||
{ icon: 'apple-fill', color: '#000000', title: 'Apple', code: 'APPLE' },
|
||||
])
|
||||
const clientType = ref('')
|
||||
@@ -212,20 +217,18 @@ onShow(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// #ifndef APP-PLUS
|
||||
//判断是否微信浏览器
|
||||
// #ifdef H5
|
||||
// 判断是否微信浏览器(仅 H5 有 window.navigator)
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
|
||||
wechatLogin.value = true
|
||||
return
|
||||
}
|
||||
clientType.value = 'H5'
|
||||
// #endif
|
||||
/**
|
||||
* 条件编译判断当前客户端类型
|
||||
*/
|
||||
//#ifdef H5
|
||||
clientType.value = 'H5'
|
||||
//#endif
|
||||
|
||||
//#ifdef APP-PLUS
|
||||
clientType.value = 'APP'
|
||||
@@ -233,7 +236,9 @@ onMounted(() => {
|
||||
uni.getProvider({
|
||||
service: 'oauth',
|
||||
success: (result) => {
|
||||
loginList.value = result.provider.map((value) => {
|
||||
loginList.value = result.provider
|
||||
.filter((value) => value === 'weixin' || value === 'apple')
|
||||
.map((value) => {
|
||||
let title = ''
|
||||
let codeVal = ''
|
||||
let color = '#8b8b8b'
|
||||
@@ -245,12 +250,6 @@ onMounted(() => {
|
||||
title = '微信'
|
||||
codeVal = 'WECHAT'
|
||||
break
|
||||
case 'qq':
|
||||
icon = 'qq-circle-fill'
|
||||
color = '#38ace9'
|
||||
title = 'QQ'
|
||||
codeVal = 'QQ'
|
||||
break
|
||||
case 'apple':
|
||||
icon = 'apple-fill'
|
||||
color = '#000000'
|
||||
@@ -277,14 +276,11 @@ onMounted(() => {
|
||||
})
|
||||
//#endif
|
||||
|
||||
//特殊平台,登录方式需要过滤
|
||||
// #ifdef H5
|
||||
methodFilter(['QQ'])
|
||||
// #endif
|
||||
|
||||
//微信小程序,只支持微信登录
|
||||
//微信小程序:仅手机号登录,不展示账号密码/微信第三方入口
|
||||
// #ifdef MP-WEIXIN
|
||||
methodFilter(['WECHAT'])
|
||||
clientType.value = 'WECHAT_MP'
|
||||
enableUserPwdBox.value = false
|
||||
loginList.value = []
|
||||
// #endif
|
||||
})
|
||||
|
||||
@@ -711,10 +707,20 @@ declare function miniProgramLogin(code: string | undefined): Promise<{ data: any
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding-top: 64rpx;
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.login-header {
|
||||
padding-top: 96rpx;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.title {
|
||||
padding-top: calc(104rpx);
|
||||
padding-top: 0;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
font-size: 56rpx;
|
||||
color: #333;
|
||||
@@ -727,9 +733,9 @@ declare function miniProgramLogin(code: string | undefined): Promise<{ data: any
|
||||
.desc,
|
||||
.desc-light {
|
||||
font-size: 32rpx;
|
||||
line-height: 32rpx;
|
||||
line-height: 44rpx;
|
||||
color: #333333;
|
||||
margin-top: 40rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
@@ -745,8 +751,12 @@ declare function miniProgramLogin(code: string | undefined): Promise<{ data: any
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-block {
|
||||
margin-top: 96rpx;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
margin-top: 80rpx;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.disable {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<view class="btns">
|
||||
<button type="primary" :disabled="logingFlag" bindtap="getUserProfile" @click="getUserProfile()"
|
||||
class="btn-auth">登录</button>
|
||||
<div @click="backToHome" class="btn-callback">暂不登录</div>
|
||||
<div @click="goMobileLogin" class="btn-callback">手机号登录</div>
|
||||
</view>
|
||||
<view class="privacy">
|
||||
<view class="privacy-row">
|
||||
@@ -86,9 +86,9 @@ function back() {
|
||||
whetherNavigate('wx')
|
||||
}
|
||||
|
||||
function backToHome() {
|
||||
uni.switchTab({
|
||||
url: '/pages/tabbar/home/index',
|
||||
function goMobileLogin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/passport/login',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -317,7 +317,8 @@
|
||||
/************接口API***************/
|
||||
import { ref, reactive, computed, watch, nextTick, getCurrentInstance } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { getGoods, getGoodsList, getMpScene, getGoodsDistribution } from '@/api/goods.js'
|
||||
import { getGoods, getGoodsList, getMpScene } from '@/api/goods.js'
|
||||
import { recordDistributionGoodsVisit } from '@/api/distribution.js'
|
||||
import * as API_trade from '@/api/trade.js'
|
||||
import * as API_Members from '@/api/members.js'
|
||||
import * as API_store from '@/api/store.js'
|
||||
@@ -461,10 +462,15 @@ onShow(async () => {
|
||||
const res = await getMpScene(routerVal.value.scene)
|
||||
if (res.data.success) {
|
||||
let data = res.data.result.split(",")
|
||||
init(data[0], data[1], data[2])
|
||||
init(data[0], data[1], data[2], data[3] || '')
|
||||
}
|
||||
} else {
|
||||
init(routerVal.value.id, routerVal.value.goodsId, routerVal.value.distributionId)
|
||||
init(
|
||||
routerVal.value.id,
|
||||
routerVal.value.goodsId,
|
||||
routerVal.value.distributionId,
|
||||
routerVal.value.shareId || ''
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -494,7 +500,7 @@ function selectSku(idObj: any) {
|
||||
init(idObj.skuId, idObj.goodsId)
|
||||
}
|
||||
|
||||
async function init(id: any, goodsId: any, distributionId = "") {
|
||||
async function init(id: any, goodsId: any, distributionId = "", shareId = "") {
|
||||
isGroup.value = false
|
||||
productId.value = id
|
||||
|
||||
@@ -511,11 +517,18 @@ async function init(id: any, goodsId: any, distributionId = "") {
|
||||
return
|
||||
}
|
||||
|
||||
if ((distributionId || store.state.distributionId) && isLogin("auth")) {
|
||||
let disResult = await getGoodsDistribution(distributionId)
|
||||
if (!disResult?.data?.success || disResult.statusCode == 403) {
|
||||
store.state.distributionId = distributionId
|
||||
const distId = distributionId || store.state.distributionId
|
||||
if (distId && goodsId && id) {
|
||||
recordDistributionGoodsVisit({
|
||||
skuId: id,
|
||||
goodsId,
|
||||
distributionId: distId,
|
||||
shareId: shareId || undefined,
|
||||
}).then((res) => {
|
||||
if (res?.data?.success) {
|
||||
store.state.distributionId = distId
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const resultData = response.data.result?.data
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout" v-if="list.length">
|
||||
<div class="background">
|
||||
<u-notice-bar mode="vertical" :bg-color="res.list[0].bk_color" :color="res.list[0].color" :list="list"></u-notice-bar>
|
||||
<u-notice-bar
|
||||
direction="column"
|
||||
:bg-color="noticeStyle.bk_color"
|
||||
:color="noticeStyle.color"
|
||||
:text="list"
|
||||
></u-notice-bar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,9 +16,15 @@ import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const list = computed(() =>
|
||||
props.res.list[0].title.map((i: { context: string }) => i.context)
|
||||
);
|
||||
const noticeStyle = computed(() => props.res?.list?.[0] || {});
|
||||
|
||||
const list = computed(() => {
|
||||
const titles = props.res?.list?.[0]?.title;
|
||||
if (!Array.isArray(titles)) return [];
|
||||
return titles
|
||||
.map((item: { context?: string }) => (item?.context == null ? "" : String(item.context)))
|
||||
.filter((text: string) => text.length > 0);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { distribution as fetchDistribution } from '@/api/goods'
|
||||
import { distribution as fetchDistribution } from '@/api/distribution'
|
||||
import configs from '@/config/config'
|
||||
import storage from '@/utils/storage'
|
||||
import { tipsToLogin, setClipboard } from '@/utils/filters.js'
|
||||
@@ -184,7 +184,16 @@ function linkMsgDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
function goRecruitJoin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mine/distribution/join',
|
||||
})
|
||||
}
|
||||
|
||||
function distribution() {
|
||||
if (!tipsToLogin('normal')) {
|
||||
return
|
||||
}
|
||||
fetchDistribution().then((res) => {
|
||||
if (res.data.result) {
|
||||
const type = res.data.result.distributionStatus
|
||||
@@ -193,9 +202,7 @@ function distribution() {
|
||||
url: '/pages/mine/distribution/home',
|
||||
})
|
||||
} else if (type == 'REFUSE') {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mine/distribution/auth',
|
||||
})
|
||||
goRecruitJoin()
|
||||
} else if (type == 'RETREAT') {
|
||||
uni.showToast({
|
||||
title: '您的分销资格已被清退。请联系管理员!',
|
||||
@@ -216,9 +223,7 @@ function distribution() {
|
||||
icon: 'none',
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mine/distribution/auth',
|
||||
})
|
||||
goRecruitJoin()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -528,6 +528,48 @@ export function serviceStatusList (val) {
|
||||
return statusList[val];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化商品规格展示(兼容 SKU JSON 与纯文本)
|
||||
*/
|
||||
export function formatGoodsSpecs(specs) {
|
||||
if (specs === null || specs === undefined) {
|
||||
return '默认'
|
||||
}
|
||||
const trimmed = String(specs).trim()
|
||||
if (!trimmed) {
|
||||
return '默认'
|
||||
}
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
||||
return trimmed
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(trimmed)
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return trimmed
|
||||
}
|
||||
const parts = []
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (key === 'images') {
|
||||
return
|
||||
}
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return
|
||||
}
|
||||
const text = String(value).trim()
|
||||
if (text) {
|
||||
parts.push(text)
|
||||
}
|
||||
})
|
||||
return parts.length ? parts.join(' ') : '默认'
|
||||
} catch (e) {
|
||||
return '默认'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态列表
|
||||
*/
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
const path = require('path')
|
||||
|
||||
// module.exports = {
|
||||
// /**
|
||||
// * 此处为发行h5,微信小程序,app中删除console
|
||||
// * 如需显示console 需要注释此处重新运行
|
||||
// */
|
||||
// chainWebpack: (config) => {
|
||||
// // 发行或运行时启用了压缩时会生效
|
||||
// config.optimization.minimizer('terser').tap((args) => {
|
||||
// const compress = args[0].terserOptions.compress
|
||||
// // 非 App 平台移除 console 代码(包含所有 console 方法,如 log,debug,info...)
|
||||
// compress.drop_console = true
|
||||
// compress.pure_funcs = [
|
||||
// '__f__', // App 平台 vue 移除日志代码
|
||||
// // 'console.debug' // 可移除指定的 console 方法
|
||||
// ]
|
||||
// return args
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
module.exports = {
|
||||
transpileDependencies: [
|
||||
path.join(__dirname, 'js_sdk/u-draw-poster'),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user