mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-09-20 11:52:02 +08:00
Merge branch 'feature/order-delivery-fulfillment'
This commit is contained in:
2
App.vue
2
App.vue
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/utils/theme'
|
||||
import { onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { handleAppLaunchScene } from '@/utils/distributionBind.js'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
@@ -21,6 +22,7 @@ wx.onAppRoute(() => {})
|
||||
|
||||
onLaunch((val) => {
|
||||
initTheme()
|
||||
handleAppLaunchScene(val)
|
||||
if (val?.query?.inviter) {
|
||||
storage.setInviter(val.query.inviter)
|
||||
}
|
||||
|
||||
76
README.md
76
README.md
@@ -1,34 +1,15 @@
|
||||
# Lilishop(PickMall) 开源商城系统
|
||||
|
||||
[](https://github.com/hongyehuicheng/lilishop)
|
||||
[](https://github.com/lilishop/lilishop)
|
||||
[](https://gitee.com/beijing_hongye_huicheng/lilishop)
|
||||
[](./LICENSE)
|
||||
[](https://vuejs.org/)
|
||||
[](https://uniapp.dcloud.net.cn/)
|
||||
|
||||
<p align="right">
|
||||
简体中文 | <a href="./README.en.md">English</a>
|
||||
</p>
|
||||
|
||||
## All-In-One 一键启动
|
||||
|
||||
如果只是想快速体验完整商城,不需要分别部署后端、PC、商家端、运营端、IM Web、H5、MySQL 和 Redis。安装 Docker Desktop 后,可以直接下载 `docker` 仓库 `allinone-lite-mysql-redis` 分支的一键安装脚本,按提示输入 IP/域名、端口、密码和数据目录,即可启动单镜像单容器体验环境。
|
||||
|
||||
macOS / Linux:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitee.com/beijing_hongye_huicheng/docker/raw/allinone-lite-mysql-redis/install/install-lilishop.sh -o install-lilishop.sh
|
||||
chmod +x install-lilishop.sh
|
||||
./install-lilishop.sh
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "https://gitee.com/beijing_hongye_huicheng/docker/raw/allinone-lite-mysql-redis/install/install-lilishop.ps1" -OutFile "install-lilishop.ps1"
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
.\install-lilishop.ps1
|
||||
```
|
||||
|
||||
默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web 会话/消息页、uniapp H5 和后端 API。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
|
||||
|
||||
All-In-One `lite` 是本地体验和演示形态:一个容器内包含应用、前端资源、H5、MariaDB、Redis 和本地文件存储。它不是生产高可用架构,也不内置 Elasticsearch、RocketMQ、XXL-Job、Kibana、Logstash 或 RocketMQ Dashboard;搜索、消息和定时任务逻辑在 lite 环境中使用 MySQL / Redis / 本地调度兼容实现。
|
||||
|
||||
LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开源商城系统,支持 B2B2C 多商户商城、小程序商城、微服务商城、直播电商、分销返佣、秒杀活动、Docker 私有化部署。
|
||||
|
||||
@@ -36,7 +17,7 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
|
||||
|
||||
- Java商城系统
|
||||
- 开源商城系统
|
||||
- SpringBoot商城系统
|
||||
- Spring Boot 4商城系统
|
||||
- SpringCloud微服务商城
|
||||
- B2B2C商城源码
|
||||
- 多商户商城系统
|
||||
@@ -58,14 +39,15 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
|
||||
### 1. 项目简介
|
||||
|
||||
**Lilishop** 是一款功能完善的B2B2C多商户商城系统,采用前后端分离架构,全端代码开源。
|
||||
后端基于 **SpringBoot4** 构建,具备高内聚、低耦合的特性,支持分布式部署。
|
||||
后端基于 **Spring Boot 4**(Java 21)构建,具备高内聚、低耦合的特性,支持分布式部署。
|
||||
前端覆盖PC、H5、小程序和APP,基于 **Vue** 和 **uni-app** 开发。
|
||||
|
||||
- **官方网站**: <https://pickmall.cn>
|
||||
- **官方文档**: <https://docs.pickmall.cn>
|
||||
- **Gitee 仓库**: <https://gitee.com/beijing_hongye_huicheng/lilishop>
|
||||
- **Gitee 仓库**: <https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp>
|
||||
- **GitHub 仓库**: <https://github.com/lilishop/lilishop>
|
||||
|
||||
本仓库为买家端 **H5 / 小程序 / APP** 源码(`lilishop-uniapp`)。
|
||||
|
||||
---
|
||||
|
||||
@@ -103,7 +85,7 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
|
||||
[**部署文档 -> 环境准备**](https://docs.pickmall.cn/deply/deply.html)
|
||||
|
||||
#### 数据库初始化
|
||||
- **推荐方式**: 使用项目提供的 `docker-compose` 配置,可自动完成数据库(MySQL, Redis, Elasticsearch等)的部署与初始化。
|
||||
- **推荐方式**: 使用项目提供的 `docker-compose` 配置,可自动完成数据库(MySQL、Redis 等)的部署与初始化。
|
||||
- **手动方式**: 如果您选择手动部署,SQL脚本位于以下地址。请确保获取与您代码版本一致的SQL文件。
|
||||
[**数据库脚本 (Gitee)**](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql)
|
||||
|
||||
@@ -120,15 +102,13 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
|
||||
- H5 / 小程序 / APP:`lilishop-uniapp`
|
||||
- 部署、SQL、镜像与启动脚本:`docker`
|
||||
|
||||
All-In-One 仅用于本地体验、演示或验证,不代表生产高可用部署架构。生产部署应按实际访问量、稳定性、监控、备份和扩缩容要求拆分数据库、缓存、搜索、消息、任务调度、前端静态资源和后端服务。
|
||||
---
|
||||
|
||||
### 6. 功能列表
|
||||
|
||||
[功能清单](https://bdwx2tfwwt.feishu.cn/sheets/FFQKscQKDhUx60to1k9cbMngnYb?from=from_copylink)
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 7. 开源与授权
|
||||
|
||||
@@ -145,6 +125,34 @@ All-In-One 仅用于本地体验、演示或验证,不代表生产高可用部
|
||||
我们欢迎任何形式的交流与贡献。在提问前,请先查阅 [官方文档](https://docs.pickmall.cn/) ,并参考 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md) 以便高效沟通。
|
||||
|
||||
- **[在线客服](https://work.weixin.qq.com/kfid/kfc4d8dc24a73c15f44)**
|
||||
- **微信交流1群(已满)**
|
||||
- **微信交流2群**:
|
||||
- **微信交流群**:
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 9. 升级计划
|
||||
|
||||
以下功能将陆续在后续版本中发布:✅ 已完成 · 🔜 规划中
|
||||
|
||||
- ✅ 商品定时上下架
|
||||
- ✅ 商品分组
|
||||
- ✅ 会员分组
|
||||
- ✅ 商品虚拟销量
|
||||
- ✅ 专用发票(发票上传)
|
||||
- ✅ 置顶评论
|
||||
- ✅ 客户等级
|
||||
- ✅ 购物卡
|
||||
- ✅ 腾讯云-云直播
|
||||
- ✅ 统计:营业概况、商品概况、会员概况、积分分析、储值分析、营销概况、分销统计
|
||||
- ✅ 经营报表(店铺业绩报表、商品同比环比报表、销售订单明细报表、商品销售汇总报表)
|
||||
- ✅ 卡密商品
|
||||
- ✅ 二级分销
|
||||
- ✅ 定时任务管理
|
||||
- 🔜 主题色管理
|
||||
- 🔜 店铺结算汇总
|
||||
- 🔜 支付方式汇总
|
||||
- 🔜 结算台账
|
||||
- 🔜 钱包流水
|
||||
- 🔜 财务汇总
|
||||
- 🔜 微信小店对接
|
||||
- 🔜 抖店对接
|
||||
|
||||
224
api/distribution.js
Normal file
224
api/distribution.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 分销员相关 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 getRecruitInviter(distributionId) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/inviter`,
|
||||
method: Method.GET,
|
||||
params: { distributionId },
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交招募申请 */
|
||||
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>
|
||||
@@ -289,6 +289,8 @@ const buy = (data: any) => {
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
uni.hideLoading()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
382
pages/mine/distribution/bank-accounts.vue
Normal file
382
pages/mine/distribution/bank-accounts.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="content">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 账户列表 -->
|
||||
<view v-else-if="accounts.length" class="account-list">
|
||||
<view
|
||||
class="account-item"
|
||||
v-for="item in accounts"
|
||||
:key="item.id"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<view class="item-left">
|
||||
<view v-if="isSelectMode" class="radio-circle" :class="{ active: item.id === selectedId }">
|
||||
<view class="radio-dot" v-if="item.id === selectedId" />
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.holderName }}</text>
|
||||
<view class="item-meta">
|
||||
<text class="item-bank">{{ item.bankName }}</text>
|
||||
<text class="item-card">{{ maskCard(item.cardNo) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!isSelectMode && item.isDefault" class="default-tag">默认</view>
|
||||
</view>
|
||||
<view class="item-actions">
|
||||
<view v-if="!isSelectMode && !item.isDefault" class="set-default-btn" @click.stop="handleSetDefault(item.id)">
|
||||
<text class="set-default-text">设为默认</text>
|
||||
</view>
|
||||
<view class="item-delete" @click.stop="handleDelete(item.id)">
|
||||
<u-icon name="trash" color="#ccc" size="18" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="empty-wrap">
|
||||
<text class="empty-text">暂无银行卡,请添加</text>
|
||||
</view>
|
||||
|
||||
<!-- 添加账户按钮 -->
|
||||
<view class="add-btn-wrap">
|
||||
<view class="add-btn" @click="openAddPopup">+ 添加账户</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加账户 Popup -->
|
||||
<u-popup v-model:show="addPopupVisible" mode="bottom" round="16">
|
||||
<view class="popup-wrap">
|
||||
<view class="popup-title">添加到账账户</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">姓名</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.holderName"
|
||||
placeholder="请输入收款人姓名"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-divider" />
|
||||
<view class="form-item">
|
||||
<text class="form-label">银行</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.bankName"
|
||||
placeholder="如:中国工商银行"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-divider" />
|
||||
<view class="form-item">
|
||||
<text class="form-label">卡号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.cardNo"
|
||||
type="number"
|
||||
placeholder="请输入银行卡号"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="addPopupVisible = false">取消</view>
|
||||
<view class="popup-btn primary" :class="{ disabled: saveLoading }" @click="saveAccount">
|
||||
{{ saveLoading ? '保存中...' : '保存' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getBankCards, addBankCard, deleteBankCard, setDefaultBankCard } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
// 从 getStorageSync 读取上次选择的默认卡 id(仅选择模式使用)
|
||||
const SELECTED_KEY = 'dist_selected_account_id'
|
||||
|
||||
const isSelectMode = ref(false)
|
||||
|
||||
const accounts = ref<any[]>([])
|
||||
const selectedId = ref('')
|
||||
const loading = ref(false)
|
||||
const addPopupVisible = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const form = ref({ holderName: '', bankName: '', cardNo: '' })
|
||||
|
||||
onLoad((options: any) => {
|
||||
isSelectMode.value = options?.mode === 'select'
|
||||
uni.setNavigationBarTitle({ title: isSelectMode.value ? '选择账户' : '到账账户' })
|
||||
if (isSelectMode.value) {
|
||||
selectedId.value = uni.getStorageSync(SELECTED_KEY) || ''
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadAccounts()
|
||||
})
|
||||
|
||||
function loadAccounts() {
|
||||
loading.value = true
|
||||
getBankCards()
|
||||
.then((res: any) => {
|
||||
accounts.value = res?.data?.result || []
|
||||
// 如果选择模式没有已选卡,默认选中 isDefault=true 的卡
|
||||
if (isSelectMode.value && !selectedId.value && accounts.value.length) {
|
||||
const def = accounts.value.find((a: any) => a.isDefault)
|
||||
selectedId.value = def ? def.id : accounts.value[0].id
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleSelect(item: any) {
|
||||
if (!isSelectMode.value) return
|
||||
selectedId.value = item.id
|
||||
uni.setStorageSync(SELECTED_KEY, item.id)
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function handleDelete(id: string) {
|
||||
uni.showModal({
|
||||
title: '删除账户',
|
||||
content: '确认删除该银行卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
deleteBankCard(id)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
loadAccounts()
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const msg = err?.data?.message || '删除失败'
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleSetDefault(id: string) {
|
||||
setDefaultBankCard(id)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '已设为默认', icon: 'success' })
|
||||
loadAccounts()
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const msg = err?.data?.message || '操作失败'
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function openAddPopup() {
|
||||
form.value = { holderName: '', bankName: '', cardNo: '' }
|
||||
addPopupVisible.value = true
|
||||
}
|
||||
|
||||
function saveAccount() {
|
||||
if (!form.value.holderName.trim()) {
|
||||
uni.showToast({ title: '请输入姓名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.value.bankName.trim()) {
|
||||
uni.showToast({ title: '请输入银行名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.value.cardNo.trim().length === 0) {
|
||||
uni.showToast({ title: '请输入银行卡号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saveLoading.value = true
|
||||
addBankCard({
|
||||
holderName: form.value.holderName.trim(),
|
||||
bankName: form.value.bankName.trim(),
|
||||
cardNo: form.value.cardNo.trim(),
|
||||
})
|
||||
.then((res: any) => {
|
||||
const newCard = res?.data?.result
|
||||
addPopupVisible.value = false
|
||||
uni.showToast({ title: '添加成功', icon: 'success' })
|
||||
loadAccounts()
|
||||
// 如果是选择模式,自动选中刚添加的卡
|
||||
if (isSelectMode.value && newCard?.id) {
|
||||
selectedId.value = newCard.id
|
||||
uni.setStorageSync(SELECTED_KEY, newCard.id)
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const msg = err?.data?.message || '添加失败,请重试'
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
saveLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function maskCard(cardNo: string) {
|
||||
if (!cardNo || cardNo.length < 4) return cardNo
|
||||
return '****' + cardNo.slice(-4)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.content { padding: 24rpx; }
|
||||
|
||||
/* ── 状态 ──────────────────────────────────── */
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
.state-text { color: #999; font-size: 26rpx; }
|
||||
|
||||
/* ── 账户列表 ──────────────────────────────── */
|
||||
.account-list { margin-bottom: 24rpx; }
|
||||
|
||||
.account-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.radio-circle {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
&.active { border-color: var(--theme-primary); }
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--theme-primary);
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-name { font-size: 28rpx; font-weight: 500; color: #333; }
|
||||
|
||||
.item-meta { display: flex; align-items: center; gap: 12rpx; }
|
||||
|
||||
.item-bank { font-size: 24rpx; color: #666; }
|
||||
.item-card { font-size: 24rpx; color: #999; }
|
||||
|
||||
.default-tag {
|
||||
font-size: 20rpx;
|
||||
color: var(--theme-primary);
|
||||
border: 1rpx solid var(--theme-primary);
|
||||
border-radius: 6rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.set-default-btn { padding: 4rpx 0; }
|
||||
.set-default-text { font-size: 22rpx; color: #999; }
|
||||
|
||||
.item-delete { padding: 10rpx; }
|
||||
|
||||
/* ── 空状态 ────────────────────────────────── */
|
||||
.empty-wrap { padding: 120rpx 0; text-align: center; }
|
||||
.empty-text { font-size: 26rpx; color: #ccc; }
|
||||
|
||||
/* ── 添加按钮 ──────────────────────────────── */
|
||||
.add-btn-wrap { padding-bottom: 40rpx; }
|
||||
|
||||
.add-btn {
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid var(--theme-primary);
|
||||
color: var(--theme-primary);
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── 弹窗 ──────────────────────────────────── */
|
||||
.popup-wrap { padding: 32rpx 28rpx 48rpx; }
|
||||
|
||||
.popup-title {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-item { display: flex; align-items: center; padding: 18rpx 0; }
|
||||
|
||||
.form-label { font-size: 28rpx; color: #333; width: 100rpx; flex-shrink: 0; }
|
||||
|
||||
.form-input { flex: 1; font-size: 28rpx; color: #333; height: 64rpx; }
|
||||
|
||||
.form-placeholder { color: #ccc; font-size: 26rpx; }
|
||||
|
||||
.form-divider { height: 1rpx; background: #f5f5f5; }
|
||||
|
||||
.popup-actions { display: flex; gap: 20rpx; margin-top: 32rpx; }
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-btn.ghost { color: #666; background: #f5f5f5; }
|
||||
.popup-btn.primary { color: #fff; background: var(--theme-primary); }
|
||||
.popup-btn.disabled { opacity: 0.5; }
|
||||
</style>
|
||||
32
pages/mine/distribution/bind.vue
Normal file
32
pages/mine/distribution/bind.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<view class="bind-page"></view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import {
|
||||
resolveDistributionIdFromScene,
|
||||
tryBindDistribution,
|
||||
} from '@/utils/distributionBind.js'
|
||||
|
||||
onLoad(async (options) => {
|
||||
let distributionId = (options?.distributionId as string) || ''
|
||||
|
||||
if (options?.scene) {
|
||||
distributionId = await resolveDistributionIdFromScene(String(options.scene))
|
||||
}
|
||||
|
||||
if (distributionId) {
|
||||
await tryBindDistribution(distributionId)
|
||||
}
|
||||
|
||||
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bind-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
204
pages/mine/distribution/cash-history.vue
Normal file
204
pages/mine/distribution/cash-history.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view v-if="cashLoading && !cashList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!cashList.length" class="state-wrap">
|
||||
<text class="state-text">暂无提现记录</text>
|
||||
</view>
|
||||
<view v-else class="log-list">
|
||||
<view class="log-item" v-for="item in cashList" :key="item.id">
|
||||
<view class="log-left">
|
||||
<view class="log-sn">单号:{{ item.sn || item.id }}</view>
|
||||
<view class="log-time">{{ formatTime(item.createTime) }}</view>
|
||||
</view>
|
||||
<view class="log-right">
|
||||
<text class="log-amount log-amount-out">-{{ formatMoney(item.price) }}</text>
|
||||
<view class="cash-status-tag" :class="cashStatusClass(item.distributionCashStatus)">
|
||||
{{ cashStatusLabel(item.distributionCashStatus) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-footer">
|
||||
<text v-if="cashFinished && cashList.length" class="footer-text">没有更多数据了</text>
|
||||
<text v-else-if="cashLoadingMore" class="footer-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { cashLog } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const cashList = ref<any[]>([])
|
||||
const cashPage = ref(1)
|
||||
const cashTotal = ref(0)
|
||||
const cashLoading = ref(false)
|
||||
const cashLoadingMore = ref(false)
|
||||
const cashFinished = ref(false)
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
onLoad(() => {
|
||||
loadCashHistory(true)
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
loadCashHistory(false)
|
||||
})
|
||||
|
||||
function loadCashHistory(reset: boolean) {
|
||||
if (!reset && (cashLoading.value || cashLoadingMore.value || cashFinished.value)) return
|
||||
if (reset) {
|
||||
cashLoading.value = true
|
||||
cashPage.value = 1
|
||||
cashFinished.value = false
|
||||
} else {
|
||||
cashLoadingMore.value = true
|
||||
}
|
||||
cashLog({ pageNumber: cashPage.value, pageSize: PAGE_SIZE, sort: 'createTime', order: 'desc' })
|
||||
.then((res: any) => {
|
||||
const result = res?.data?.result || {}
|
||||
const records: any[] = result.records || []
|
||||
cashTotal.value = Number(result.total || 0)
|
||||
cashList.value = reset ? records : [...cashList.value, ...records]
|
||||
if (records.length < PAGE_SIZE || cashList.value.length >= cashTotal.value) {
|
||||
cashFinished.value = true
|
||||
} else {
|
||||
cashPage.value += 1
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
cashLoading.value = false
|
||||
cashLoadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const CASH_STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
APPLY: { label: '审核中', cls: 'status-pending' },
|
||||
VIA_AUDITING: { label: '已通过', cls: 'status-success' },
|
||||
FAIL_AUDITING: { label: '已驳回', cls: 'status-fail' },
|
||||
}
|
||||
|
||||
function cashStatusLabel(status: string) {
|
||||
return CASH_STATUS_MAP[status]?.label || status
|
||||
}
|
||||
|
||||
function cashStatusClass(status: string) {
|
||||
return CASH_STATUS_MAP[status]?.cls || ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
padding: 16rpx 24rpx 40rpx;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
padding-top: 16rpx;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.log-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.log-sn {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
max-width: 360rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.log-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.log-amount-out {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cash-status-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff8e6;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #e8f8ec;
|
||||
color: #3aaa52;
|
||||
}
|
||||
|
||||
.status-fail {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
padding: 12rpx 0 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</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>
|
||||
485
pages/mine/distribution/invite.vue
Normal file
485
pages/mine/distribution/invite.vue
Normal file
@@ -0,0 +1,485 @@
|
||||
<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>
|
||||
|
||||
<template v-else>
|
||||
<view v-if="qrLoading || posterComposing" class="card state-card">
|
||||
<text class="state-text">{{ posterComposing ? '邀请卡合成中...' : qrLoadingText }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="qrError || composeError" class="card state-card">
|
||||
<text class="state-text">{{ composeError || qrError }}</text>
|
||||
<view class="retry-btn" @click="retryGenerate">重新生成</view>
|
||||
</view>
|
||||
|
||||
<image
|
||||
v-else-if="posterImage"
|
||||
class="poster-image"
|
||||
:src="posterImage"
|
||||
mode="widthFix"
|
||||
show-menu-by-longpress
|
||||
@click="previewPoster"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<view v-if="posterImage" class="footer-actions">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="btn primary share-btn" open-type="share">
|
||||
<text class="btn-text">微信分享</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<view class="save-btn" @click="savePoster">保存海报</view>
|
||||
<view class="hint-text">点击预览,长按或保存到相册</view>
|
||||
</view>
|
||||
|
||||
<view class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas type="2d" id="inviteQrCanvas" class="qr-canvas" />
|
||||
<canvas type="2d" id="inviteComposeCanvas" :style="composeCanvasStyle" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas canvas-id="inviteQrCanvas" class="qr-canvas" />
|
||||
<canvas canvas-id="inviteComposeCanvas" id="inviteComposeCanvas" :style="composeCanvasStyle" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { onLoad, onReady, 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'
|
||||
import {
|
||||
POSTER_WIDTH,
|
||||
POSTER_HEIGHT,
|
||||
composeDistributionPoster,
|
||||
savePosterToAlbum,
|
||||
} from '@/utils/distributionPosterCanvas'
|
||||
|
||||
const DEFAULT_JOIN_PAGE = 'pages/mine/distribution/join'
|
||||
const QR_CANVAS_SIZE = 280
|
||||
const COMPOSE_CANVAS_SELECTOR = 'inviteComposeCanvas'
|
||||
|
||||
function getCanvasPixelRatio() {
|
||||
return wx.getWindowInfo?.().pixelRatio || wx.getDeviceInfo?.().pixelRatio || 2
|
||||
}
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const composeCanvasStyle = {
|
||||
width: `${POSTER_WIDTH}px`,
|
||||
height: `${POSTER_HEIGHT}px`,
|
||||
}
|
||||
|
||||
let canvas2dNode: any = null
|
||||
|
||||
const pageLoading = ref(true)
|
||||
const pageLoadingText = ref('加载邀请卡设置...')
|
||||
const pageError = ref('')
|
||||
const qrLoading = ref(false)
|
||||
const qrLoadingText = ref('葵花码生成中...')
|
||||
const qrError = ref('')
|
||||
const qrImage = ref('')
|
||||
const posterImage = ref('')
|
||||
const posterComposing = ref(false)
|
||||
const composeError = ref('')
|
||||
const inviteContext = ref<Record<string, any>>({})
|
||||
|
||||
const avatarUrl = computed(() => parseGoodsImageUrl(inviteContext.value.memberAvatar))
|
||||
const backgroundUrl = computed(() => parseGoodsImageUrl(inviteContext.value.backgroundImage))
|
||||
|
||||
onLoad(() => {
|
||||
loadInviteCardSetting()
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
initCanvas2d()
|
||||
// #endif
|
||||
})
|
||||
|
||||
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: posterImage.value || 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 initCanvas2d() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (canvas2dNode) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(instance?.proxy)
|
||||
.select('#inviteQrCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res: any[]) => {
|
||||
const node = res?.[0]?.node
|
||||
if (!node) {
|
||||
reject(new Error('canvas 节点获取失败'))
|
||||
return
|
||||
}
|
||||
canvas2dNode = node
|
||||
const dpr = getCanvasPixelRatio()
|
||||
node.width = QR_CANVAS_SIZE * dpr
|
||||
node.height = QR_CANVAS_SIZE * dpr
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function exportQrByCanvas2d(qrPath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = canvas2dNode
|
||||
const ctx = canvas.getContext('2d')
|
||||
const dpr = getCanvasPixelRatio()
|
||||
ctx.scale(dpr, dpr)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
|
||||
const img = canvas.createImage()
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
setTimeout(() => {
|
||||
uni.canvasToTempFilePath(
|
||||
{
|
||||
canvas,
|
||||
width: QR_CANVAS_SIZE,
|
||||
height: QR_CANVAS_SIZE,
|
||||
destWidth: QR_CANVAS_SIZE,
|
||||
destHeight: QR_CANVAS_SIZE,
|
||||
success: (file: any) => resolve(file.tempFilePath),
|
||||
fail: () => reject(new Error('葵花码导出失败')),
|
||||
},
|
||||
instance?.proxy
|
||||
)
|
||||
}, 300)
|
||||
}
|
||||
img.onerror = () => reject(new Error('葵花码图片加载失败'))
|
||||
img.src = qrPath
|
||||
})
|
||||
}
|
||||
|
||||
function exportQrByCanvasLegacy(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 exportQrByCanvas(qrPath: string): Promise<string> {
|
||||
// #ifdef MP-WEIXIN
|
||||
await initCanvas2d()
|
||||
return exportQrByCanvas2d(qrPath)
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
return exportQrByCanvasLegacy(qrPath)
|
||||
// #endif
|
||||
}
|
||||
|
||||
async function resolveQrDisplay(result: string) {
|
||||
const tempPath = await writeBase64ToTemp(result)
|
||||
qrImage.value = await exportQrByCanvas(tempPath)
|
||||
}
|
||||
|
||||
async function composePoster() {
|
||||
if (!qrImage.value) return
|
||||
|
||||
posterComposing.value = true
|
||||
composeError.value = ''
|
||||
posterImage.value = ''
|
||||
|
||||
try {
|
||||
const ctx = inviteContext.value
|
||||
posterImage.value = await composeDistributionPoster(
|
||||
COMPOSE_CANVAS_SELECTOR,
|
||||
instance?.proxy || instance,
|
||||
{
|
||||
backgroundImage: backgroundUrl.value,
|
||||
memberAvatar: avatarUrl.value,
|
||||
memberName: ctx.memberName || '分销员',
|
||||
slogan: ctx.slogan || '',
|
||||
qrImage: qrImage.value,
|
||||
textColor: ctx.textColor || '#1f2a44',
|
||||
showMemberInfo: ctx.memberInfoVisible !== false,
|
||||
}
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error('compose invite card failed', error)
|
||||
composeError.value = error?.message || '邀请卡合成失败,请稍后重试'
|
||||
} finally {
|
||||
posterComposing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInviteCardSetting() {
|
||||
pageLoading.value = true
|
||||
pageLoadingText.value = '加载邀请卡设置...'
|
||||
pageError.value = ''
|
||||
qrImage.value = ''
|
||||
posterImage.value = ''
|
||||
qrError.value = ''
|
||||
composeError.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 = ''
|
||||
posterImage.value = ''
|
||||
composeError.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)
|
||||
await composePoster()
|
||||
} catch (error: any) {
|
||||
console.error('load invite qr code failed', error)
|
||||
qrError.value = error?.message || '葵花码生成失败,请稍后重试'
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function retryGenerate() {
|
||||
if (qrImage.value && composeError.value) {
|
||||
composePoster()
|
||||
return
|
||||
}
|
||||
loadQrCode()
|
||||
}
|
||||
|
||||
function previewPoster() {
|
||||
if (!posterImage.value) return
|
||||
uni.previewImage({
|
||||
current: posterImage.value,
|
||||
urls: [posterImage.value],
|
||||
})
|
||||
}
|
||||
|
||||
function savePoster() {
|
||||
savePosterToAlbum(posterImage.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,
|
||||
.save-btn {
|
||||
display: inline-block;
|
||||
margin-top: 24rpx;
|
||||
padding: 12rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 630 / 1000;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin-top: 24rpx;
|
||||
color: #b3b3b3;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-hide {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.qr-canvas {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +1,887 @@
|
||||
<template>
|
||||
<view class="wrapper" :style="themeStyle">
|
||||
<u-tabs
|
||||
:list="stepList"
|
||||
:scrollable="false"
|
||||
v-model:current="currentStep"
|
||||
:lineColor="lightColor"
|
||||
:activeStyle="{ color: lightColor }"
|
||||
></u-tabs>
|
||||
|
||||
<view class="feedBack-box">
|
||||
<up-form
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
ref="uFormRef"
|
||||
>
|
||||
<up-form-item label="会员昵称" prop="name">
|
||||
<u-input
|
||||
border="none"
|
||||
class="field-input"
|
||||
v-model="formData.name"
|
||||
:custom-style="fieldInputStyle"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item label="账户类型" prop="name"></up-form-item>
|
||||
<up-form-item label="收款人姓名" prop="name">
|
||||
<u-input
|
||||
border="none"
|
||||
class="field-input"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入收款人姓名"
|
||||
:custom-style="fieldInputStyle"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item label="收款账号" prop="name">
|
||||
<u-input
|
||||
border="none"
|
||||
class="field-input"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入收款人账号"
|
||||
:custom-style="fieldInputStyle"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item label="银行名称" prop="name">
|
||||
<u-input
|
||||
border="none"
|
||||
class="field-input"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入开户银行支行名称"
|
||||
:custom-style="fieldInputStyle"
|
||||
/>
|
||||
</up-form-item>
|
||||
</up-form>
|
||||
<view 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': showLoginFooter || showManualApply || isApplyPending }">
|
||||
<!-- 邀请人信息(来自图文邀请卡) -->
|
||||
<view class="inviter-header" v-if="showInviterHeader">
|
||||
<image class="inviter-avatar" :src="inviterAvatar" mode="aspectFill" />
|
||||
<view class="inviter-meta">
|
||||
<view class="inviter-label">邀请您加入</view>
|
||||
<view class="inviter-name">{{ inviterName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit" @click="submitForm">提交</view>
|
||||
<!-- 招募海报 -->
|
||||
<view class="section poster-section" v-if="showPoster">
|
||||
<image
|
||||
class="poster-image"
|
||||
:src="plan.posterBackground"
|
||||
mode="widthFix"
|
||||
@click="previewPoster"
|
||||
/>
|
||||
<view class="poster-text" v-if="posterTitle">{{ posterTitle }}</view>
|
||||
<view class="poster-benefit" v-if="posterBenefitText">{{ posterBenefitText }}</view>
|
||||
<view class="poster-rule" v-if="posterRuleText">{{ posterRuleText }}</view>
|
||||
<view class="poster-contact" v-if="posterContact">联系方式:{{ posterContact }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 加入条件 -->
|
||||
<view class="section" v-if="hasJoinCondition">
|
||||
<view class="title">加入条件</view>
|
||||
<view class="condition-tip">
|
||||
{{ isUserLoggedIn ? '满足以下条件后可申请成为分销员' : '登录后可查看您的达标进度并申请成为分销员' }}
|
||||
</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="showLoginFooter">
|
||||
<view class="apply-footer-card">
|
||||
<view class="login-tip">登录后即可查看加入条件并申请成为分销员</view>
|
||||
<view class="submit" @click="goLogin">立即登录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="apply-footer-fixed" v-else-if="showManualApply || isApplyPending">
|
||||
<view class="apply-footer-card">
|
||||
<view class="agree" v-if="showManualApply" @click="agreed = !agreed">
|
||||
<checkbox :checked="agreed" @click.stop="agreed = !agreed" />
|
||||
<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 storage from '@/utils/storage.js'
|
||||
import config from '@/config/config'
|
||||
import { parseGoodsImageUrl, tipsToLogin } from '@/utils/filters.js'
|
||||
import { resolveRecruitDistributionIdFromScene } from '@/utils/distributionBind.js'
|
||||
import { getRecruitPage, getRecruitProgress, getRecruitInviter, submitRecruitApplication } from '@/api/distribution'
|
||||
|
||||
const defaultAvatar = config.defaultUserPhoto
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
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 inviterInfo = ref<any>(null)
|
||||
|
||||
const stepList = [
|
||||
{ name: '推广人资料' },
|
||||
{ name: '平台审核' },
|
||||
{ name: '完成' },
|
||||
]
|
||||
const isUserLoggedIn = computed(() => !!storage.getAccessToken())
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
radio: '',
|
||||
const plan = computed(() => pageInfo.value.plan || null)
|
||||
const fields = computed(() => pageInfo.value.fields || [])
|
||||
const recruitGoods = computed(() => plan.value?.recruitGoods || [])
|
||||
const agreementContent = computed(() => plan.value?.agreementContent || '')
|
||||
|
||||
const showPoster = computed(() => {
|
||||
const p = plan.value
|
||||
if (!p) return false
|
||||
if (p.posterEnabled === false) return false
|
||||
return !!(p.posterBackground && String(p.posterBackground).trim())
|
||||
})
|
||||
|
||||
const 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 showLoginFooter = computed(() => {
|
||||
return !!plan.value && !isUserLoggedIn.value && !loading.value
|
||||
})
|
||||
|
||||
const showInviterHeader = computed(() => {
|
||||
if (!recruitToken.value) return false
|
||||
const info = inviterInfo.value
|
||||
return !!(info?.memberName || info?.memberAvatar)
|
||||
})
|
||||
|
||||
const inviterName = computed(() => inviterInfo.value?.memberName || '分销员')
|
||||
const inviterAvatar = computed(() => parseGoodsImageUrl(inviterInfo.value?.memberAvatar) || defaultAvatar)
|
||||
|
||||
const showManualApply = computed(() => {
|
||||
return isUserLoggedIn.value && !showApplyResult.value && conditionsMet.value && isManualApply.value
|
||||
})
|
||||
|
||||
const showAutoApplyTip = computed(() => {
|
||||
return isUserLoggedIn.value && !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(async (query) => {
|
||||
let token = query?.token || query?.distributionId || ''
|
||||
if (!token && query?.scene) {
|
||||
token = await resolveRecruitDistributionIdFromScene(query.scene)
|
||||
}
|
||||
recruitToken.value = token ? String(token) : ''
|
||||
if (recruitToken.value) {
|
||||
loadInviterInfo(recruitToken.value)
|
||||
}
|
||||
})
|
||||
|
||||
function loadInviterInfo(distributionId: string) {
|
||||
getRecruitInviter(distributionId).then((res) => {
|
||||
if (res.data?.success) {
|
||||
inviterInfo.value = res.data.result || null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
uFormRef.value?.setRules(rules)
|
||||
function emptyProgress() {
|
||||
return {
|
||||
satisfied: false,
|
||||
currentSelfPurchaseAmount: 0,
|
||||
currentConsumeCount: 0,
|
||||
purchaseGoodsSatisfied: false,
|
||||
}
|
||||
}
|
||||
|
||||
function hasConditionalJoin(planData: any) {
|
||||
if (!planData || planData.joinConditionType !== 'CONDITIONAL') return false
|
||||
return !!(planData.requireSelfPurchaseAmount || planData.requireConsumeCount || planData.requirePurchaseGoods)
|
||||
}
|
||||
|
||||
function goLogin() {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.navigateTo({ url: '/pages/passport/wechatMPLogin' })
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({ url: '/pages/passport/login' })
|
||||
// #endif
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
function submitForm() {
|
||||
uFormRef.value?.validate().catch(() => {
|
||||
uni.showToast({ title: '请填写有效信息', icon: 'none' })
|
||||
function loadPageData() {
|
||||
loading.value = true
|
||||
applySubmitted.value = false
|
||||
submitAuditMode.value = ''
|
||||
submitApplicationStatus.value = ''
|
||||
formInfoCompleted.value = false
|
||||
Object.keys(fieldErrors).forEach((key) => delete fieldErrors[key])
|
||||
const tasks: Promise<any>[] = [getRecruitPage()]
|
||||
if (isUserLoggedIn.value) {
|
||||
tasks.push(getRecruitProgress())
|
||||
}
|
||||
Promise.all(tasks)
|
||||
.then((results) => {
|
||||
const pageRes = results[0]
|
||||
const pageResult = pageRes.data?.result || {}
|
||||
pageInfo.value = pageResult
|
||||
if (results.length > 1) {
|
||||
progress.value = results[1].data?.result || emptyProgress()
|
||||
} else {
|
||||
progress.value = hasConditionalJoin(pageResult.plan) ? emptyProgress() : { satisfied: true }
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatMoney(val: number | string | undefined) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function previewPoster() {
|
||||
if (!plan.value?.posterBackground) return
|
||||
uni.previewImage({
|
||||
urls: [plan.value.posterBackground],
|
||||
current: plan.value.posterBackground,
|
||||
})
|
||||
}
|
||||
|
||||
function goDistributionHome() {
|
||||
uni.redirectTo({
|
||||
url: '/pages/mine/distribution/home',
|
||||
})
|
||||
}
|
||||
|
||||
function openAgreement() {
|
||||
agreementVisible.value = true
|
||||
}
|
||||
|
||||
function clearFieldError(fieldId: string) {
|
||||
if (fieldErrors[fieldId]) {
|
||||
delete fieldErrors[fieldId]
|
||||
}
|
||||
}
|
||||
|
||||
function validateFormFields() {
|
||||
let valid = true
|
||||
Object.keys(fieldErrors).forEach((key) => delete fieldErrors[key])
|
||||
fields.value.forEach((field: any) => {
|
||||
if (!field.required) return
|
||||
const value = formItems[field.id]
|
||||
if (value == null || String(value).trim() === '') {
|
||||
fieldErrors[field.id] = `请填写${field.fieldName}`
|
||||
valid = false
|
||||
}
|
||||
})
|
||||
return valid
|
||||
}
|
||||
|
||||
function confirmApplyForm() {
|
||||
if (!validateFormFields()) {
|
||||
return
|
||||
}
|
||||
formInfoCompleted.value = true
|
||||
applyFormVisible.value = false
|
||||
uni.showToast({ title: '请点击申请成为分销员', icon: 'none' })
|
||||
}
|
||||
|
||||
function doSubmit() {
|
||||
const formItemList = showApplyForm.value
|
||||
? fields.value.map((field: any) => ({
|
||||
fieldId: field.id,
|
||||
fieldValue: formItems[field.id] || '',
|
||||
}))
|
||||
: []
|
||||
submitRecruitApplication({
|
||||
recruitToken: recruitToken.value,
|
||||
agreementAccepted: agreed.value,
|
||||
formItems: formItemList,
|
||||
}).then((res) => {
|
||||
if (res.data?.success) {
|
||||
handleApplySuccess(res.data.result)
|
||||
if (isApplyApproved.value) {
|
||||
uni.showToast({ title: '申请成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '已提交,等待审核', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: res.data?.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleApplySuccess(application: any) {
|
||||
const auditMode = application?.auditMode || plan.value?.auditMode || 'MANUAL'
|
||||
const status = application?.applicationStatus || 'PENDING'
|
||||
applySubmitted.value = true
|
||||
submitAuditMode.value = auditMode
|
||||
submitApplicationStatus.value = status
|
||||
if (auditMode === 'AUTO_PASS' || status === 'APPROVED') {
|
||||
pageInfo.value.distributionStatus = 'PASS'
|
||||
if (pageInfo.value.currentApplication) {
|
||||
pageInfo.value.currentApplication.applicationStatus = 'APPROVED'
|
||||
}
|
||||
} else if (pageInfo.value.currentApplication) {
|
||||
pageInfo.value.currentApplication.applicationStatus = 'PENDING'
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!tipsToLogin('normal')) {
|
||||
return
|
||||
}
|
||||
if (!showManualApply.value) {
|
||||
return
|
||||
}
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意分销推广服务协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (showApplyForm.value && !formInfoCompleted.value) {
|
||||
applyFormVisible.value = true
|
||||
return
|
||||
}
|
||||
doSubmit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
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;
|
||||
}
|
||||
|
||||
.inviter-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.inviter-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: #f5f5f5;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inviter-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.inviter-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.inviter-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
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>
|
||||
421
pages/mine/distribution/poster.vue
Normal file
421
pages/mine/distribution/poster.vue
Normal file
@@ -0,0 +1,421 @@
|
||||
<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>
|
||||
|
||||
<template v-else>
|
||||
<view v-if="qrLoading || posterComposing" class="card state-card">
|
||||
<text class="state-text">{{ posterComposing ? '海报合成中...' : qrLoadingText }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="qrError || composeError" class="card state-card">
|
||||
<text class="state-text">{{ composeError || qrError }}</text>
|
||||
<view class="retry-btn" @click="retryGenerate">重新生成</view>
|
||||
</view>
|
||||
|
||||
<image
|
||||
v-else-if="posterImage"
|
||||
class="poster-image"
|
||||
:src="posterImage"
|
||||
mode="widthFix"
|
||||
show-menu-by-longpress
|
||||
@click="previewPoster"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<view v-if="posterImage" class="hint-fixed">长按保存分享</view>
|
||||
|
||||
<view class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas type="2d" id="qrCanvas" class="qr-canvas" />
|
||||
<canvas type="2d" id="posterComposeCanvas" :style="composeCanvasStyle" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas canvas-id="qrCanvas" class="qr-canvas" />
|
||||
<canvas canvas-id="posterComposeCanvas" id="posterComposeCanvas" :style="composeCanvasStyle" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { onLoad, onReady, 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'
|
||||
import {
|
||||
POSTER_WIDTH,
|
||||
POSTER_HEIGHT,
|
||||
composeDistributionPoster,
|
||||
} from '@/utils/distributionPosterCanvas'
|
||||
|
||||
const DEFAULT_HOME_PAGE = 'pages/tabbar/home/index'
|
||||
const QR_CANVAS_SIZE = 280
|
||||
const COMPOSE_CANVAS_SELECTOR = 'posterComposeCanvas'
|
||||
|
||||
function getCanvasPixelRatio() {
|
||||
return wx.getWindowInfo?.().pixelRatio || wx.getDeviceInfo?.().pixelRatio || 2
|
||||
}
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const composeCanvasStyle = {
|
||||
width: `${POSTER_WIDTH}px`,
|
||||
height: `${POSTER_HEIGHT}px`,
|
||||
}
|
||||
|
||||
let canvas2dNode: any = null
|
||||
|
||||
const pageLoading = ref(true)
|
||||
const pageLoadingText = ref('加载海报设置...')
|
||||
const pageError = ref('')
|
||||
const qrLoading = ref(false)
|
||||
const qrLoadingText = ref('葵花码生成中...')
|
||||
const qrError = ref('')
|
||||
const qrImage = ref('')
|
||||
const posterImage = ref('')
|
||||
const posterComposing = ref(false)
|
||||
const composeError = ref('')
|
||||
const posterContext = ref<Record<string, any>>({})
|
||||
|
||||
onLoad(() => {
|
||||
loadPosterSetting()
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
initCanvas2d()
|
||||
// #endif
|
||||
})
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const ctx = posterContext.value
|
||||
const sharePage = normalizeSharePage(ctx.sharePage)
|
||||
return {
|
||||
title: ctx.slogan || '邀请你一起逛逛',
|
||||
path: `/${sharePage}`,
|
||||
imageUrl: posterImage.value || 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 initCanvas2d() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (canvas2dNode) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(instance?.proxy)
|
||||
.select('#qrCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res: any[]) => {
|
||||
const node = res?.[0]?.node
|
||||
if (!node) {
|
||||
reject(new Error('canvas 节点获取失败'))
|
||||
return
|
||||
}
|
||||
canvas2dNode = node
|
||||
const dpr = getCanvasPixelRatio()
|
||||
node.width = QR_CANVAS_SIZE * dpr
|
||||
node.height = QR_CANVAS_SIZE * dpr
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function exportQrByCanvas2d(qrPath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = canvas2dNode
|
||||
const ctx = canvas.getContext('2d')
|
||||
const dpr = getCanvasPixelRatio()
|
||||
ctx.scale(dpr, dpr)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
|
||||
const img = canvas.createImage()
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
|
||||
setTimeout(() => {
|
||||
uni.canvasToTempFilePath(
|
||||
{
|
||||
canvas,
|
||||
width: QR_CANVAS_SIZE,
|
||||
height: QR_CANVAS_SIZE,
|
||||
destWidth: QR_CANVAS_SIZE,
|
||||
destHeight: QR_CANVAS_SIZE,
|
||||
success: (file: any) => resolve(file.tempFilePath),
|
||||
fail: () => reject(new Error('葵花码导出失败')),
|
||||
},
|
||||
instance?.proxy
|
||||
)
|
||||
}, 300)
|
||||
}
|
||||
img.onerror = () => reject(new Error('葵花码图片加载失败'))
|
||||
img.src = qrPath
|
||||
})
|
||||
}
|
||||
|
||||
function exportQrByCanvasLegacy(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 exportQrByCanvas(qrPath: string): Promise<string> {
|
||||
// #ifdef MP-WEIXIN
|
||||
await initCanvas2d()
|
||||
return exportQrByCanvas2d(qrPath)
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
return exportQrByCanvasLegacy(qrPath)
|
||||
// #endif
|
||||
}
|
||||
|
||||
async function resolveQrDisplay(result: string) {
|
||||
const tempPath = await writeBase64ToTemp(result)
|
||||
qrImage.value = await exportQrByCanvas(tempPath)
|
||||
}
|
||||
|
||||
async function composePoster() {
|
||||
if (!qrImage.value) return
|
||||
|
||||
posterComposing.value = true
|
||||
composeError.value = ''
|
||||
posterImage.value = ''
|
||||
|
||||
try {
|
||||
const ctx = posterContext.value
|
||||
posterImage.value = await composeDistributionPoster(
|
||||
COMPOSE_CANVAS_SELECTOR,
|
||||
instance?.proxy || instance,
|
||||
{
|
||||
backgroundImage: backgroundUrl.value,
|
||||
memberAvatar: avatarUrl.value,
|
||||
memberName: ctx.memberName || '分销员',
|
||||
slogan: ctx.slogan || '',
|
||||
qrImage: qrImage.value,
|
||||
textColor: ctx.textColor || '#1f2a44',
|
||||
showMemberInfo: ctx.memberInfoVisible !== false,
|
||||
}
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error('compose poster failed', error)
|
||||
composeError.value = error?.message || '海报合成失败,请稍后重试'
|
||||
} finally {
|
||||
posterComposing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPosterSetting() {
|
||||
pageLoading.value = true
|
||||
pageLoadingText.value = '加载海报设置...'
|
||||
pageError.value = ''
|
||||
qrImage.value = ''
|
||||
posterImage.value = ''
|
||||
qrError.value = ''
|
||||
composeError.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 = ''
|
||||
posterImage.value = ''
|
||||
composeError.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)
|
||||
await composePoster()
|
||||
} catch (error: any) {
|
||||
console.error('load qr code failed', error)
|
||||
qrError.value = error?.message || '葵花码生成失败,请稍后重试'
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function retryGenerate() {
|
||||
if (qrImage.value && composeError.value) {
|
||||
composePoster()
|
||||
return
|
||||
}
|
||||
loadQrCode()
|
||||
}
|
||||
|
||||
function previewPoster() {
|
||||
if (!posterImage.value) return
|
||||
uni.previewImage({
|
||||
current: posterImage.value,
|
||||
urls: [posterImage.value],
|
||||
})
|
||||
}
|
||||
|
||||
const avatarUrl = computed(() => parseGoodsImageUrl(posterContext.value.memberAvatar))
|
||||
const backgroundUrl = computed(() => parseGoodsImageUrl(posterContext.value.backgroundImage))
|
||||
</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;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 630 / 1000;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hint-fixed {
|
||||
margin-top: 24rpx;
|
||||
color: #b3b3b3;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-hide {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.qr-canvas {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
</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>
|
||||
191
pages/mine/distribution/wallet-log.vue
Normal file
191
pages/mine/distribution/wallet-log.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view v-if="walletLoading && !walletList.length" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!walletList.length" class="state-wrap">
|
||||
<text class="state-text">暂无流水记录</text>
|
||||
</view>
|
||||
<view v-else class="log-list">
|
||||
<view class="log-item" v-for="item in walletList" :key="item.id">
|
||||
<view class="log-left">
|
||||
<text class="log-title">{{ logTypeLabel(item.logType) }}</text>
|
||||
<text class="log-time">{{ formatTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view class="log-right">
|
||||
<text class="log-amount" :class="logAmountClass(item.logType)">
|
||||
{{ logAmountSign(item.logType) }}{{ formatMoney(Math.abs(item.changeAmount)) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-footer">
|
||||
<text v-if="walletFinished && walletList.length" class="footer-text">没有更多数据了</text>
|
||||
<text v-else-if="walletLoadingMore" class="footer-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getWalletLog } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const walletList = ref<any[]>([])
|
||||
const walletPage = ref(1)
|
||||
const walletTotal = ref(0)
|
||||
const walletLoading = ref(false)
|
||||
const walletLoadingMore = ref(false)
|
||||
const walletFinished = ref(false)
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
onLoad(() => {
|
||||
loadWalletLog(true)
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
loadWalletLog(false)
|
||||
})
|
||||
|
||||
function loadWalletLog(reset: boolean) {
|
||||
if (!reset && (walletLoading.value || walletLoadingMore.value || walletFinished.value)) return
|
||||
if (reset) {
|
||||
walletLoading.value = true
|
||||
walletPage.value = 1
|
||||
walletFinished.value = false
|
||||
} else {
|
||||
walletLoadingMore.value = true
|
||||
}
|
||||
getWalletLog({ pageNumber: walletPage.value, pageSize: PAGE_SIZE })
|
||||
.then((res: any) => {
|
||||
const result = res?.data?.result || {}
|
||||
const records: any[] = result.records || []
|
||||
walletTotal.value = Number(result.total || 0)
|
||||
walletList.value = reset ? records : [...walletList.value, ...records]
|
||||
if (records.length < PAGE_SIZE || walletList.value.length >= walletTotal.value) {
|
||||
walletFinished.value = true
|
||||
} else {
|
||||
walletPage.value += 1
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
walletLoading.value = false
|
||||
walletLoadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const LOG_TYPE_MAP: Record<string, { label: string; income: boolean }> = {
|
||||
COMMISSION_FREEZE: { label: '佣金待结算', income: false },
|
||||
COMMISSION_SETTLE: { label: '销售员佣金', income: true },
|
||||
COMMISSION_REFUND: { label: '佣金退回', income: false },
|
||||
WITHDRAW_APPLY: { label: '提现申请', income: false },
|
||||
WITHDRAW_APPROVE: { label: '提现到账', income: false },
|
||||
WITHDRAW_REJECT: { label: '提现驳回', income: true },
|
||||
}
|
||||
|
||||
function logTypeLabel(type: string) {
|
||||
return LOG_TYPE_MAP[type]?.label || type
|
||||
}
|
||||
|
||||
function logAmountClass(type: string) {
|
||||
return LOG_TYPE_MAP[type]?.income ? 'log-amount-in' : 'log-amount-out'
|
||||
}
|
||||
|
||||
function logAmountSign(type: string) {
|
||||
return LOG_TYPE_MAP[type]?.income ? '+' : '-'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
padding: 16rpx 24rpx 40rpx;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
padding-top: 16rpx;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.log-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.log-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.log-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.log-amount-in {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
.log-amount-out {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
padding: 12rpx 0 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
502
pages/mine/distribution/withdraw-apply.vue
Normal file
502
pages/mine/distribution/withdraw-apply.vue
Normal file
@@ -0,0 +1,502 @@
|
||||
<template>
|
||||
<view class="page" :style="themeStyle">
|
||||
<view class="content">
|
||||
<!-- 到账账户 -->
|
||||
<view class="section-card">
|
||||
<view class="account-row" @click="openAccountDrawer">
|
||||
<text class="account-label">到账账户</text>
|
||||
<view class="account-right">
|
||||
<view v-if="selectedAccount" class="account-info">
|
||||
<text class="account-bank">{{ selectedAccount.bankName }}</text>
|
||||
<text class="account-card">{{ maskCard(selectedAccount.cardNo) }}</text>
|
||||
</view>
|
||||
<text v-else class="account-add-text">请添加</text>
|
||||
<u-icon name="arrow-right" color="#bbb" size="14" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现金额 -->
|
||||
<view class="section-card amount-card">
|
||||
<text class="amount-label">提现金额</text>
|
||||
<view class="amount-input-wrap">
|
||||
<text class="amount-symbol">¥</text>
|
||||
<input
|
||||
class="amount-input"
|
||||
type="digit"
|
||||
v-model="amount"
|
||||
placeholder="0.00"
|
||||
placeholder-class="amount-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="amount-divider" />
|
||||
<view class="amount-hint-row">
|
||||
<text class="amount-balance-hint">可提现余额 ¥{{ formatMoney(canRebate) }}</text>
|
||||
<text class="amount-all" @click="fillAll">全部提现</text>
|
||||
</view>
|
||||
<text v-if="errorMsg" class="amount-error">{{ errorMsg }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提现按钮 -->
|
||||
<view
|
||||
class="submit-btn"
|
||||
:class="{ 'submit-btn--disabled': !canSubmit }"
|
||||
@click="submitWithdraw"
|
||||
>{{ submitLoading ? '提交中...' : '提现' }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择账户抽屉 -->
|
||||
<u-popup v-model:show="drawerVisible" mode="bottom" round="16">
|
||||
<view class="drawer-wrap">
|
||||
<view class="drawer-header">
|
||||
<text class="drawer-title">选择账户</text>
|
||||
<view class="drawer-close" @click="drawerVisible = false">
|
||||
<u-icon name="close" color="#999" size="18" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="drawerLoading" class="drawer-state">
|
||||
<text class="drawer-state-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="allCards.length" class="drawer-list">
|
||||
<view
|
||||
class="drawer-item"
|
||||
v-for="item in allCards"
|
||||
:key="item.id"
|
||||
@click="selectAccount(item)"
|
||||
>
|
||||
<view class="radio-circle" :class="{ active: selectedAccount?.id === item.id }">
|
||||
<view class="radio-dot" v-if="selectedAccount?.id === item.id" />
|
||||
</view>
|
||||
<view class="drawer-item-info">
|
||||
<text class="drawer-item-name">{{ item.holderName }}</text>
|
||||
<view class="drawer-item-meta">
|
||||
<text class="drawer-item-bank">{{ item.bankName }}</text>
|
||||
<text class="drawer-item-card">{{ maskCard(item.cardNo) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="drawer-state">
|
||||
<text class="drawer-state-text">暂无银行卡</text>
|
||||
</view>
|
||||
|
||||
<view class="drawer-add-btn" @click="openAddForm">+ 添加账户</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 添加账户 Popup -->
|
||||
<u-popup v-model:show="addPopupVisible" mode="bottom" round="16">
|
||||
<view class="popup-wrap">
|
||||
<view class="popup-title">添加到账账户</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">姓名</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.holderName"
|
||||
placeholder="请输入收款人姓名"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-divider" />
|
||||
<view class="form-item">
|
||||
<text class="form-label">银行</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.bankName"
|
||||
placeholder="如:中国工商银行"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-divider" />
|
||||
<view class="form-item">
|
||||
<text class="form-label">卡号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.cardNo"
|
||||
type="number"
|
||||
placeholder="请输入银行卡号"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="addPopupVisible = false">取消</view>
|
||||
<view
|
||||
class="popup-btn primary"
|
||||
:class="{ disabled: saveLoading }"
|
||||
@click="saveAccount"
|
||||
>{{ saveLoading ? '保存中...' : '保存' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</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 { distribution, cash, getBankCards, addBankCard } from '@/api/distribution'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const canRebate = ref(0)
|
||||
const amount = ref('')
|
||||
const submitLoading = ref(false)
|
||||
const selectedAccount = ref<any>(null)
|
||||
const allCards = ref<any[]>([])
|
||||
const inited = ref(false)
|
||||
|
||||
// 抽屉
|
||||
const drawerVisible = ref(false)
|
||||
const drawerLoading = ref(false)
|
||||
|
||||
// 添加账户弹窗
|
||||
const addPopupVisible = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const form = ref({ holderName: '', bankName: '', cardNo: '' })
|
||||
|
||||
function loadInfo() {
|
||||
distribution().then((res: any) => {
|
||||
const d = res?.data?.result || {}
|
||||
canRebate.value = Number(d.canRebate || 0)
|
||||
if (!inited.value) {
|
||||
const prefill = Math.min(canRebate.value, 9999)
|
||||
amount.value = prefill > 0 ? prefill.toFixed(2) : ''
|
||||
inited.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadCards() {
|
||||
drawerLoading.value = true
|
||||
getBankCards()
|
||||
.then((res: any) => {
|
||||
allCards.value = res?.data?.result || []
|
||||
if (!selectedAccount.value && allCards.value.length) {
|
||||
const def = allCards.value.find((a: any) => a.isDefault) || allCards.value[0]
|
||||
selectedAccount.value = def
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
drawerLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadInfo()
|
||||
loadCards()
|
||||
})
|
||||
|
||||
function openAccountDrawer() {
|
||||
drawerVisible.value = true
|
||||
loadCards()
|
||||
}
|
||||
|
||||
function selectAccount(item: any) {
|
||||
selectedAccount.value = item
|
||||
drawerVisible.value = false
|
||||
}
|
||||
|
||||
function openAddForm() {
|
||||
form.value = { holderName: '', bankName: '', cardNo: '' }
|
||||
addPopupVisible.value = true
|
||||
}
|
||||
|
||||
function saveAccount() {
|
||||
if (!form.value.holderName.trim()) {
|
||||
uni.showToast({ title: '请输入姓名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.value.bankName.trim()) {
|
||||
uni.showToast({ title: '请输入银行名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.value.cardNo.trim()) {
|
||||
uni.showToast({ title: '请输入银行卡号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saveLoading.value = true
|
||||
addBankCard({
|
||||
holderName: form.value.holderName.trim(),
|
||||
bankName: form.value.bankName.trim(),
|
||||
cardNo: form.value.cardNo.trim(),
|
||||
})
|
||||
.then((res: any) => {
|
||||
const newCard = res?.data?.result
|
||||
addPopupVisible.value = false
|
||||
uni.showToast({ title: '添加成功', icon: 'success' })
|
||||
loadCards()
|
||||
if (newCard?.id) {
|
||||
selectedAccount.value = newCard
|
||||
drawerVisible.value = false
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const msg = err?.data?.message || '添加失败,请重试'
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
saveLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function fillAll() {
|
||||
const max = Math.min(canRebate.value, 9999)
|
||||
amount.value = max.toFixed(2)
|
||||
}
|
||||
|
||||
const errorMsg = computed(() => {
|
||||
if (!amount.value) return ''
|
||||
const price = Number(amount.value)
|
||||
if (price < 1) return '最小提现金额为1元'
|
||||
if (price > 9999) return '单次提现最多9999元'
|
||||
if (price > canRebate.value) return '提现金额不能超过可提现余额'
|
||||
return ''
|
||||
})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
const price = Number(amount.value)
|
||||
return !submitLoading.value && price >= 1 && price <= 9999 && price <= canRebate.value
|
||||
})
|
||||
|
||||
function submitWithdraw() {
|
||||
if (!selectedAccount.value) {
|
||||
uni.showToast({ title: '请先添加到账账户', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!canSubmit.value) return
|
||||
const price = Number(amount.value)
|
||||
submitLoading.value = true
|
||||
cash({ price })
|
||||
.then(() => {
|
||||
uni.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const msg = err?.data?.message || '提现失败,请重试'
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatMoney(val: number) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function maskCard(cardNo: string) {
|
||||
if (!cardNo || cardNo.length < 4) return cardNo
|
||||
return '****' + cardNo.slice(-4)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.content { padding: 24rpx; }
|
||||
|
||||
.section-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 0 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.account-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.account-label { font-size: 28rpx; color: #333; font-weight: 500; }
|
||||
.account-right { display: flex; align-items: center; gap: 8rpx; }
|
||||
.account-info { display: flex; align-items: center; gap: 10rpx; }
|
||||
.account-bank { font-size: 24rpx; color: #999; }
|
||||
.account-card { font-size: 24rpx; color: #999; }
|
||||
.account-add-text { font-size: 26rpx; color: var(--theme-primary); }
|
||||
|
||||
.amount-card { padding: 32rpx; }
|
||||
|
||||
.amount-label { font-size: 26rpx; color: #999; display: block; margin-bottom: 20rpx; }
|
||||
|
||||
.amount-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.amount-symbol {
|
||||
font-size: 36rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 80rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
flex: 1;
|
||||
font-size: 60rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.amount-placeholder { font-size: 60rpx; color: #ddd; font-weight: 300; }
|
||||
|
||||
.amount-divider { height: 1rpx; background: #f5f5f5; margin-bottom: 20rpx; }
|
||||
|
||||
.amount-hint-row { display: flex; align-items: center; justify-content: space-between; }
|
||||
|
||||
.amount-balance-hint { font-size: 24rpx; color: #999; }
|
||||
.amount-all { font-size: 24rpx; color: var(--theme-primary); }
|
||||
|
||||
.amount-error { display: block; font-size: 24rpx; color: #f56c6c; margin-top: 16rpx; }
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 16rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 16rpx;
|
||||
background: var(--theme-primary);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
box-shadow: 0 8rpx 24rpx var(--theme-primary-30);
|
||||
}
|
||||
|
||||
.submit-btn--disabled { opacity: 0.5; box-shadow: none; }
|
||||
|
||||
/* ── 选择账户抽屉 ──────────────────────────── */
|
||||
.drawer-wrap { padding: 0 0 48rpx; }
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.drawer-title { font-size: 30rpx; font-weight: 600; color: #333; }
|
||||
|
||||
.drawer-close {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.drawer-list { max-height: 600rpx; overflow-y: auto; }
|
||||
|
||||
.drawer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f8f8f8;
|
||||
}
|
||||
|
||||
.radio-circle {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
&.active { border-color: var(--theme-primary); }
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--theme-primary);
|
||||
}
|
||||
|
||||
.drawer-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drawer-item-name { font-size: 28rpx; font-weight: 500; color: #333; }
|
||||
|
||||
.drawer-item-meta { display: flex; align-items: center; gap: 12rpx; }
|
||||
|
||||
.drawer-item-bank { font-size: 24rpx; color: #666; }
|
||||
.drawer-item-card { font-size: 24rpx; color: #999; }
|
||||
|
||||
.drawer-state { padding: 60rpx 0; text-align: center; }
|
||||
.drawer-state-text { font-size: 26rpx; color: #ccc; }
|
||||
|
||||
.drawer-add-btn {
|
||||
margin: 24rpx 32rpx 0;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid var(--theme-primary);
|
||||
color: var(--theme-primary);
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── 添加账户弹窗 ──────────────────────────── */
|
||||
.popup-wrap { padding: 32rpx 28rpx 48rpx; }
|
||||
|
||||
.popup-title {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-item { display: flex; align-items: center; padding: 18rpx 0; }
|
||||
|
||||
.form-label { font-size: 28rpx; color: #333; width: 100rpx; flex-shrink: 0; }
|
||||
|
||||
.form-input { flex: 1; font-size: 28rpx; color: #333; height: 64rpx; }
|
||||
|
||||
.form-placeholder { color: #ccc; font-size: 26rpx; }
|
||||
|
||||
.form-divider { height: 1rpx; background: #f5f5f5; }
|
||||
|
||||
.popup-actions { display: flex; gap: 20rpx; margin-top: 32rpx; }
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-btn.ghost { color: #666; background: #f5f5f5; }
|
||||
.popup-btn.primary { color: #fff; background: var(--theme-primary); }
|
||||
.popup-btn.disabled { opacity: 0.5; }
|
||||
</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'
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="bagbar">{{ formatSendTime(row.send_time) }}</div>
|
||||
</div>
|
||||
<u-card
|
||||
@click="navigateToLogisticsDetail(row.sn, row.logi_id, row.ship_no)"
|
||||
@click="navigateToLogisticsDetail(row.sn)"
|
||||
:title="pageTitle"
|
||||
title-color="#666666"
|
||||
title-size="24"
|
||||
@@ -69,9 +69,9 @@ function extractRecords(res: any) {
|
||||
return []
|
||||
}
|
||||
|
||||
function navigateToLogisticsDetail(sn: string, logiId: string, shipNo: string) {
|
||||
function navigateToLogisticsDetail(sn: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/msgTips/packageMsg/logisticsDetail?order_sn=${sn}&logi_id=${logiId}&ship_no=${shipNo}`,
|
||||
url: `/pages/order/deliverDetail?order_sn=${sn}`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +1,52 @@
|
||||
<template>
|
||||
<view class="logistics-detail">
|
||||
<view class="card">
|
||||
<view class="card-title">
|
||||
<span>{{ logisticsInfo.shipper }}</span>快递 <span>{{ logisticsInfo.logisticCode }}</span>
|
||||
</view>
|
||||
<view class="time-line">
|
||||
<u-time-line v-if="logisticsInfo.traces && logisticsInfo.traces.length != 0">
|
||||
<u-time-line-item nodeTop="2" v-for="(item, index) in logisticsInfo.traces" :key="index">
|
||||
<template #node>
|
||||
<view
|
||||
v-if="index == logisticsInfo.traces.length - 1"
|
||||
class="u-node"
|
||||
:style="{ background: lightColor }"
|
||||
style="padding: 0 4px"
|
||||
>
|
||||
<u-icon name="pushpin-fill" color="#fff" :size="24"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
<template #content>
|
||||
<view>
|
||||
<view class="u-order-desc">{{ item.AcceptStation }}</view>
|
||||
<view class="u-order-time">{{ item.AcceptTime }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</u-time-line-item>
|
||||
</u-time-line>
|
||||
<u-empty class="empty" v-else text="目前没有物流订单" mode="list"></u-empty>
|
||||
</view>
|
||||
</view>
|
||||
<view class="redirect-wrap">
|
||||
<u-loading mode="circle" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getExpress } from '@/api/trade.js'
|
||||
import { getPackage, getExpress } from '@/api/trade.js'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const logisticsInfo = ref<Record<string, any>>({})
|
||||
|
||||
onLoad((option) => {
|
||||
fetchLogistics(option.order_sn)
|
||||
})
|
||||
|
||||
function fetchLogistics(orderSn: string) {
|
||||
getExpress(orderSn).then((res) => {
|
||||
logisticsInfo.value = res.data.result || {}
|
||||
})
|
||||
onLoad(async (option) => {
|
||||
const orderSn = option?.order_sn
|
||||
if (!orderSn) {
|
||||
uni.showToast({ title: '订单号缺失', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const packageRes = await getPackage(orderSn)
|
||||
const packages = packageRes?.data?.result || []
|
||||
if (packages.length) {
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/deliverDetail?order_sn=${orderSn}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const traceRes = await getExpress(orderSn)
|
||||
if (traceRes?.data?.result) {
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/deliverDetail?order_sn=${orderSn}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 降级跳转,由 deliverDetail 统一处理展示
|
||||
}
|
||||
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/deliverDetail?order_sn=${orderSn}`,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.card-title {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
.logistics-detail {
|
||||
margin-top: 20rpx;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
width: 100%;
|
||||
> .card-title {
|
||||
font-size: 24rpx;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
> .time-line {
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
}
|
||||
.u-order-desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
.u-order-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.empty {
|
||||
padding: 40rpx 0;
|
||||
.redirect-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,238 +1,310 @@
|
||||
<template>
|
||||
<div>
|
||||
<view class="logistics-detail">
|
||||
<view class="card">
|
||||
<div v-if="logisticsList && logisticsList.length>0">
|
||||
<ul class="express-log" v-for="(packageItem, packageIndex) in logisticsList" :key="packageIndex">
|
||||
<div class="layui-layer-wrap">
|
||||
<dl>
|
||||
<dt>物流公司:</dt>
|
||||
<dd><div class="text-box">{{ packageItem.logisticsName }}</div></dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>快递单号:</dt>
|
||||
<dd>
|
||||
<div nctype="ordersSn" class="text-box">
|
||||
<a class="item" :href='"https://www.baidu.com/s?wd="+packageItem.logisticsNo' target="_blank">{{ packageItem.logisticsNo }}</a>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="div-express-log">
|
||||
<ul class="express-log express-log-name">
|
||||
<li v-for="(item, index) in packageItem.orderPackageItemList" :key="index">
|
||||
<p class="time" style="width: 50%;"><span>商品名称:</span><span>{{ item.goodsName }}</span></p>
|
||||
<p class="time" style="width: 30%;"><span>发货时间:</span><span>{{ item.logisticsTime }}</span></p>
|
||||
<p class="time" style="width: 20%;"><span>发货数量:</span><span>{{ item.deliverNumber }}</span></p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="div-express-log">
|
||||
<ul class="express-log" v-if="packageItem.traces && packageItem.traces.traces">
|
||||
<li v-for="(item, index) in packageItem.traces.traces" :key="index">
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<u-loading mode="circle" />
|
||||
</view>
|
||||
|
||||
<view v-else-if="loadError" class="error-wrap">
|
||||
<u-empty text="加载失败,请重试" mode="list" />
|
||||
<u-button size="medium" shape="circle" @click="retryLoad">重试</u-button>
|
||||
</view>
|
||||
|
||||
<view v-else class="card">
|
||||
<view v-if="orderStatus === 'PARTS_DELIVERED'" class="parts-tip">
|
||||
其余商品待商家继续发货
|
||||
</view>
|
||||
|
||||
<!-- 包裹列表 -->
|
||||
<view
|
||||
v-for="(packageItem, index) in packageList"
|
||||
:key="packageItem.packageNo || index"
|
||||
class="package-card"
|
||||
>
|
||||
<view class="package-title">{{ getPackageTitle(packageItem, index) }}</view>
|
||||
|
||||
<template v-if="isNoDelivery(packageItem)">
|
||||
<view v-if="packageItem.logisticsTime" class="info-row">
|
||||
<text class="label">完成时间:</text>
|
||||
<text class="value">{{ packageItem.logisticsTime }}</text>
|
||||
</view>
|
||||
<view v-if="packageItem.deliveryRemark" class="info-row">
|
||||
<text class="label">商家说明:</text>
|
||||
<text class="value">{{ packageItem.deliveryRemark }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="info-row">
|
||||
<text class="label">物流公司:</text>
|
||||
<text class="value">{{ packageItem.logisticsName || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">运单号:</text>
|
||||
<text class="value">{{ packageItem.logisticsNo || '-' }}</text>
|
||||
</view>
|
||||
<view v-if="packageItem.logisticsTime" class="info-row">
|
||||
<text class="label">发货时间:</text>
|
||||
<text class="value">{{ packageItem.logisticsTime }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view
|
||||
v-if="packageItem.orderPackageItemList && packageItem.orderPackageItemList.length"
|
||||
class="div-express-log goods-block"
|
||||
>
|
||||
<view
|
||||
class="goods-row"
|
||||
v-for="(item, gIndex) in packageItem.orderPackageItemList"
|
||||
:key="item.orderItemSn || gIndex"
|
||||
>
|
||||
<text class="goods-name">{{ item.goodsName }}</text>
|
||||
<text class="goods-meta">x{{ item.deliverNumber }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!isNoDelivery(packageItem)" class="div-express-log">
|
||||
<ul class="express-log" v-if="packageItem.traces && packageItem.traces.traces && packageItem.traces.traces.length">
|
||||
<li v-for="(item, tIndex) in packageItem.traces.traces" :key="tIndex">
|
||||
<span class="time">{{ item.AcceptTime || item.acceptTime }}</span>
|
||||
<span class="detail">{{ item.AcceptStation || item.remark }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="express-log" v-else>
|
||||
<li>暂无物流信息</li>
|
||||
<li>物流信息同步中</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</div>
|
||||
|
||||
<!-- 历史订单降级:主表物流 -->
|
||||
<view v-if="!packageList.length && legacyTraces" class="package-card">
|
||||
<view class="package-title">整单发货</view>
|
||||
<view class="info-row">
|
||||
<text class="label">物流公司:</text>
|
||||
<text class="value">{{ legacyTraces.shipper || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">运单号:</text>
|
||||
<text class="value">{{ legacyTraces.logisticCode || '-' }}</text>
|
||||
</view>
|
||||
<view class="div-express-log">
|
||||
<ul class="express-log" v-if="legacyTraces.traces && legacyTraces.traces.length">
|
||||
<li v-for="(item, index) in legacyTraces.traces" :key="index">
|
||||
<span class="time">{{ item.AcceptTime || item.acceptTime }}</span>
|
||||
<span class="detail">{{ item.AcceptStation || item.remark }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="express-log" v-else>
|
||||
<li>物流信息同步中</li>
|
||||
</ul>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<u-empty
|
||||
v-if="!packageList.length && !legacyTraces"
|
||||
class="empty"
|
||||
text="物流信息同步中"
|
||||
mode="list"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getPackage } from '@/api/trade.js'
|
||||
import { getPackage, getExpress } from '@/api/trade.js'
|
||||
import { getOrderDetail } from '@/api/order.js'
|
||||
import { isNoDelivery, getPackageTitle } from '@/utils/orderDelivery.js'
|
||||
|
||||
const logisticsList = ref<any[]>([])
|
||||
const packageList = ref<any[]>([])
|
||||
const legacyTraces = ref<any>(null)
|
||||
const orderSn = ref('')
|
||||
const orderStatus = ref('')
|
||||
const loading = ref(true)
|
||||
const loadError = ref(false)
|
||||
|
||||
onLoad((option) => {
|
||||
const sn = option.order_sn
|
||||
if (sn) fetchLogistics(sn)
|
||||
})
|
||||
|
||||
function fetchLogistics(sn: string) {
|
||||
getPackage(sn).then((res) => {
|
||||
if (res.data.success) {
|
||||
logisticsList.value = res.data.result
|
||||
orderSn.value = option?.order_sn || ''
|
||||
if (orderSn.value) {
|
||||
loadData(orderSn.value)
|
||||
} else {
|
||||
loading.value = false
|
||||
loadError.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function retryLoad() {
|
||||
if (orderSn.value) loadData(orderSn.value)
|
||||
}
|
||||
|
||||
async function loadData(sn: string) {
|
||||
loading.value = true
|
||||
loadError.value = false
|
||||
packageList.value = []
|
||||
legacyTraces.value = null
|
||||
|
||||
try {
|
||||
const orderRes = await getOrderDetail(sn)
|
||||
if (orderRes.data?.success) {
|
||||
orderStatus.value = orderRes.data.result?.order?.orderStatus || ''
|
||||
}
|
||||
|
||||
const packageRes = await getPackage(sn)
|
||||
if (!packageRes.data?.success) {
|
||||
loadError.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const packages = packageRes.data.result || []
|
||||
if (packages.length) {
|
||||
packageList.value = packages
|
||||
return
|
||||
}
|
||||
|
||||
const traceRes = await getExpress(sn)
|
||||
if (traceRes.data?.success && traceRes.data.result) {
|
||||
legacyTraces.value = traceRes.data.result
|
||||
}
|
||||
} catch {
|
||||
loadError.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background: #fff;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @import url('./goods.scss');
|
||||
.goods-item-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10rpx 30rpx;
|
||||
|
||||
.goods-img {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
padding-left: 30rpx;
|
||||
flex: 3;
|
||||
|
||||
.goods-title {
|
||||
margin-bottom: 10rpx;
|
||||
color: $font-color-dark;
|
||||
}
|
||||
|
||||
.goods-specs {
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 10rpx;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 10rpx;
|
||||
color: #ff5a10;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-num {
|
||||
>.good-complaint {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
width: 60rpx;
|
||||
color: $main-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.goods-info {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
.logistics-detail {
|
||||
margin-top: 20rpx;
|
||||
padding: 0 16rpx;
|
||||
padding: 0 16rpx 40rpx;
|
||||
}
|
||||
|
||||
.loading-wrap,
|
||||
.error-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
width: 100%;
|
||||
> .card-title {
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.parts-tip {
|
||||
margin: 0 20rpx 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
font-size: 24rpx;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
> .time-line {
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
}
|
||||
.u-order-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.u-order-desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
.u-order-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.empty {
|
||||
padding: 40rpx 0;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.express-log {
|
||||
/*margin: 5px -10px 5px 5px;*/
|
||||
.package-card {
|
||||
padding: 20rpx;
|
||||
list-style-type: none;
|
||||
li {
|
||||
display: flex;
|
||||
margin-top: 18rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
li:nth-of-type(1) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.time {
|
||||
width: 140rpx;
|
||||
display: flex;
|
||||
// float: left;
|
||||
flex-direction: column;
|
||||
font-size: 24rpx;
|
||||
line-height: 40rpx;
|
||||
span:nth-of-type(1) {
|
||||
|
||||
.package-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
margin-left: 30rpx;
|
||||
display: inline-block;
|
||||
font-size: 24rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 60rpx;
|
||||
}
|
||||
}
|
||||
.layui-layer-wrap {
|
||||
dl {
|
||||
border-top: solid 1px #f5f5f5;
|
||||
margin-top: -2rpx;
|
||||
overflow: hidden;
|
||||
|
||||
dt {
|
||||
.info-row {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
display: inline-block;
|
||||
padding: 16rpx 1% 16rpx 0;
|
||||
line-height: 44rpx;
|
||||
padding: 4rpx 0;
|
||||
|
||||
.label {
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
display: inline-block;
|
||||
padding: 16rpx 0 16rpx 16rpx;
|
||||
border-left: solid 2rpx #f5f5f5;
|
||||
|
||||
.text-box {
|
||||
line-height: 40rpx;
|
||||
.value {
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-block {
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.goods-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
line-height: 44rpx;
|
||||
color: #666;
|
||||
|
||||
.goods-name {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.goods-meta {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.express-log {
|
||||
padding: 20rpx;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
margin-top: 18rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 180rpx;
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.detail {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 40rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.div-express-log {
|
||||
// max-height: 600rpx;
|
||||
border: solid 2rpx #e7e7e7;
|
||||
background: #fafafa;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 6rpx;
|
||||
// overflow-y: auto;
|
||||
// overflow-x: auto;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
</style>
|
||||
@@ -294,18 +294,9 @@ function waitPay(val: any) {
|
||||
|
||||
function pay(val: any) {
|
||||
if (val.sn) {
|
||||
// #ifdef MP-WEIXIN
|
||||
new LiLiWXPay({
|
||||
sn: val.sn,
|
||||
price: val.flowPrice,
|
||||
orderType: 'ORDER',
|
||||
}).pay()
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: '/pages/cart/payment/payOrder?order_sn=' + val.sn,
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 物流信息 / 卡密入口 -->
|
||||
<view class="info-view logistics-view">
|
||||
<!-- 物流/配送信息 / 卡密入口 -->
|
||||
<view class="info-view logistics-view" v-if="showDeliveryInfoBlock">
|
||||
<view class="logistics-List">
|
||||
<view v-if="isECouponOrder" class="card-key-entry">
|
||||
<view v-if="ecouponCardKeyDelivered" class="card-key-entry__btn" @click="goCardKeyDetail">
|
||||
@@ -25,16 +25,16 @@
|
||||
<view class="verificationCode" v-else-if="order.verificationCode">
|
||||
券码: {{ order.orderStatus == 'CANCELLED' ? '已失效' : order.verificationCode }}
|
||||
</view>
|
||||
<view @click="handleClickDeliver()" class="info-view logi-view" v-else-if="orderPackage && orderPackage.length">
|
||||
<view class="verificationCode">
|
||||
当前订单有 {{ orderPackage.length }} 个包裹快递
|
||||
<view
|
||||
v-else-if="allowOperation.showLogistics"
|
||||
@click="handleClickDeliver()"
|
||||
class="info-view logi-view"
|
||||
>
|
||||
<view class="verificationCode">{{ deliveryEntryText }}</view>
|
||||
<div>点击此处查看</div>
|
||||
</view>
|
||||
<div>
|
||||
点击此处查看
|
||||
</div>
|
||||
</view>
|
||||
<view v-else-if="!isECouponOrder" class="logistics-List-title">
|
||||
{{ '暂无物流信息' }}
|
||||
<view v-else-if="order.orderStatus === 'UNDELIVERED'" class="logistics-List-title">
|
||||
等待商家发货
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -144,7 +144,7 @@
|
||||
<view class="customer-service"
|
||||
v-if="orderDetail.allowOperationVO && orderDetail.allowOperationVO.cancel == true"
|
||||
@click="onCancel(order.sn)">取消订单</view>
|
||||
<view class="customer-service" v-if="order.orderStatus == 'DELIVERED' && !isECouponOrder" @click="onLogistics(order)">查看物流</view>
|
||||
<view class="customer-service" v-if="allowOperation.showLogistics" @click="handleClickDeliver()">查看物流</view>
|
||||
<view class="customer-service" v-if="order.orderStatus != 'UNPAID' && order.orderPromotionType == 'PINTUAN'"
|
||||
@click="ByUserMessage(order)">查看拼团信息</view>
|
||||
<view class="customer-service"
|
||||
@@ -219,7 +219,7 @@
|
||||
>立即付款</view>
|
||||
<view
|
||||
class="pay-btn"
|
||||
v-if="order.orderStatus == 'DELIVERED' && !isECouponOrder"
|
||||
v-if="allowOperation.rog"
|
||||
@click="onRog(order.sn)"
|
||||
>确认收货</view>
|
||||
<view
|
||||
@@ -275,6 +275,11 @@ import {
|
||||
talkIm,
|
||||
callPhone,
|
||||
} from '@/utils/filters.js'
|
||||
import {
|
||||
shouldLoadDelivery,
|
||||
getDeliveryEntryText,
|
||||
getOrderAllowOperation,
|
||||
} from '@/utils/orderDelivery.js'
|
||||
import {
|
||||
isECouponOrder as checkECouponOrder,
|
||||
isNonPhysicalOrder as checkNonPhysicalOrder,
|
||||
@@ -300,7 +305,6 @@ const orderStatusMap: Record<string, { title: string; value?: string }> = {
|
||||
TAKE: { title: '待核验' },
|
||||
}
|
||||
|
||||
const logisticsList = ref<any>('')
|
||||
const shareFlag = ref(false)
|
||||
const order = ref<Record<string, any>>({})
|
||||
const cancelShow = ref(false)
|
||||
@@ -311,8 +315,9 @@ const sn = ref('')
|
||||
const cancelList = ref<any[]>([])
|
||||
const rogShow = ref(false)
|
||||
const reason = ref('')
|
||||
const orderPackage = ref<any>('')
|
||||
// orderType=E_COUPON:无物流/核销码;卡密见 cardKeyDetail(仅本单 orderItems,不含满赠子单)
|
||||
const orderPackage = ref<any[]>([])
|
||||
const legacyTraces = ref<any>(null)
|
||||
|
||||
const isECouponOrder = computed(() => checkECouponOrder(order.value?.orderType))
|
||||
const isNonPhysicalOrder = computed(() => checkNonPhysicalOrder(order.value?.orderType))
|
||||
const ecouponCardKeyDelivered = computed(() =>
|
||||
@@ -325,6 +330,27 @@ const ecouponPendingMessage = computed(() => {
|
||||
return pending ? resolveCardKeyFulfillMessage(pending) : ''
|
||||
})
|
||||
|
||||
const allowOperation = computed(() =>
|
||||
getOrderAllowOperation(order.value, orderDetail.value)
|
||||
)
|
||||
|
||||
const showDeliveryInfoBlock = computed(() => {
|
||||
if (isECouponOrder.value) return true
|
||||
if (order.value.orderType === 'VIRTUAL') return false
|
||||
if (order.value.deliveryMethod === 'SELF_PICK_UP') return false
|
||||
if (order.value.verificationCode) return true
|
||||
if (order.value.deliveryMethod === 'LOGISTICS') return true
|
||||
return false
|
||||
})
|
||||
|
||||
const deliveryEntryText = computed(() =>
|
||||
getDeliveryEntryText(
|
||||
orderPackage.value,
|
||||
order.value.orderStatus,
|
||||
legacyTraces.value
|
||||
)
|
||||
)
|
||||
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
@@ -335,12 +361,27 @@ onLoad((options) => {
|
||||
loadData(orderSnParam)
|
||||
})
|
||||
|
||||
function getOrderPackage() {
|
||||
getPackage(order.value.sn).then((res) => {
|
||||
if (res.data.success) {
|
||||
orderPackage.value = res.data.result
|
||||
async function loadDelivery(orderData: Record<string, any>) {
|
||||
orderPackage.value = []
|
||||
legacyTraces.value = null
|
||||
|
||||
if (!shouldLoadDelivery(orderData)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const packageResponse = await getPackage(orderData.sn)
|
||||
const packages = packageResponse?.data?.result || []
|
||||
if (packages.length) {
|
||||
orderPackage.value = packages
|
||||
return
|
||||
}
|
||||
|
||||
const traceResponse = await getExpress(orderData.sn)
|
||||
legacyTraces.value = traceResponse?.data?.result || null
|
||||
} catch {
|
||||
// 配送信息加载失败时不阻断订单详情展示
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleClickDeliver() {
|
||||
@@ -349,6 +390,13 @@ function handleClickDeliver() {
|
||||
})
|
||||
}
|
||||
|
||||
function goCardKeyDetail() {
|
||||
if (!sn.value) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/cardKey/cardKeyDetail?sn=${sn.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
function refundPriceList(status: string) {
|
||||
switch (status) {
|
||||
case 'ALL_REFUND':
|
||||
@@ -380,12 +428,6 @@ function goToShopPage(val: any) {
|
||||
})
|
||||
}
|
||||
|
||||
function loadLogistics(orderSnParam: string) {
|
||||
getExpress(orderSnParam).then((res) => {
|
||||
logisticsList.value = res.data.result
|
||||
})
|
||||
}
|
||||
|
||||
function inviteGroup() {
|
||||
shareFlag.value = true
|
||||
}
|
||||
@@ -404,32 +446,19 @@ function ByUserMessage(orderItem: any) {
|
||||
|
||||
function loadData(orderSnParam: string) {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
getOrderDetail(orderSnParam).then((res) => {
|
||||
getOrderDetail(orderSnParam).then(async (res) => {
|
||||
const result = res.data.result
|
||||
order.value = result.order
|
||||
orderGoodsList.value = result.orderItems
|
||||
orderDetail.value = result
|
||||
if (
|
||||
result.order.deliveryMethod === 'LOGISTICS' &&
|
||||
!checkECouponOrder(result.order.orderType) &&
|
||||
result.order.orderType !== 'VIRTUAL'
|
||||
) {
|
||||
// E_COUPON 无物流信息
|
||||
loadLogistics(orderSnParam)
|
||||
getOrderPackage()
|
||||
if (!order.value.allowOperationVO && result.allowOperationVO) {
|
||||
order.value.allowOperationVO = result.allowOperationVO
|
||||
}
|
||||
await loadDelivery(order.value)
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
function goCardKeyDetail() {
|
||||
// 仅传 orderSn,卡密在二级页重新请求订单详情,不经路由/Vuex 传递明文
|
||||
if (!sn.value) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/cardKey/cardKeyDetail?sn=${sn.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
function onReceipt(val: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/order/invoice/invoiceDetail?id=' + val.id,
|
||||
@@ -528,18 +557,6 @@ function onComment(_orderSnText: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function onLogistics(orderItem: any) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
'/pages/mine/msgTips/packageMsg/logisticsDetail?logi_id=' +
|
||||
orderItem.logi_id +
|
||||
'&ship_no=' +
|
||||
orderItem.ship_no +
|
||||
'&order_sn=' +
|
||||
orderItem.sn,
|
||||
})
|
||||
}
|
||||
|
||||
function reasonChange(val: string) {
|
||||
reason.value = val
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -318,7 +318,9 @@
|
||||
/************接口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 { tryBindDistribution } from '@/utils/distributionBind.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'
|
||||
@@ -469,10 +471,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 || ''
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -502,7 +509,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
|
||||
|
||||
@@ -519,11 +526,21 @@ 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) {
|
||||
await tryBindDistribution(distId)
|
||||
}
|
||||
if (distId && goodsId && id && storage.getAccessToken()) {
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
190
utils/distributionBind.js
Normal file
190
utils/distributionBind.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 分销海报 / 商品分享扫码绑客(统一入口)
|
||||
*/
|
||||
import storage from '@/utils/storage.js'
|
||||
import store from '@/store'
|
||||
import { getGoodsDistribution } from '@/api/distribution.js'
|
||||
import { getMpScene } from '@/api/goods.js'
|
||||
|
||||
const PENDING_KEY = 'pending_distribution_id'
|
||||
|
||||
/** 本次冷启动内已尝试绑定的分销员 ID,避免重复请求 */
|
||||
const launchBoundIds = new Set()
|
||||
|
||||
let flushing = false
|
||||
|
||||
export function clearPendingDistributionId() {
|
||||
try {
|
||||
uni.removeStorageSync(PENDING_KEY)
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function setPendingDistributionId(distributionId) {
|
||||
if (!distributionId) return
|
||||
try {
|
||||
uni.setStorageSync(PENDING_KEY, String(distributionId))
|
||||
} catch (e) {
|
||||
console.warn('set pending distributionId failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPendingDistributionId() {
|
||||
try {
|
||||
return uni.getStorageSync(PENDING_KEY) || ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function resetLaunchBindCache() {
|
||||
launchBoundIds.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析葵花码 scene:支持 shortLinkId 或明文 bind,{distributionId}
|
||||
*/
|
||||
export async function resolveDistributionIdFromScene(scene) {
|
||||
if (!scene) return ''
|
||||
const raw = decodeURIComponent(String(scene)).trim()
|
||||
if (!raw) return ''
|
||||
|
||||
if (raw.startsWith('bind,')) {
|
||||
const id = raw.split(',')[1]
|
||||
return id ? String(id).trim() : ''
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMpScene(raw)
|
||||
const params = res?.data?.success ? String(res.data.result || '').trim() : ''
|
||||
if (params.startsWith('bind,')) {
|
||||
const id = params.split(',')[1]
|
||||
return id ? String(id).trim() : ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('resolve distribution scene failed', error)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function parseRecruitIdFromParams(params) {
|
||||
if (!params) return ''
|
||||
const text = String(params).trim()
|
||||
if (text.startsWith('recruit_')) {
|
||||
const id = text.substring('recruit_'.length)
|
||||
return id ? String(id).trim() : ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析图文邀请卡 scene:支持 shortLinkId 或明文 recruit_{distributionId}
|
||||
*/
|
||||
export async function resolveRecruitDistributionIdFromScene(scene) {
|
||||
if (!scene) return ''
|
||||
const raw = decodeURIComponent(String(scene)).trim()
|
||||
if (!raw) return ''
|
||||
|
||||
const direct = parseRecruitIdFromParams(raw)
|
||||
if (direct) return direct
|
||||
|
||||
try {
|
||||
const res = await getMpScene(raw)
|
||||
const params = res?.data?.success ? String(res.data.result || '').trim() : ''
|
||||
return parseRecruitIdFromParams(params)
|
||||
} catch (error) {
|
||||
console.warn('resolve recruit scene failed', error)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const CLEAR_PENDING_CODES = new Set([22000, 22001])
|
||||
|
||||
function shouldClearPendingOnFailure(res) {
|
||||
const code = res?.data?.code
|
||||
if (code != null && CLEAR_PENDING_CODES.has(Number(code))) {
|
||||
return true
|
||||
}
|
||||
const message = String(res?.data?.message || '')
|
||||
if (message.includes('分销员不存在') || message.includes('分销功能关闭')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试绑定分销员:已登录直接请求;未登录写入 pending
|
||||
*/
|
||||
export async function tryBindDistribution(distributionId, options = {}) {
|
||||
const id = String(distributionId || '').trim()
|
||||
if (!id) return
|
||||
|
||||
const force = options.force === true
|
||||
if (!force && launchBoundIds.has(id)) {
|
||||
return
|
||||
}
|
||||
launchBoundIds.add(id)
|
||||
|
||||
if (!storage.getAccessToken()) {
|
||||
setPendingDistributionId(id)
|
||||
store.state.distributionId = id
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getGoodsDistribution(id)
|
||||
if (res?.data?.success) {
|
||||
clearPendingDistributionId()
|
||||
store.state.distributionId = id
|
||||
return
|
||||
}
|
||||
if (shouldClearPendingOnFailure(res)) {
|
||||
clearPendingDistributionId()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('bind distribution failed', error)
|
||||
const res = error?.data ? error : error?.response?.data ? { data: error.response.data } : null
|
||||
if (res && shouldClearPendingOnFailure(res)) {
|
||||
clearPendingDistributionId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后补绑 pending
|
||||
*/
|
||||
export async function flushPendingBind() {
|
||||
if (flushing) return
|
||||
const pendingId = getPendingDistributionId()
|
||||
if (!pendingId || !storage.getAccessToken()) {
|
||||
return
|
||||
}
|
||||
flushing = true
|
||||
try {
|
||||
launchBoundIds.delete(pendingId)
|
||||
await tryBindDistribution(pendingId, { force: true })
|
||||
} finally {
|
||||
flushing = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* App onLaunch:有 scene 则解析绑客;无 scene 清除 pending
|
||||
*/
|
||||
export async function handleAppLaunchScene(options) {
|
||||
resetLaunchBindCache()
|
||||
|
||||
const scene = options?.query?.scene
|
||||
if (!scene) {
|
||||
clearPendingDistributionId()
|
||||
return
|
||||
}
|
||||
|
||||
const distributionId = await resolveDistributionIdFromScene(scene)
|
||||
if (!distributionId) {
|
||||
return
|
||||
}
|
||||
|
||||
await tryBindDistribution(distributionId)
|
||||
}
|
||||
289
utils/distributionPosterCanvas.ts
Normal file
289
utils/distributionPosterCanvas.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { nextTick } from 'vue'
|
||||
import DrawPoster from '@/js_sdk/u-draw-poster'
|
||||
|
||||
export const POSTER_WIDTH = 630
|
||||
export const POSTER_HEIGHT = 1000
|
||||
|
||||
const QR_SIZE = 180
|
||||
const AVATAR_SIZE = 86
|
||||
const PADDING_H = 24
|
||||
const MEMBER_LEFT = PADDING_H + 16
|
||||
const BOTTOM_RATIO = 0.06
|
||||
const MEMBER_ROW_GAP = 12
|
||||
const NAME_FONT_SIZE = 30
|
||||
const SLOGAN_FONT_SIZE = 24
|
||||
const NAME_LINE_HEIGHT = 38
|
||||
const SLOGAN_LINE_HEIGHT = 32
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const st2 = (size: number) => size * 2
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
const st2 = (size: number) => size
|
||||
// #endif
|
||||
|
||||
export interface DistributionPosterOptions {
|
||||
backgroundImage?: string
|
||||
memberAvatar?: string
|
||||
memberName?: string
|
||||
slogan?: string
|
||||
qrImage: string
|
||||
textColor?: string
|
||||
showMemberInfo?: boolean
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export 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/') ||
|
||||
url.startsWith('file://')
|
||||
) {
|
||||
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 calcCoverBox(imgW: number, imgH: number, boxW: number, boxH: number) {
|
||||
const scale = Math.max(boxW / imgW, boxH / imgH)
|
||||
const width = imgW * scale
|
||||
const height = imgH * scale
|
||||
return {
|
||||
x: (boxW - width) / 2,
|
||||
y: (boxH - height) / 2,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDrawPosterCache(selector: string) {
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1] as any
|
||||
if (!page) return
|
||||
delete page[`#${selector}__dp`]
|
||||
delete page[`${selector}__dp`]
|
||||
}
|
||||
|
||||
async function waitCanvasReady() {
|
||||
await nextTick()
|
||||
await sleep(150)
|
||||
}
|
||||
|
||||
async function drawCircleImage(ctx: any, src: string, x: number, y: number, size: number) {
|
||||
const radius = size / 2
|
||||
const cx = x + radius
|
||||
const cy = y + radius
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.arc(st2(cx), st2(cy), st2(radius), 0, Math.PI * 2)
|
||||
ctx.clip()
|
||||
await ctx.drawImage(src, st2(x), st2(y), st2(size), st2(size))
|
||||
ctx.restore()
|
||||
ctx.save()
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'
|
||||
ctx.lineWidth = st2(2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(st2(cx), st2(cy), st2(radius - 1), 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
export async function composeDistributionPoster(
|
||||
selector: string,
|
||||
componentThis: any,
|
||||
options: DistributionPosterOptions
|
||||
): Promise<string> {
|
||||
const {
|
||||
backgroundImage = '',
|
||||
memberAvatar = '',
|
||||
memberName = '分销员',
|
||||
slogan = '',
|
||||
qrImage,
|
||||
textColor = '#1f2a44',
|
||||
showMemberInfo = true,
|
||||
} = options
|
||||
|
||||
if (!qrImage) {
|
||||
throw new Error('葵花码未生成')
|
||||
}
|
||||
|
||||
clearDrawPosterCache(selector)
|
||||
await waitCanvasReady()
|
||||
|
||||
const dp = await DrawPoster.build({
|
||||
selector,
|
||||
componentThis,
|
||||
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 localQr = await resolveImagePath(qrImage)
|
||||
let localBg = ''
|
||||
if (backgroundImage) {
|
||||
try {
|
||||
localBg = await resolveImagePath(backgroundImage)
|
||||
} catch (error) {
|
||||
console.warn('poster background download failed', error)
|
||||
}
|
||||
}
|
||||
let localAvatar = ''
|
||||
if (showMemberInfo && memberAvatar) {
|
||||
try {
|
||||
localAvatar = await resolveImagePath(memberAvatar)
|
||||
} catch (error) {
|
||||
console.warn('poster avatar download failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
const bottomOffset = POSTER_HEIGHT * BOTTOM_RATIO
|
||||
const rowBottom = POSTER_HEIGHT - bottomOffset
|
||||
const qrX = POSTER_WIDTH - PADDING_H - QR_SIZE
|
||||
const qrY = rowBottom - QR_SIZE
|
||||
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT))
|
||||
})
|
||||
|
||||
if (localBg) {
|
||||
await dp.draw(async (ctx: any) => {
|
||||
const { width: imgW, height: imgH } = await getImageInfo(localBg)
|
||||
const fit = calcCoverBox(imgW, imgH, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
await ctx.drawImage(
|
||||
localBg,
|
||||
st2(fit.x),
|
||||
st2(fit.y),
|
||||
st2(fit.width),
|
||||
st2(fit.height)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
await dp.draw(async (ctx: any) => {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRoundRect(st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE), st2(12))
|
||||
await ctx.drawImage(localQr, st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE))
|
||||
})
|
||||
|
||||
if (showMemberInfo) {
|
||||
await dp.draw(async (ctx: any) => {
|
||||
const textMaxWidth = qrX - MEMBER_LEFT - 16
|
||||
const memberBlockHeight =
|
||||
AVATAR_SIZE +
|
||||
MEMBER_ROW_GAP +
|
||||
NAME_LINE_HEIGHT +
|
||||
(slogan ? MEMBER_ROW_GAP + SLOGAN_LINE_HEIGHT : 0)
|
||||
const blockHeight = Math.max(memberBlockHeight, QR_SIZE)
|
||||
const blockTop = rowBottom - blockHeight
|
||||
|
||||
const avatarX = MEMBER_LEFT
|
||||
const avatarY = blockTop
|
||||
const nameY = avatarY + AVATAR_SIZE + MEMBER_ROW_GAP
|
||||
const sloganY = nameY + NAME_LINE_HEIGHT + MEMBER_ROW_GAP
|
||||
|
||||
if (localAvatar) {
|
||||
await drawCircleImage(ctx, localAvatar, avatarX, avatarY, AVATAR_SIZE)
|
||||
} else {
|
||||
const radius = AVATAR_SIZE / 2
|
||||
ctx.save()
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
|
||||
ctx.beginPath()
|
||||
ctx.arc(st2(avatarX + radius), st2(avatarY + radius), st2(radius), 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'
|
||||
ctx.lineWidth = st2(2)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillStyle = textColor
|
||||
ctx.font = `bold ${st2(NAME_FONT_SIZE)}px PingFang SC`
|
||||
ctx.fillWarpText({
|
||||
text: memberName,
|
||||
maxWidth: st2(textMaxWidth),
|
||||
x: st2(avatarX),
|
||||
y: st2(nameY),
|
||||
layer: 1,
|
||||
lineHeight: st2(NAME_LINE_HEIGHT),
|
||||
})
|
||||
|
||||
if (slogan) {
|
||||
ctx.font = `${st2(SLOGAN_FONT_SIZE)}px PingFang SC`
|
||||
ctx.fillWarpText({
|
||||
text: slogan,
|
||||
maxWidth: st2(textMaxWidth),
|
||||
x: st2(avatarX),
|
||||
y: st2(sloganY),
|
||||
layer: 1,
|
||||
lineHeight: st2(SLOGAN_LINE_HEIGHT),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const path = await dp.createImagePath()
|
||||
if (!path || path === '---stop createImagePath---') {
|
||||
throw new Error('海报图片导出失败')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function savePosterToAlbum(filePath: string) {
|
||||
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' })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 '默认'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态列表
|
||||
*/
|
||||
|
||||
47
utils/orderDelivery.js
Normal file
47
utils/orderDelivery.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 订单配送展示工具(整单 / 拆单 / 无需配送)
|
||||
*/
|
||||
|
||||
export function shouldLoadDelivery(order) {
|
||||
return (
|
||||
order?.deliveryMethod === 'LOGISTICS' &&
|
||||
order?.allowOperationVO?.showLogistics === true
|
||||
)
|
||||
}
|
||||
|
||||
export function isNoDelivery(packageItem) {
|
||||
return (
|
||||
packageItem?.fulfillmentMode === 'NO_DELIVERY' ||
|
||||
packageItem?.logisticsRequired === false
|
||||
)
|
||||
}
|
||||
|
||||
export function getPackageTitle(packageItem, index) {
|
||||
if (isNoDelivery(packageItem)) return '无需配送'
|
||||
if (packageItem?.fulfillmentMode === 'WHOLE') return '整单发货'
|
||||
if (packageItem?.fulfillmentMode === 'SPLIT') return `包裹 ${index + 1}`
|
||||
return `包裹 ${index + 1}`
|
||||
}
|
||||
|
||||
export function getDeliveryEntryText(packages, orderStatus, legacyTraces) {
|
||||
if (packages?.length) {
|
||||
const allNoDelivery = packages.every(isNoDelivery)
|
||||
if (allNoDelivery) return '查看配送说明'
|
||||
if (orderStatus === 'PARTS_DELIVERED') {
|
||||
return `已发出 ${packages.length} 个包裹,剩余商品待发货`
|
||||
}
|
||||
return `共 ${packages.length} 个配送包裹`
|
||||
}
|
||||
|
||||
const traces = legacyTraces?.traces
|
||||
if (Array.isArray(traces) && traces.length) {
|
||||
const latest = traces[0]
|
||||
return latest?.AcceptStation || '商家已发货,物流信息同步中'
|
||||
}
|
||||
|
||||
return '商家已发货,物流信息同步中'
|
||||
}
|
||||
|
||||
export function getOrderAllowOperation(order, orderDetail) {
|
||||
return order?.allowOperationVO || orderDetail?.allowOperationVO || {}
|
||||
}
|
||||
@@ -84,6 +84,14 @@ export default {
|
||||
// 写入登录
|
||||
setHasLogin(val) {
|
||||
uni.setStorageSync(HAS_LOGIN, val);
|
||||
if (val) {
|
||||
try {
|
||||
const { flushPendingBind } = require("@/utils/distributionBind.js");
|
||||
flushPendingBind();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
},
|
||||
// 获取是否登录
|
||||
getHasLogin() {
|
||||
|
||||
@@ -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