Compare commits
20 Commits
ea4b76fefe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fc778e9de6 | |||
| 0ae33997ee | |||
| 1c7f35b40f | |||
| a2009c8cea | |||
| 2ff740fb07 | |||
| b5f66017cf | |||
| 099855e121 | |||
| 8128e2ffb2 | |||
| 815678a1de | |||
| 238a652afc | |||
| d2cdd42846 | |||
| 0a72306d6a | |||
| 128563bfeb | |||
| 1cd535d517 | |||
| 5f4ea47300 | |||
| f14e190a79 | |||
| 61025522aa | |||
| 25abd81d9f | |||
| 0be67bdb91 | |||
| 969bc00b53 |
39
.workbuddy/expert-history.json
Normal file
39
.workbuddy/expert-history.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"sessions": {
|
||||||
|
"8469bdd5894b41e1ba171a40f33d6a83": [
|
||||||
|
{
|
||||||
|
"expertId": "UiDesigner",
|
||||||
|
"name": "Sam",
|
||||||
|
"profession": "UI设计师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/01-Design/UiDesigner/UiDesigner.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/01-Design/UiDesigner/UiDesigner_zh.md",
|
||||||
|
"usedAt": 1775709039214,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2d8018ea3c7f4b92a608c23c2ee6211a": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1776102350082,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"06a306d869f24d2eb36c381b2a67c63e": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "吴八哥",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1776328275045,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lastUpdated": 1776332994640
|
||||||
|
}
|
||||||
33
.workbuddy/memory/2026-04-08.md
Normal file
33
.workbuddy/memory/2026-04-08.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 2026-04-08 工作日志
|
||||||
|
|
||||||
|
## dealer/customer/add 新增接待人员选择功能
|
||||||
|
|
||||||
|
- 在 `src/api/shop/shopDealerApply/model/index.ts` 中为 `ShopDealerApply` 接口新增了 `receptionistId`(接待人员用户ID)和 `receptionistName`(接待人员姓名)字段
|
||||||
|
- 在 `src/dealer/customer/add.tsx` 中实现了接待人员选择功能:
|
||||||
|
- 引入 `Popup`、`SearchBar`、`ArrowRight`、`Del` 组件
|
||||||
|
- 引入 `pageShopDealerUser` API
|
||||||
|
- 添加接待人员相关状态(`showReceptionistPicker`、`receptionistList`、`selectedReceptionist` 等)
|
||||||
|
- 在表单手机号字段后新增 `Cell` 显示已选接待人员,支持清除
|
||||||
|
- 底部 `Popup` 弹出层,含搜索框 + 分销商用户列表选择
|
||||||
|
- 编辑模式下自动回填已保存的接待人员信息
|
||||||
|
- 提交时携带 `receptionistId` 和 `receptionistName` 字段
|
||||||
|
|
||||||
|
## 首页新增品牌画册区域
|
||||||
|
|
||||||
|
- 创建 `src/pages/index/CatalogShowcase.tsx` 组件:
|
||||||
|
- 展示品牌画册封面预览
|
||||||
|
- "点击查看"按钮,点击后复制链接并提示用户到浏览器打开
|
||||||
|
- 链接地址:https://book.yunzhan365.com/mdfy/tjcs/mobile/index.html
|
||||||
|
- 创建 `src/pages/index/CatalogShowcase.scss` 样式文件
|
||||||
|
- 在 `src/pages/index/index.tsx` 中引入并添加 `CatalogShowcase` 组件,位于 `CaseShowcase` 和 `ContactSection` 之间
|
||||||
|
|
||||||
|
## pages/webview/index.tsx 修复
|
||||||
|
|
||||||
|
- 原代码 `getUrl()` 在渲染阶段直接调用 `Taro.getCurrentPages()`,数据可能未就绪导致 URL 取不到
|
||||||
|
- 改为 `useRouter()` Hook + `useState` 获取 `params.url`,确保参数可靠后再渲染 web-view
|
||||||
|
- URL 无效时直接 return null 并 toast 提示后返回
|
||||||
|
|
||||||
|
## pages/index/CatalogShowcase.tsx 图标名修复
|
||||||
|
|
||||||
|
- `@nutui/icons-react-taro` 中不存在 `FileText` 图标,正确名称为 `File`
|
||||||
|
- `React error #130`(Element type is undefined)即因此导致,已修复
|
||||||
34
.workbuddy/memory/2026-04-09.md
Normal file
34
.workbuddy/memory/2026-04-09.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 2026-04-09 工作日志
|
||||||
|
|
||||||
|
## 首页UI重构 — 去除"豆腐块"布局
|
||||||
|
用户反馈首页过于规整的"豆腐块"布局,要求重新设计。完成以下改动:
|
||||||
|
|
||||||
|
### 设计思路
|
||||||
|
- 打破均匀间距,各模块使用差异化 padding/margin
|
||||||
|
- 去掉公式化的"模块标题+白色卡片"结构
|
||||||
|
- 引入沉浸式视觉区域和横向交互
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
1. **index.tsx** — 重构布局结构,引入 Hero 区域概念,Banner+Grid 融为一体
|
||||||
|
2. **index.scss** — 全局样式重写,浅灰背景(#f5f5f7),自定义公告条样式
|
||||||
|
3. **TrustSection** — 从均匀三列改为横向滑动渐变卡片,突出数据亮点(10年/15年/98%)
|
||||||
|
4. **ContactSection** — 从2x2网格改为全宽深色底部(#1e293b→#0f172a),沉浸式设计
|
||||||
|
5. **BestSellers** — 从纵向列表改为横向图文卡片(左图右信息),更紧凑
|
||||||
|
6. **CatalogShowcase** — 从小卡片改为大面积渐变视觉卡片,带装饰性书本图形
|
||||||
|
7. **NoticeBar** — 替换NutUI NoticeBar为自定义轻量公告条
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- `Fire` 图标在 @nutui/icons-react-taro 中不存在,改用 `StarFill`
|
||||||
|
- Banner 组件保留动态数据加载能力,圆角样式通过外层 CSS 控制
|
||||||
|
- Menu 组件已在原代码中 hidden,本次未改动
|
||||||
|
|
||||||
|
## Grid.tsx 硬编码改造 (13:48)
|
||||||
|
- 将首页4个功能按钮从后端接口请求改为硬编码
|
||||||
|
- 菜单项:推荐客户、客户列表、邀请好友、个人中心
|
||||||
|
- 图片使用OSS直链,避免接口延迟
|
||||||
|
- 对应页面路径保持不变
|
||||||
|
|
||||||
|
## 客户列表显示接待人员 (13:57)
|
||||||
|
- 在客户卡片中增加接待人员信息显示
|
||||||
|
- 字段来源:receptionistName(来自 ShopDealerApply model)
|
||||||
|
- 仅当接待人员存在时显示
|
||||||
84
.workbuddy/memory/2026-04-14.md
Normal file
84
.workbuddy/memory/2026-04-14.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 2026-04-14 日志
|
||||||
|
|
||||||
|
## 项目:客资管理系统(Customer Lead Management System)
|
||||||
|
|
||||||
|
### 需求背景
|
||||||
|
用户委托开发一个客资管理系统,具备以下功能:
|
||||||
|
1. **客资派单** - 管理员直接派单给业务员
|
||||||
|
2. **全民推荐** - 任何人可推荐客户赚取推荐费
|
||||||
|
3. **推荐人报备** - 注册用户可报备客户
|
||||||
|
4. **实时跟进** - 实时查看跟进情况和成交状态
|
||||||
|
5. **多管理员** - 支持多管理员设置
|
||||||
|
6. **数据统计导出** - 统计报表功能
|
||||||
|
|
||||||
|
### 涉及项目
|
||||||
|
- Java后端: `/Users/gxwebsoft/JAVA/mp-java`
|
||||||
|
- Vue后台管理: `/Users/gxwebsoft/VUE/mp-vue`
|
||||||
|
- 小程序端: `/Users/gxwebsoft/VUE/template-10582`
|
||||||
|
|
||||||
|
### 状态
|
||||||
|
- [x] 探索代码结构
|
||||||
|
- [x] 设计数据库schema
|
||||||
|
- [x] 设计API接口
|
||||||
|
- [x] 实施开发
|
||||||
|
|
||||||
|
### 实施成果
|
||||||
|
|
||||||
|
#### Java后端 (`/Users/gxwebsoft/JAVA/mp-java`)
|
||||||
|
- SQL脚本: `docs/sql/customer_lead_system.sql`
|
||||||
|
- Entity: CustomerLeadEntity, LeadDispatch, LeadFollowLog, LeadReferral, LeadStatistics, UserRoleExtend
|
||||||
|
- Mapper: CustomerLeadMapper, LeadDispatchMapper, LeadFollowLogMapper, LeadReferralMapper
|
||||||
|
- Service: CustomerLeadService, LeadReferralService
|
||||||
|
- Controller: CustomerLeadController, LeadReferralController
|
||||||
|
- 总结文档: `docs/ai/customer-lead-system-summary.md`
|
||||||
|
|
||||||
|
#### Vue后台 (`/Users/gxwebsoft/VUE/mp-vue`)
|
||||||
|
- API: `api/cms/customerLead/index.ts`, `model.ts`
|
||||||
|
- 页面: `views/cms/customerLead/index.vue`
|
||||||
|
|
||||||
|
#### 小程序端 (`/Users/gxwebsoft/VUE/template-10582`)
|
||||||
|
- API: `api/shop/referral.ts`
|
||||||
|
- 页面: `dealer/referral/index.tsx`
|
||||||
|
- 首页入口: 在分销商首页添加「推荐客户」功能
|
||||||
|
|
||||||
|
### 编译错误修复(10:14)
|
||||||
|
1. **Controller层** - `R.ok()` → `ApiResult` + `success()`,两个Controller均继承 `BaseController`
|
||||||
|
- `CustomerLeadController`: 去掉 `import R`,改用 `ApiResult` + `extends BaseController`
|
||||||
|
- `LeadReferralController`: 同上
|
||||||
|
|
||||||
|
2. **Service层** - `userService.getLoginUser()` 方法不存在
|
||||||
|
- `CustomerLeadServiceImpl`: 去掉 `UserService` 注入,改用私有 `getLoginUser()` 方法(通过 `SecurityContextHolder` 实现,与 `BaseController` 逻辑一致)
|
||||||
|
- `LeadReferralServiceImpl`: 同上;另外修正 `userService.getUserById()` → `userService.getByIdRel()`(`UserService` 仍保留用于按ID查询用户)
|
||||||
|
|
||||||
|
**项目规范记录**:
|
||||||
|
- 返回值用 `ApiResult<T>` + `success()`,Controller继承 `BaseController`
|
||||||
|
- Service层获取登录用户:`SecurityContextHolder.getContext().getAuthentication().getPrincipal()` 强转 `User`
|
||||||
|
- 按ID查询用户:`userService.getByIdRel(userId)`(非 `getUserById`)
|
||||||
|
|
||||||
|
### 推荐客户模块重构(2026-04-16)
|
||||||
|
- **包名变更**:`cms.LeadReferralController` → `app.recommendation.LeadReferralController`
|
||||||
|
- **API路径**:`/lead/referral` → `/app/lead/referral`
|
||||||
|
- **数据库表前缀**:`cms_contact_lead` → `app_lead_referral`
|
||||||
|
|
||||||
|
#### 新建文件清单
|
||||||
|
| 项目 | 文件 |
|
||||||
|
|------|------|
|
||||||
|
| Java后端 | `app/recommendation/entity/LeadReferral.java` |
|
||||||
|
| | `app/recommendation/entity/ReferrerInfo.java` |
|
||||||
|
| | `app/recommendation/entity/ReferralSettlement.java` |
|
||||||
|
| | `app/recommendation/mapper/LeadReferralMapper.java` |
|
||||||
|
| | `app/recommendation/mapper/ReferrerInfoMapper.java` |
|
||||||
|
| | `app/recommendation/mapper/ReferralSettlementMapper.java` |
|
||||||
|
| | `app/recommendation/service/LeadReferralService.java` |
|
||||||
|
| | `app/recommendation/service/impl/LeadReferralServiceImpl.java` |
|
||||||
|
| | `app/recommendation/controller/LeadReferralController.java`(小程序端) |
|
||||||
|
| | `app/recommendation/controller/LeadReferralAdminController.java`(后台管理) |
|
||||||
|
| | `app/recommendation/param/LeadReferralParam.java` |
|
||||||
|
| | `resources/db/sql/app_lead_referral.sql` |
|
||||||
|
| 小程序 | `src/api/app/referral.ts`(新API) |
|
||||||
|
| | `src/recommendation/index.tsx`(独立推荐页面) |
|
||||||
|
| | `src/recommendation/index.scss` |
|
||||||
|
| | `src/dealer/referral/index.tsx`(改用新API) |
|
||||||
|
| | `src/app.config.ts`(新增recommendation子包路由) |
|
||||||
|
| Vue后台 | `src/api/app/referral.ts` |
|
||||||
|
| | `src/views/cms/recommendation/index.vue` |
|
||||||
@@ -20,21 +20,21 @@
|
|||||||
- 邀请码:`src/dealer/qrcode/index`
|
- 邀请码:`src/dealer/qrcode/index`
|
||||||
- 导航工具:`src/utils/common.ts` (navTo函数)
|
- 导航工具:`src/utils/common.ts` (navTo函数)
|
||||||
|
|
||||||
## 首页结构 (2026-04-02 更新)
|
## 首页结构 (2026-04-09 重大重构)
|
||||||
- Header (吸顶搜索栏)
|
- Header (吸顶搜索栏,深色渐变背景)
|
||||||
- Menu (导航菜单,hidden)
|
- **Hero区域** (Banner + Grid 融合,圆角过渡)
|
||||||
- Banner (轮播广告)
|
- **NoticeStrip** (自定义轻量公告条,黄色圆点+文字)
|
||||||
- Grid (功能菜单)
|
- **BestSellers** (热销推荐 — 横向图文卡片,左图右信息)
|
||||||
- NoticeBar (公告栏)
|
- **CatalogShowcase** (品牌画册 — 大面积渐变视觉卡片)
|
||||||
- **BrochureEntry** (品牌画册入口卡片 — 位于公告栏下方)
|
- **TrustSection** (品质信任区 — 横向滑动渐变卡片:10年质保/15年经验/98%满意度)
|
||||||
- BestSellers (热销商品)
|
- **ContactSection** (联系我们 — 全宽深色沉浸式底部)
|
||||||
- **TrustSection** (品牌信任区 - 3列水平布局)
|
- PopUpAd (弹窗广告)
|
||||||
- 品质保障:10年质保,德国进口五金
|
|
||||||
- 专业团队:15年安装经验,持证上岗
|
### 设计特点
|
||||||
- 客户好评:5000+家庭选择,98%满意度
|
- 非豆腐块布局,差异化间距和视觉层次
|
||||||
- ~~CaseShowcase~~ (已注释隐藏,待有真实素材后再恢复)
|
- 页面背景:#f5f5f7
|
||||||
- **ContactSection** (联系方式 - 2x2网格布局)
|
- 底部区域为深色渐变(#1e293b→#0f172a)
|
||||||
- 客服热线、在线咨询、门店地址、关注我们
|
- ~~CaseShowcase~~ (已注释隐藏)
|
||||||
|
|
||||||
## 图标使用注意事项
|
## 图标使用注意事项
|
||||||
- NutUI图标库中不存在的图标:
|
- NutUI图标库中不存在的图标:
|
||||||
@@ -45,13 +45,23 @@
|
|||||||
- 所有图标必须从 `@nutui/icons-react-taro` 导入
|
- 所有图标必须从 `@nutui/icons-react-taro` 导入
|
||||||
- 构建前需验证图标名称是否在可用导出列表中
|
- 构建前需验证图标名称是否在可用导出列表中
|
||||||
|
|
||||||
## 字体大小规范
|
## 字体大小规范 (2026-04-09 更新)
|
||||||
- 微信小程序使用 TailwindCSS 文本类,不使用固定像素值
|
- 微信小程序端按移动端标准设计,最小辅助文字 13-14px
|
||||||
- 主标题:`text-lg font-semibold text-gray-800`
|
- 使用 TailwindCSS 工具类(text-xs/sm/base/lg/xl/2xl/3xl/4xl),不使用 SCSS font-size
|
||||||
- 副标题:`text-sm text-gray-500`
|
- 扩展了 tailwind.config.js:text-15(15px)、text-17(17px)、text-28(28px)
|
||||||
- 项目标题:`text-base font-semibold text-gray-800`
|
- 标签/辅助:14px (text-sm)
|
||||||
- 项目描述:`text-xs text-gray-500`
|
- 正文/描述:15-16px (text-15/text-base)
|
||||||
- 小文本:`text-xs text-gray-500`
|
- 小标题/按钮文字:16-17px (text-base/text-17)
|
||||||
|
- 区块标题:20-24px (text-xl/text-2xl)
|
||||||
|
- 强调数字(价格、数据):28-36px (text-28/text-4xl)
|
||||||
|
- 所有图标必须从 `@nutui/icons-react-taro` 导入
|
||||||
|
|
||||||
|
## Tailwind 小程序兼容性注意事项
|
||||||
|
- **禁止使用斜杠透明度简写**:`text-white/40`、`bg-black/50`、`border-gray/30` 等含 `/` 的写法在小程序不兼容,需要透明度时改用 SCSS `color: rgba(...)` 或在 tailwind.config.js 中定义语义色
|
||||||
|
- **禁止使用小数间距**:`mt-0.5`、`p-0.5`、`gap-0.5` 等含小数点的工具类在小程序不兼容,改用整数如 `mt-1`、`p-1`
|
||||||
|
- **禁止使用任意值 `-[?]` 写法**:`text-[14px]`、`w-[100px]` 等方括号任意值写法在小程序不兼容,改用 Tailwind 预设类或在 tailwind.config.js 中扩展
|
||||||
|
- **颜色替代方案**:`text-white` 在小程序中可能不生效,改用 `text-gray`(如 `text-gray-100`~`text-gray-50` 等),深色背景上的白色文字优先用 `text-gray-100`
|
||||||
|
- 当前代码中 ContactSection/TrustSection/CatalogShowcase 共有 9 处 `text-white/xx` 待修复
|
||||||
|
|
||||||
## 画册页正式版 (2026-04-01)
|
## 画册页正式版 (2026-04-01)
|
||||||
- 首页已实际挂载 `src/pages/index/BrochureEntry.tsx` 入口,位置在 Banner 下方。
|
- 首页已实际挂载 `src/pages/index/BrochureEntry.tsx` 入口,位置在 Banner 下方。
|
||||||
|
|||||||
112
src/api/app/referral.ts
Normal file
112
src/api/app/referral.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* 推荐客户 API(app 模块)
|
||||||
|
* 小程序端调用
|
||||||
|
*/
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报备参数
|
||||||
|
*/
|
||||||
|
export interface ReferralParam {
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
customerCompany?: string
|
||||||
|
requirement?: string
|
||||||
|
appointmentTime?: string
|
||||||
|
remarks?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐记录
|
||||||
|
*/
|
||||||
|
export interface ReferralRecord {
|
||||||
|
id: number
|
||||||
|
referralCode: string
|
||||||
|
referrerId?: number
|
||||||
|
referrerName?: string
|
||||||
|
referrerPhone?: string
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
customerCompany?: string
|
||||||
|
requirement?: string
|
||||||
|
appointmentTime?: string
|
||||||
|
remarks?: string
|
||||||
|
referralFee: string
|
||||||
|
referralStatus: number
|
||||||
|
referralStatusText?: string
|
||||||
|
invalidReason?: string
|
||||||
|
invalidTime?: string
|
||||||
|
confirmedTime?: string
|
||||||
|
settledTime?: string
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的推荐统计
|
||||||
|
*/
|
||||||
|
export interface ReferralStats {
|
||||||
|
totalCount: number
|
||||||
|
pendingCount: number
|
||||||
|
validCount: number
|
||||||
|
settledCount: number
|
||||||
|
pendingAmount: string
|
||||||
|
referralCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页结果
|
||||||
|
*/
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
pageNum: number
|
||||||
|
pageSize: number
|
||||||
|
pages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报备客户
|
||||||
|
*/
|
||||||
|
export function addReferral(data: ReferralParam) {
|
||||||
|
return request.post<{ code: number; message: string; data: ReferralRecord }>(
|
||||||
|
'/app/lead/referral/add',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的推荐码
|
||||||
|
*/
|
||||||
|
export function getMyReferralCode() {
|
||||||
|
return request.get<{ code: number; message: string; data: string }>(
|
||||||
|
'/app/lead/referral/my/code'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的推荐记录(分页)
|
||||||
|
*/
|
||||||
|
export function getMyReferrals(params: { pageNum?: number; pageSize?: number }) {
|
||||||
|
return request.get<{ code: number; message: string; data: PageResult<ReferralRecord> }>(
|
||||||
|
'/app/lead/referral/my',
|
||||||
|
{ params }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的推荐统计
|
||||||
|
*/
|
||||||
|
export function getMyStats() {
|
||||||
|
return request.get<{ code: number; message: string; data: ReferralStats }>(
|
||||||
|
'/app/lead/referral/my/stats'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据推荐码获取推荐人信息
|
||||||
|
*/
|
||||||
|
export function getReferrerByCode(code: string) {
|
||||||
|
return request.get<{ code: number; message: string; data: { referrerId: number; referrerName: string; referralCode: string } }>(
|
||||||
|
`/app/lead/referral/referrer/${code}`
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -105,8 +105,8 @@ export async function getCmsNavigation(id: number) {
|
|||||||
const res = await request.get<ApiResult<CmsNavigation>>(
|
const res = await request.get<ApiResult<CmsNavigation>>(
|
||||||
'/cms/cms-navigation/' + id
|
'/cms/cms-navigation/' + id
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data ?? null;
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|||||||
135
src/api/shop/referral.ts
Normal file
135
src/api/shop/referral.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* 客资推荐人 API
|
||||||
|
* 小程序端调用
|
||||||
|
*/
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐人报备参数
|
||||||
|
*/
|
||||||
|
export interface ReferralParam {
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
customerCompany?: string
|
||||||
|
requirement?: string
|
||||||
|
appointmentTime?: string
|
||||||
|
remarks?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐人报备结果
|
||||||
|
*/
|
||||||
|
export interface ReferralResult {
|
||||||
|
referralId: number
|
||||||
|
referralCode: string
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
referralFee?: number
|
||||||
|
referralStatus: number
|
||||||
|
referralStatusText: string
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐人统计
|
||||||
|
*/
|
||||||
|
export interface ReferralStats {
|
||||||
|
totalCount: number
|
||||||
|
pendingCount: number
|
||||||
|
validCount: number
|
||||||
|
settledCount: number
|
||||||
|
pendingAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐人记录
|
||||||
|
*/
|
||||||
|
export interface ReferralRecord {
|
||||||
|
referralId: number
|
||||||
|
referredLeadId: number
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
referralFee: number
|
||||||
|
referralStatus: number
|
||||||
|
referralStatusText: string
|
||||||
|
leadStatus?: number
|
||||||
|
leadStatusText?: string
|
||||||
|
dealAmount?: number
|
||||||
|
createTime: string
|
||||||
|
settlementTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册用户报备客户
|
||||||
|
*/
|
||||||
|
export function addReferral(data: ReferralParam) {
|
||||||
|
return request.post<{ code: number; message: string; data: ReferralResult }>(
|
||||||
|
'/lead/referral/user',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐人的推荐记录
|
||||||
|
*/
|
||||||
|
export function getReferralList(params: { pageNum?: number; pageSize?: number }) {
|
||||||
|
return request.get<{ code: number; message: string; data: { list: ReferralRecord[]; total: number } }>(
|
||||||
|
'/lead/referral/page',
|
||||||
|
{ params }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐人统计
|
||||||
|
*/
|
||||||
|
export function getReferralStats(userId: number) {
|
||||||
|
return request.get<{ code: number; message: string; data: ReferralStats }>(
|
||||||
|
`/lead/referral/stats/${userId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成推荐码
|
||||||
|
*/
|
||||||
|
export function generateReferralCode() {
|
||||||
|
return request.get<{ code: number; message: string; data: string }>(
|
||||||
|
'/lead/referral/generateCode'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户的推荐码
|
||||||
|
*/
|
||||||
|
export function getMyReferralCode() {
|
||||||
|
return request.get<{ code: number; message: string; data: { referralCode: string } }>(
|
||||||
|
'/lead/referral/my/code'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认推荐有效(管理员)
|
||||||
|
*/
|
||||||
|
export function confirmReferral(referralId: number) {
|
||||||
|
return request.put<{ code: number; message: string }>(
|
||||||
|
`/lead/referral/confirm/${referralId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作废推荐(管理员)
|
||||||
|
*/
|
||||||
|
export function invalidateReferral(referralId: number, reason?: string) {
|
||||||
|
return request.put<{ code: number; message: string }>(
|
||||||
|
`/lead/referral/invalidate/${referralId}`,
|
||||||
|
{ reason }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结算推荐费(管理员)
|
||||||
|
*/
|
||||||
|
export function settleReferral(referralId: number) {
|
||||||
|
return request.put<{ code: number; message: string }>(
|
||||||
|
`/lead/referral/settle/${referralId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -48,6 +48,10 @@ export interface ShopDealerApply {
|
|||||||
nickName?: string;
|
nickName?: string;
|
||||||
// 推荐人名称
|
// 推荐人名称
|
||||||
refereeName?: string;
|
refereeName?: string;
|
||||||
|
// 接待人员用户ID
|
||||||
|
receptionistId?: number;
|
||||||
|
// 接待人员姓名
|
||||||
|
receptionistName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,4 +66,5 @@ export interface ShopDealerApplyParam extends PageParam {
|
|||||||
userId?: number;
|
userId?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
applyStatus?: number; // 申请状态筛选 (10待审核 20审核通过 30驳回)
|
applyStatus?: number; // 申请状态筛选 (10待审核 20审核通过 30驳回)
|
||||||
|
receptionistId?: number; // 接待人员用户ID(用于查询分配给自己的客户)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ export async function getShopDealerReferee(id: number) {
|
|||||||
const res = await request.get<ApiResult<ShopDealerReferee>>(
|
const res = await request.get<ApiResult<ShopDealerReferee>>(
|
||||||
'/shop/shop-dealer-referee/' + id
|
'/shop/shop-dealer-referee/' + id
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data ?? null;
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
@@ -107,8 +107,8 @@ export async function getShopDealerRefereeByUserId(userId: number) {
|
|||||||
const res = await request.get<ApiResult<ShopDealerReferee>>(
|
const res = await request.get<ApiResult<ShopDealerReferee>>(
|
||||||
'/shop/shop-dealer-referee/getByUserId/' + userId
|
'/shop/shop-dealer-referee/getByUserId/' + userId
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data ?? null;
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,14 @@ export default defineAppConfig({
|
|||||||
"customer/trading",
|
"customer/trading",
|
||||||
"wechat/index",
|
"wechat/index",
|
||||||
"bank/index",
|
"bank/index",
|
||||||
"bank/add"
|
"bank/add",
|
||||||
|
"referral/index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "recommendation",
|
||||||
|
"pages": [
|
||||||
|
"index"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const AddUserAddress = () => {
|
|||||||
if (roles.length > 0) {
|
if (roles.length > 0) {
|
||||||
await updateUserRole({
|
await updateUserRole({
|
||||||
...roles[0],
|
...roles[0],
|
||||||
roleId: 1848
|
roleId: 1935
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,16 +231,20 @@ const AddUserAddress = () => {
|
|||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
// 注册成功后等待1.5秒,让权限同步生效
|
||||||
Taro.navigateBack();
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
// 重新登录刷新用户状态(包括最新权限)
|
||||||
console.error('验证邀请人失败:', error);
|
Taro.removeStorageSync('Token');
|
||||||
|
Taro.removeStorageSync('UserId');
|
||||||
|
Taro.reLaunch({ url: '/pages/index/index' });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('注册失败:', error);
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '注册失败,请重试',
|
title: error?.message || '注册失败,请重试',
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
})
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
1549
src/dealer/customer/add-bak.tsx
Normal file
1549
src/dealer/customer/add-bak.tsx
Normal file
File diff suppressed because it is too large
Load Diff
276
src/dealer/customer/add.scss
Normal file
276
src/dealer/customer/add.scss
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
.add-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 顶部模式提示条 ═══ */
|
||||||
|
.add-page__mode-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__mode-bar--edit {
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 表单卡片分组 ═══ */
|
||||||
|
.add-page__card {
|
||||||
|
margin: 12px 12px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: white;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ NutUI Form/CellGroup 样式覆盖 ═══ */
|
||||||
|
.add-page__card .nut-cell-group {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__card .nut-cell::after {
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__card .nut-form-item .nut-cell__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__card .nut-form-item .nut-input {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__card .nut-form-item .nut-input__inner {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__card .nut-cell__value {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 章节标题 ═══ */
|
||||||
|
.add-page__section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 16px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #9ca3af;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__section-title::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #3b82f6;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 接待人员选择 ═══ */
|
||||||
|
.add-page__receptionist-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__receptionist-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__receptionist-name {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #111827;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__receptionist-placeholder {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__receptionist-clear {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 日期选择器触发器 ═══ */
|
||||||
|
.add-page__date-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__date-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__date-placeholder {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 底部按钮区 ═══ */
|
||||||
|
.add-page__footer {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 50;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
|
background: linear-gradient(to top, rgba(255, 255, 255, 1) 60%, rgba(255, 255, 255, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__submit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
|
color: white;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__submit-btn--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__bottom-spacer {
|
||||||
|
height: 100px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 审核状态区 ═══ */
|
||||||
|
.add-page__status-section {
|
||||||
|
margin: 12px 12px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__status-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__status-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__status-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ Popup 选择接待人员 ═══ */
|
||||||
|
.add-page__picker-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-cancel {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #3b82f6;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-search {
|
||||||
|
padding: 12px 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-item:active {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-item-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-item-phone {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-item-selected {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-page__picker-empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {useEffect, useState, useRef} from "react";
|
import {useEffect, useState, useRef} from "react";
|
||||||
import {Loading, CellGroup, Cell, Input, Form, Calendar} from '@nutui/nutui-react-taro'
|
import {Loading, CellGroup, Cell, Input, Form, Calendar, Popup, SearchBar} from '@nutui/nutui-react-taro'
|
||||||
import {Edit, Calendar as CalendarIcon} from '@nutui/icons-react-taro'
|
import {Edit, Calendar as CalendarIcon, ArrowRight, Del} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useRouter} from '@tarojs/taro'
|
import {useRouter} from '@tarojs/taro'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
extractDateForCalendar, formatDateForDisplay
|
extractDateForCalendar, formatDateForDisplay
|
||||||
} from "@/utils/dateUtils";
|
} from "@/utils/dateUtils";
|
||||||
import {ShopDealerUser} from "@/api/shop/shopDealerUser/model";
|
import {ShopDealerUser} from "@/api/shop/shopDealerUser/model";
|
||||||
import {getShopDealerUser} from "@/api/shop/shopDealerUser";
|
import {getShopDealerUser, pageShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||||
|
|
||||||
const AddShopDealerApply = () => {
|
const AddShopDealerApply = () => {
|
||||||
const {params} = useRouter();
|
const {params} = useRouter();
|
||||||
@@ -62,6 +62,13 @@ const AddShopDealerApply = () => {
|
|||||||
const [applyTime, setApplyTime] = useState<string>('')
|
const [applyTime, setApplyTime] = useState<string>('')
|
||||||
const [contractTime, setContractTime] = useState<string>('')
|
const [contractTime, setContractTime] = useState<string>('')
|
||||||
|
|
||||||
|
// 接待人员选择状态
|
||||||
|
const [showReceptionistPicker, setShowReceptionistPicker] = useState<boolean>(false)
|
||||||
|
const [receptionistSearch, setReceptionistSearch] = useState<string>('')
|
||||||
|
const [receptionistList, setReceptionistList] = useState<ShopDealerUser[]>([])
|
||||||
|
const [receptionistLoading, setReceptionistLoading] = useState<boolean>(false)
|
||||||
|
const [selectedReceptionist, setSelectedReceptionist] = useState<ShopDealerUser | null>(null)
|
||||||
|
|
||||||
// 获取审核状态文字
|
// 获取审核状态文字
|
||||||
const getApplyStatusText = (status?: number) => {
|
const getApplyStatusText = (status?: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -133,6 +140,15 @@ const AddShopDealerApply = () => {
|
|||||||
setContractTime(extractDateForCalendar(dealerApply.contractTime))
|
setContractTime(extractDateForCalendar(dealerApply.contractTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 回填接待人员
|
||||||
|
if (dealerApply.receptionistId) {
|
||||||
|
setSelectedReceptionist({
|
||||||
|
userId: dealerApply.receptionistId,
|
||||||
|
dealerName: dealerApply.receptionistName || '',
|
||||||
|
realName: dealerApply.receptionistName || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Taro.setNavigationBarTitle({title: '签约'})
|
Taro.setNavigationBarTitle({title: '签约'})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -143,6 +159,43 @@ const AddShopDealerApply = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载接待人员列表
|
||||||
|
const loadReceptionistList = async (keyword?: string) => {
|
||||||
|
setReceptionistLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await pageShopDealerUser({keywords: keyword || '', limit: 50, page: 1})
|
||||||
|
setReceptionistList(res?.list || [])
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载接待人员失败:', e)
|
||||||
|
} finally {
|
||||||
|
setReceptionistLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开接待人员选择
|
||||||
|
const openReceptionistPicker = () => {
|
||||||
|
setReceptionistSearch('')
|
||||||
|
loadReceptionistList()
|
||||||
|
setShowReceptionistPicker(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索接待人员
|
||||||
|
const handleReceptionistSearch = (val: string) => {
|
||||||
|
setReceptionistSearch(val)
|
||||||
|
loadReceptionistList(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择接待人员
|
||||||
|
const handleSelectReceptionist = (user: ShopDealerUser) => {
|
||||||
|
setSelectedReceptionist(user)
|
||||||
|
setShowReceptionistPicker(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除接待人员
|
||||||
|
const handleClearReceptionist = () => {
|
||||||
|
setSelectedReceptionist(null)
|
||||||
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
// 计算保护期过期时间(15天后)
|
// 计算保护期过期时间(15天后)
|
||||||
const calculateExpirationTime = (): string => {
|
const calculateExpirationTime = (): string => {
|
||||||
@@ -455,7 +508,10 @@ const AddShopDealerApply = () => {
|
|||||||
expirationTime: expirationTime,
|
expirationTime: expirationTime,
|
||||||
// 确保日期数据正确提交(使用数据库格式)
|
// 确保日期数据正确提交(使用数据库格式)
|
||||||
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
|
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
|
||||||
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : '')
|
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : ''),
|
||||||
|
// 接待人员
|
||||||
|
receptionistId: selectedReceptionist?.userId || undefined,
|
||||||
|
receptionistName: selectedReceptionist ? (selectedReceptionist.realName || selectedReceptionist.dealerName || '') : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 调试信息
|
// 调试信息
|
||||||
@@ -546,8 +602,33 @@ const AddShopDealerApply = () => {
|
|||||||
onFinish={(values) => submitSucceed(values)}
|
onFinish={(values) => submitSucceed(values)}
|
||||||
onFinishFailed={(errors) => submitFailed(errors)}
|
onFinishFailed={(errors) => submitFailed(errors)}
|
||||||
>
|
>
|
||||||
<View className={'bg-gray-100 h-3'}></View>
|
<CellGroup style={{padding: '0'}}>
|
||||||
<CellGroup style={{padding: '4px 0'}}>
|
{/* 接待人员选择 */}
|
||||||
|
<Cell
|
||||||
|
title="接待人员"
|
||||||
|
extra={
|
||||||
|
<View className="flex items-center">
|
||||||
|
{selectedReceptionist ? (
|
||||||
|
<View className="flex items-center">
|
||||||
|
<Text className="text-sm text-gray-800 mr-2">
|
||||||
|
{selectedReceptionist.realName || selectedReceptionist.dealerName || '已选择'}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleClearReceptionist(); }}
|
||||||
|
className="flex items-center px-1"
|
||||||
|
>
|
||||||
|
<Del size={14} color="#999"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text className="text-sm text-gray-400">请选择</Text>
|
||||||
|
)}
|
||||||
|
<ArrowRight size={14} color="#ccc"/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
onClick={openReceptionistPicker}
|
||||||
|
/>
|
||||||
|
<View className={'bg-gray-100 h-2'}></View>
|
||||||
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
|
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
|
||||||
<Input placeholder="幸福里" disabled={isEditMode}/>
|
<Input placeholder="幸福里" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -566,6 +647,9 @@ const AddShopDealerApply = () => {
|
|||||||
<Form.Item name="mobile" label="手机号" initialValue={FormData?.mobile} required>
|
<Form.Item name="mobile" label="手机号" initialValue={FormData?.mobile} required>
|
||||||
<Input placeholder="手机号" disabled={isEditMode} maxLength={11}/>
|
<Input placeholder="手机号" disabled={isEditMode} maxLength={11}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="comments" label="备注" initialValue={FormData?.comments}>
|
||||||
|
<Input placeholder="请输入备注信息" />
|
||||||
|
</Form.Item>
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
|
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
|
||||||
@@ -628,6 +712,59 @@ const AddShopDealerApply = () => {
|
|||||||
onConfirm={handleContractTimeConfirm}
|
onConfirm={handleContractTimeConfirm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 接待人员选择弹出层 */}
|
||||||
|
<Popup
|
||||||
|
visible={showReceptionistPicker}
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
onClose={() => setShowReceptionistPicker(false)}
|
||||||
|
style={{height: '70%'}}
|
||||||
|
>
|
||||||
|
<View className="flex flex-col h-full">
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<View className="flex items-center justify-between px-4 py-3 border-b border-gray-100">
|
||||||
|
<Text className="text-base font-semibold text-gray-800">选择接待人员</Text>
|
||||||
|
<View onClick={() => setShowReceptionistPicker(false)}>
|
||||||
|
<Text className="text-sm text-blue-500">取消</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<View className="px-3 py-2">
|
||||||
|
<SearchBar
|
||||||
|
value={receptionistSearch}
|
||||||
|
placeholder="搜索姓名/手机号"
|
||||||
|
onChange={handleReceptionistSearch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{/* 列表 */}
|
||||||
|
<View className="flex-1 overflow-y-auto">
|
||||||
|
{receptionistLoading ? (
|
||||||
|
<View className="flex justify-center items-center py-8">
|
||||||
|
<Loading>加载中</Loading>
|
||||||
|
</View>
|
||||||
|
) : receptionistList.length === 0 ? (
|
||||||
|
<View className="flex justify-center items-center py-8">
|
||||||
|
<Text className="text-sm text-gray-400">暂无数据</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
receptionistList.map((user) => (
|
||||||
|
<Cell
|
||||||
|
key={user.userId}
|
||||||
|
title={user.realName || user.dealerName || '未知'}
|
||||||
|
description={user.mobile || user.dealerPhone || ''}
|
||||||
|
extra={
|
||||||
|
selectedReceptionist?.userId === user.userId ? (
|
||||||
|
<Text className="text-sm text-blue-500">已选</Text>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={() => handleSelectReceptionist(user)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
{/* 审核状态显示(仅在编辑模式下显示) */}
|
{/* 审核状态显示(仅在编辑模式下显示) */}
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useState, useEffect, useCallback, useRef} from 'react'
|
import {useState, useEffect, useCallback} from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
import Taro, {useDidShow} from '@tarojs/taro'
|
||||||
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
||||||
@@ -33,24 +33,10 @@ const CustomerIndex = () => {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
|
||||||
// 非分销商不允许查看客户列表
|
// 权限检查:只要登录就能查看客户列表
|
||||||
const {user, hasRole, loading: userLoading} = useUser()
|
const {user, loading: userLoading} = useUser()
|
||||||
// 管理员允许查看全部;普通分销商仅查看自己
|
|
||||||
const isAdminUser = user?.isAdmin === true
|
|
||||||
const canView = hasRole('dealer') || isAdminUser
|
|
||||||
const roleCheckFinished = !userLoading
|
const roleCheckFinished = !userLoading
|
||||||
const noPermissionShownRef = useRef(false)
|
const isLoggedIn = roleCheckFinished && user !== null
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!roleCheckFinished || canView) return
|
|
||||||
if (noPermissionShownRef.current) return
|
|
||||||
noPermissionShownRef.current = true
|
|
||||||
Taro.showToast({
|
|
||||||
title: '没有查看权限',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 1500
|
|
||||||
})
|
|
||||||
}, [roleCheckFinished, canView])
|
|
||||||
|
|
||||||
// Tab配置
|
// Tab配置
|
||||||
const tabList = getStatusOptions();
|
const tabList = getStatusOptions();
|
||||||
@@ -201,14 +187,13 @@ const CustomerIndex = () => {
|
|||||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
||||||
|
|
||||||
// 构建API参数,根据状态筛选
|
// 构建API参数,根据状态筛选
|
||||||
|
// 查看自己提交的(userId)或分配给自己的(receptionistId)的客户
|
||||||
const params: any = {
|
const params: any = {
|
||||||
type: 4,
|
type: 4,
|
||||||
page: currentPage
|
page: currentPage,
|
||||||
|
userId: currentUserId,
|
||||||
|
receptionistId: currentUserId
|
||||||
};
|
};
|
||||||
// 非管理员:只看自己添加的客户
|
|
||||||
if (!isAdminUser && currentUserId > 0) {
|
|
||||||
params.userId = currentUserId;
|
|
||||||
}
|
|
||||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||||
if (applyStatus !== undefined) {
|
if (applyStatus !== undefined) {
|
||||||
params.applyStatus = applyStatus;
|
params.applyStatus = applyStatus;
|
||||||
@@ -251,7 +236,7 @@ const CustomerIndex = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [activeTab, page, isAdminUser, user?.userId]);
|
}, [activeTab, page, user?.userId]);
|
||||||
|
|
||||||
const reloadMore = async () => {
|
const reloadMore = async () => {
|
||||||
if (loading || !hasMore) return; // 防止重复加载
|
if (loading || !hasMore) return; // 防止重复加载
|
||||||
@@ -300,11 +285,11 @@ const CustomerIndex = () => {
|
|||||||
const fetchStatusCounts = useCallback(async () => {
|
const fetchStatusCounts = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
||||||
const baseParams: any = {type: 4};
|
const baseParams: any = {
|
||||||
// 非管理员:只统计自己添加的客户
|
type: 4,
|
||||||
if (!isAdminUser && currentUserId > 0) {
|
userId: currentUserId,
|
||||||
baseParams.userId = currentUserId;
|
receptionistId: currentUserId
|
||||||
}
|
};
|
||||||
|
|
||||||
// 并行获取各状态的数量
|
// 并行获取各状态的数量
|
||||||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
||||||
@@ -323,7 +308,7 @@ const CustomerIndex = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取状态统计失败:', error);
|
console.error('获取状态统计失败:', error);
|
||||||
}
|
}
|
||||||
}, [isAdminUser, user?.userId]);
|
}, [user?.userId]);
|
||||||
|
|
||||||
const getStatusCounts = () => statusCounts;
|
const getStatusCounts = () => statusCounts;
|
||||||
|
|
||||||
@@ -364,22 +349,22 @@ const CustomerIndex = () => {
|
|||||||
|
|
||||||
// 初始化统计数据
|
// 初始化统计数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roleCheckFinished || !canView) return;
|
if (!isLoggedIn) return;
|
||||||
fetchStatusCounts().then();
|
fetchStatusCounts().then();
|
||||||
}, [roleCheckFinished, canView]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
// 当activeTab变化时重新获取数据
|
// 当activeTab变化时重新获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roleCheckFinished || !canView) return;
|
if (!isLoggedIn) return;
|
||||||
setList([]); // 清空列表
|
setList([]); // 清空列表
|
||||||
setPage(1); // 重置页码
|
setPage(1); // 重置页码
|
||||||
setHasMore(true); // 重置加载状态
|
setHasMore(true); // 重置加载状态
|
||||||
fetchCustomerData(activeTab, true);
|
fetchCustomerData(activeTab, true);
|
||||||
}, [activeTab, roleCheckFinished, canView]);
|
}, [activeTab, isLoggedIn]);
|
||||||
|
|
||||||
// 监听页面显示,当从其他页面返回时刷新数据
|
// 监听页面显示,当从其他页面返回时刷新数据
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
if (!roleCheckFinished || !canView) return;
|
if (!isLoggedIn) return;
|
||||||
// 刷新当前tab的数据和统计信息
|
// 刷新当前tab的数据和统计信息
|
||||||
setList([]);
|
setList([]);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@@ -465,6 +450,13 @@ const CustomerIndex = () => {
|
|||||||
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
|
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 接待人员 */}
|
||||||
|
{customer.receptionistName && (
|
||||||
|
<View className="flex items-center my-1">
|
||||||
|
<Text className="text-xs text-gray-500">接待人员:{customer.receptionistName}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 显示 comments 字段 */}
|
{/* 显示 comments 字段 */}
|
||||||
<Space className="flex items-center">
|
<Space className="flex items-center">
|
||||||
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
||||||
@@ -586,10 +578,11 @@ const CustomerIndex = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canView) {
|
// 未登录时显示提示
|
||||||
|
if (!isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-white flex flex-col items-center justify-center p-4">
|
<View className="bg-white flex flex-col items-center justify-center p-4">
|
||||||
<Empty description="没有查看权限"/>
|
<Empty description="请先登录"/>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
style={{marginTop: '12px'}}
|
style={{marginTop: '12px'}}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
QrCode,
|
QrCode,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Purse,
|
Purse,
|
||||||
People
|
People,
|
||||||
|
Service
|
||||||
} from '@nutui/icons-react-taro'
|
} from '@nutui/icons-react-taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import {useThemeStyles} from '@/hooks/useTheme'
|
import {useThemeStyles} from '@/hooks/useTheme'
|
||||||
@@ -250,6 +251,14 @@ const DealerIndex: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
|
|
||||||
|
<Grid.Item text={'推荐客户'} onClick={() => navigateToPage('/dealer/referral/index')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-pink-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Service color="#ec4899" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '推广二维码'
|
navigationBarTitleText: '账户管理中心',
|
||||||
|
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
|
|||||||
import {View, Text, Image} from '@tarojs/components'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||||
import {Download, QrCode} from '@nutui/icons-react-taro'
|
import {Download, QrCode} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro, {useShareAppMessage} from '@tarojs/taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import {generateInviteCode} from '@/api/invite'
|
import {generateInviteCode} from '@/api/invite'
|
||||||
// import type {InviteStats} from '@/api/invite'
|
// import type {InviteStats} from '@/api/invite'
|
||||||
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
|
|||||||
|
|
||||||
const DealerQrcode: React.FC = () => {
|
const DealerQrcode: React.FC = () => {
|
||||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [codeLoading, setCodeLoading] = useState<boolean>(false)
|
||||||
|
const [saving, setSaving] = useState<boolean>(false)
|
||||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||||
const {dealerUser} = useDealerUser()
|
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
||||||
|
|
||||||
|
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
|
||||||
|
useEffect(() => {
|
||||||
|
// Some clients require explicit call to show both share entries.
|
||||||
|
Taro.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||||
|
}).catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 转发给朋友(分享小程序链接)
|
||||||
|
useShareAppMessage(() => {
|
||||||
|
const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
|
||||||
|
const inviter = Number(inviterRaw)
|
||||||
|
const hasInviter = Number.isFinite(inviter) && inviter > 0
|
||||||
|
|
||||||
|
const user = Taro.getStorageSync('User') || {}
|
||||||
|
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
||||||
|
const title = hasInviter ? `${nickname || '我'}邀请你加入桂乐淘伙伴计划` : '桂乐淘伙伴计划'
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
path: hasInviter
|
||||||
|
? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
|
||||||
|
: `/pages/index/index`,
|
||||||
|
success: function () {
|
||||||
|
Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
|
||||||
|
},
|
||||||
|
fail: function () {
|
||||||
|
Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 生成小程序码
|
// 生成小程序码
|
||||||
const generateMiniProgramCode = async () => {
|
const generateMiniProgramCode = async () => {
|
||||||
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setCodeLoading(true)
|
||||||
|
|
||||||
// 生成邀请小程序码
|
// 生成邀请小程序码
|
||||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||||
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
// 清空之前的二维码
|
// 清空之前的二维码
|
||||||
setMiniProgramCodeUrl('')
|
setMiniProgramCodeUrl('')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setCodeLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId])
|
||||||
|
|
||||||
|
const isAlbumAuthError = (errMsg?: string) => {
|
||||||
|
if (!errMsg) return false
|
||||||
|
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
|
||||||
|
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
|
||||||
|
return (
|
||||||
|
errMsg.includes('auth deny') ||
|
||||||
|
errMsg.includes('auth denied') ||
|
||||||
|
errMsg.includes('authorize') ||
|
||||||
|
errMsg.includes('scope.writePhotosAlbum')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const setting = await Taro.getSetting()
|
||||||
|
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
|
||||||
|
|
||||||
|
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
const modal = await Taro.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
|
||||||
|
confirmText: '去设置'
|
||||||
|
})
|
||||||
|
if (modal.confirm) {
|
||||||
|
await Taro.openSetting()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadImageToLocalPath = async (url: string): Promise<string> => {
|
||||||
|
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
|
||||||
|
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
|
||||||
|
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = Taro.getStorageSync('access_token')
|
||||||
|
const tenantId = Taro.getStorageSync('TenantId')
|
||||||
|
const header: Record<string, string> = {}
|
||||||
|
if (token) header.Authorization = token
|
||||||
|
if (tenantId) header.TenantId = tenantId
|
||||||
|
|
||||||
|
// 先下载到本地临时文件再保存到相册
|
||||||
|
const res = await Taro.downloadFile({url, header})
|
||||||
|
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||||
|
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
|
||||||
|
try {
|
||||||
|
await Taro.getFileInfo({filePath: res.tempFilePath})
|
||||||
|
} catch (_) {
|
||||||
|
throw new Error('图片临时文件不存在,请重试')
|
||||||
|
}
|
||||||
|
return res.tempFilePath
|
||||||
|
}
|
||||||
|
|
||||||
// 保存小程序码到相册
|
// 保存小程序码到相册
|
||||||
const saveMiniProgramCode = async () => {
|
const saveMiniProgramCode = async () => {
|
||||||
if (!miniProgramCodeUrl) {
|
if (!miniProgramCodeUrl) {
|
||||||
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先下载图片到本地
|
if (saving) return
|
||||||
const res = await Taro.downloadFile({
|
setSaving(true)
|
||||||
url: miniProgramCodeUrl
|
Taro.showLoading({title: '保存中...'})
|
||||||
})
|
|
||||||
|
|
||||||
if (res.statusCode === 200) {
|
const hasPermission = await ensureWriteAlbumPermission()
|
||||||
// 保存到相册
|
if (!hasPermission) return
|
||||||
await Taro.saveImageToPhotosAlbum({
|
|
||||||
filePath: res.tempFilePath
|
|
||||||
})
|
|
||||||
|
|
||||||
Taro.showToast({
|
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
|
||||||
title: '保存成功',
|
try {
|
||||||
icon: 'success'
|
await Taro.saveImageToPhotosAlbum({filePath})
|
||||||
})
|
} catch (e: any) {
|
||||||
|
const msg = e?.errMsg || e?.message || ''
|
||||||
|
// Fallback: some devices/clients may fail to save directly from a temp path.
|
||||||
|
if (
|
||||||
|
msg.includes('no such file or directory') &&
|
||||||
|
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
|
||||||
|
) {
|
||||||
|
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
|
||||||
|
if (saved?.savedFilePath) {
|
||||||
|
filePath = saved.savedFilePath
|
||||||
|
}
|
||||||
|
await Taro.saveImageToPhotosAlbum({filePath})
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Taro.showToast({
|
||||||
|
title: '保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.errMsg?.includes('auth deny')) {
|
const errMsg = error?.errMsg || error?.message
|
||||||
Taro.showModal({
|
if (errMsg?.includes('cancel')) {
|
||||||
|
Taro.showToast({title: '已取消', icon: 'none'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAlbumAuthError(errMsg)) {
|
||||||
|
const modal = await Taro.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '需要您授权保存图片到相册',
|
content: '需要您授权保存图片到相册',
|
||||||
success: (res) => {
|
confirmText: '去设置'
|
||||||
if (res.confirm) {
|
|
||||||
Taro.openSetting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if (modal.confirm) {
|
||||||
|
await Taro.openSetting()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Taro.showToast({
|
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||||||
|
await Taro.showModal({
|
||||||
title: '保存失败',
|
title: '保存失败',
|
||||||
icon: 'error'
|
content: errMsg || '保存失败,请稍后重试',
|
||||||
|
showCancel: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
Taro.hideLoading()
|
||||||
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
//
|
//
|
||||||
// const inviteText = `🎉 邀请您加入我的团队!
|
// const inviteText = `🎉 邀请您加入我的团队!
|
||||||
//
|
//
|
||||||
// 扫描小程序码或搜索"九云售电云"小程序,即可享受优质商品和服务!
|
// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
|
||||||
//
|
//
|
||||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||||
// 🎁 新用户专享优惠等你来拿
|
// 🎁 新用户专享优惠等你来拿
|
||||||
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (!dealerUser) {
|
if (dealerLoading) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading/>
|
<Loading/>
|
||||||
@@ -171,6 +290,33 @@ const DealerQrcode: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||||
|
<Text className="text-gray-800 font-semibold">加载失败</Text>
|
||||||
|
<Text className="text-gray-500 text-sm mt-2">{error}</Text>
|
||||||
|
<Button className="mt-6" type="primary" onClick={refresh}>重试</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
|
||||||
|
if (!dealerUser) {
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||||
|
<Text className="text-gray-800 font-semibold">你还不是分销商</Text>
|
||||||
|
<Text className="text-gray-500 text-sm mt-2 text-center">申请成为分销商后即可生成分享码</Text>
|
||||||
|
<Button
|
||||||
|
className="mt-6"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => Taro.navigateTo({url: '/dealer/apply/add'})}
|
||||||
|
>
|
||||||
|
去申请
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="bg-gray-50 min-h-screen">
|
||||||
{/* 头部卡片 */}
|
{/* 头部卡片 */}
|
||||||
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}}></View>
|
}}></View>
|
||||||
|
|
||||||
<View className="relative z-10 flex flex-col">
|
<View className="relative z-10 flex flex-col">
|
||||||
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
<Text className="text-2xl font-bold mb-2 text-white">我的分享码</Text>
|
||||||
<Text className="text-white text-opacity-80">
|
<Text className="text-white text-opacity-80">
|
||||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
与好友“共享福利 一起省、一起赚”
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
{/* 小程序码展示区 */}
|
{/* 小程序码展示区 */}
|
||||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
{loading ? (
|
{codeLoading ? (
|
||||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||||
<Loading/>
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||||
@@ -239,10 +385,10 @@ const DealerQrcode: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||||
扫码加入我的团队
|
桂乐淘伙伴计划
|
||||||
</View>
|
</View>
|
||||||
<View className="text-sm text-gray-500 mb-4">
|
<View className="text-sm text-gray-500 mb-4">
|
||||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
自购省 | 分享赚 | 好友惠
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
||||||
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
|
|||||||
block
|
block
|
||||||
icon={<Download/>}
|
icon={<Download/>}
|
||||||
onClick={saveMiniProgramCode}
|
onClick={saveMiniProgramCode}
|
||||||
disabled={!miniProgramCodeUrl || loading}
|
disabled={!miniProgramCodeUrl || codeLoading || saving}
|
||||||
>
|
>
|
||||||
保存小程序码到相册
|
保存小程序码到相册
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
{/*<View className={'my-2 bg-white'}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="large"*/}
|
|
||||||
{/* block*/}
|
|
||||||
{/* icon={<Copy/>}*/}
|
|
||||||
{/* onClick={copyInviteInfo}*/}
|
|
||||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 复制邀请信息*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
{/*<View className={'my-2 bg-white'}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="large"*/}
|
|
||||||
{/* block*/}
|
|
||||||
{/* fill="outline"*/}
|
|
||||||
{/* icon={<Share/>}*/}
|
|
||||||
{/* onClick={shareMiniProgramCode}*/}
|
|
||||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 分享给好友*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 推广说明 */}
|
{/* 推广说明 */}
|
||||||
|
|||||||
3
src/dealer/referral/index.config.ts
Normal file
3
src/dealer/referral/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '推荐客户赚佣金'
|
||||||
|
})
|
||||||
132
src/dealer/referral/index.scss
Normal file
132
src/dealer/referral/index.scss
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
.referral-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
padding: 24px 16px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.stats-title {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.nut-input-text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-body {
|
||||||
|
.record-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
323
src/dealer/referral/index.tsx
Normal file
323
src/dealer/referral/index.tsx
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import {View, Text, ScrollView, Input, Button} from '@tarojs/components'
|
||||||
|
import {ConfigProvider, Field, Cell, CellGroup, Toast} from '@nutui/nutui-react-taro'
|
||||||
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import {addReferral, getMyReferrals, getMyStats} from '@/api/app/referral'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||||
|
0: {text: '待确认', color: '#ff9800'},
|
||||||
|
1: {text: '有效', color: '#4caf50'},
|
||||||
|
2: {text: '无效', color: '#9e9e9e'},
|
||||||
|
3: {text: '已结算', color: '#2196f3'}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReferralPage: React.FC = () => {
|
||||||
|
const {dealerUser} = useDealerUser()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
customerCompany: '',
|
||||||
|
requirement: '',
|
||||||
|
remarks: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
validCount: 0,
|
||||||
|
settledCount: 0,
|
||||||
|
pendingAmount: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 记录列表
|
||||||
|
const [records, setRecords] = useState<any[]>([])
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!dealerUser?.userId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// 获取统计
|
||||||
|
const statsRes = await getMyStats()
|
||||||
|
if (statsRes.data.code === 0) {
|
||||||
|
setStats(statsRes.data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
const listRes = await getMyReferrals({pageNum: 1, pageSize: 10})
|
||||||
|
if (listRes.data.code === 0) {
|
||||||
|
setRecords(listRes.data.data.list || [])
|
||||||
|
setHasMore(listRes.data.data.list?.length === 10)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载失败', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData()
|
||||||
|
}, [dealerUser])
|
||||||
|
|
||||||
|
// 输入处理
|
||||||
|
const handleInput = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({...prev, [field]: value}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!formData.customerName.trim()) {
|
||||||
|
Toast.text('请输入客户姓名')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!formData.customerPhone.trim()) {
|
||||||
|
Toast.text('请输入客户电话')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(formData.customerPhone)) {
|
||||||
|
Toast.text('请输入正确的手机号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交报备
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true)
|
||||||
|
const res = await addReferral(formData)
|
||||||
|
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
Toast.text('报备成功!')
|
||||||
|
// 清空表单
|
||||||
|
setFormData({
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
customerCompany: '',
|
||||||
|
requirement: '',
|
||||||
|
remarks: ''
|
||||||
|
})
|
||||||
|
// 刷新数据
|
||||||
|
loadData()
|
||||||
|
} else {
|
||||||
|
Toast.text(res.data.message || '报备失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Toast.text(error.message || '报备失败')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拨打电话
|
||||||
|
const handleCall = (phone: string) => {
|
||||||
|
if (phone) {
|
||||||
|
Taro.makePhoneCall({phoneNumber: phone})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (!hasMore || loading) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextPage = page + 1
|
||||||
|
const res = await getMyReferrals({pageNum: nextPage, pageSize: 10})
|
||||||
|
if (res.data.code === 0 && res.data.data.list) {
|
||||||
|
setRecords(prev => [...prev, ...res.data.data.list])
|
||||||
|
setPage(nextPage)
|
||||||
|
setHasMore(res.data.data.list.length === 10)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载更多失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="referral-page">
|
||||||
|
{/* 头部统计 */}
|
||||||
|
<View className="stats-section" style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'}}>
|
||||||
|
<View className="stats-title">
|
||||||
|
<Text className="text-white text-lg font-bold">我的推荐奖励</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stats-grid">
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">{stats.totalCount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>总推荐</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">{stats.pendingCount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>待确认</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">{stats.validCount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>有效</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">¥{stats.pendingAmount.toFixed(2)}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>待结算</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 报备表单 */}
|
||||||
|
<View className="form-section">
|
||||||
|
<View className="section-title">
|
||||||
|
<Text className="font-bold text-gray-800">推荐新客户</Text>
|
||||||
|
</View>
|
||||||
|
<View className="form-card">
|
||||||
|
<CellGroup>
|
||||||
|
<Cell title="客户姓名">
|
||||||
|
<Input
|
||||||
|
className="nut-input-text"
|
||||||
|
placeholder="请输入客户姓名"
|
||||||
|
value={formData.customerName}
|
||||||
|
onInput={(e) => handleInput('customerName', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell title="联系电话">
|
||||||
|
<Input
|
||||||
|
className="nut-input-text"
|
||||||
|
type="number"
|
||||||
|
maxlength={11}
|
||||||
|
placeholder="请输入客户电话"
|
||||||
|
value={formData.customerPhone}
|
||||||
|
onInput={(e) => handleInput('customerPhone', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell title="公司名称">
|
||||||
|
<Input
|
||||||
|
className="nut-input-text"
|
||||||
|
placeholder="请输入公司名称(选填)"
|
||||||
|
value={formData.customerCompany}
|
||||||
|
onInput={(e) => handleInput('customerCompany', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell title="需求描述">
|
||||||
|
<Input
|
||||||
|
className="nut-input-text"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请描述客户需求(选填)"
|
||||||
|
value={formData.requirement}
|
||||||
|
onInput={(e) => handleInput('requirement', e.detail.value)}
|
||||||
|
style={{height: '80px', textAlign: 'left'}}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</CellGroup>
|
||||||
|
|
||||||
|
<View className="submit-btn">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={submitting}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
提交报备
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="tips">
|
||||||
|
<Text className="text-gray-500 text-sm">
|
||||||
|
报备成功后,业务员会尽快联系您的客户。成交后您将获得相应佣金奖励。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 推荐记录 */}
|
||||||
|
<View className="records-section">
|
||||||
|
<View className="section-title">
|
||||||
|
<Text className="font-bold text-gray-800">我的推荐记录</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{records.length === 0 ? (
|
||||||
|
<View className="empty-state">
|
||||||
|
<Text className="text-gray-400">暂无推荐记录</Text>
|
||||||
|
<Text className="text-gray-400 text-sm">快去推荐客户吧</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
scrollY
|
||||||
|
onScrollToLower={loadMore}
|
||||||
|
style={{height: '300px'}}
|
||||||
|
>
|
||||||
|
{records.map((item) => {
|
||||||
|
const statusInfo = STATUS_MAP[item.referralStatus] || STATUS_MAP[0]
|
||||||
|
return (
|
||||||
|
<View key={item.referralId} className="record-card">
|
||||||
|
<View className="record-header">
|
||||||
|
<View className="customer-info">
|
||||||
|
<Text className="font-bold text-gray-800">{item.customerName}</Text>
|
||||||
|
<Text
|
||||||
|
className="text-blue-500 text-sm ml-2"
|
||||||
|
onClick={() => handleCall(item.customerPhone)}
|
||||||
|
>
|
||||||
|
{item.customerPhone}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className="status-tag"
|
||||||
|
style={{backgroundColor: statusInfo.color + '20', color: statusInfo.color}}
|
||||||
|
>
|
||||||
|
{statusInfo.text}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="record-body">
|
||||||
|
<View className="record-row">
|
||||||
|
<Text className="text-gray-500 text-sm">推荐时间</Text>
|
||||||
|
<Text className="text-gray-700 text-sm">{item.createTime}</Text>
|
||||||
|
</View>
|
||||||
|
{item.referralFee > 0 && (
|
||||||
|
<View className="record-row">
|
||||||
|
<Text className="text-gray-500 text-sm">奖励金额</Text>
|
||||||
|
<Text className="text-red-500 font-bold text-sm">
|
||||||
|
¥{item.referralFee.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{item.leadStatusText && (
|
||||||
|
<View className="record-row">
|
||||||
|
<Text className="text-gray-500 text-sm">客户状态</Text>
|
||||||
|
<Text className="text-gray-700 text-sm">{item.leadStatusText}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!hasMore && records.length > 0 && (
|
||||||
|
<View className="no-more">
|
||||||
|
<Text className="text-gray-400 text-sm">没有更多了</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部安全区 */}
|
||||||
|
<View className="h-20"></View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReferralPage
|
||||||
@@ -325,7 +325,7 @@ const DealerTeam: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
{/* 显示手机号(仅本级可见) */}
|
{/* 显示手机号(仅本级可见) */}
|
||||||
{showPhone && member.phone && (
|
{showPhone && member.phone && (
|
||||||
<Text className="text-sm text-gray-500" onClick={(e) => {
|
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
makePhoneCall(member.phone || '');
|
makePhoneCall(member.phone || '');
|
||||||
}}>
|
}}>
|
||||||
@@ -334,14 +334,9 @@ const DealerTeam: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Space>
|
<Text className="text-xs text-gray-500">
|
||||||
<Text>
|
加入时间:{member.joinTime}
|
||||||
<Text className="text-xs text-gray-500">UID:{member.userId}</Text>
|
</Text>
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-gray-500">
|
|
||||||
加入时间:{member.joinTime}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -46,9 +46,17 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
|||||||
setDealerUser(dealer)
|
setDealerUser(dealer)
|
||||||
} else {
|
} else {
|
||||||
setDealerUser(null)
|
setDealerUser(null)
|
||||||
|
// 没有经销商记录,跳转到申请加入页面
|
||||||
|
Taro.redirectTo({ url: '/dealer/apply/add' })
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||||
|
// 如果错误消息是"操作成功"(接口返回成功但无数据),也跳转到申请页面
|
||||||
|
if (errorMessage === '操作成功' || errorMessage === '查询成功') {
|
||||||
|
setDealerUser(null)
|
||||||
|
Taro.redirectTo({ url: '/dealer/apply/add' })
|
||||||
|
return
|
||||||
|
}
|
||||||
setError(errorMessage)
|
setError(errorMessage)
|
||||||
setDealerUser(null)
|
setDealerUser(null)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const highlightItems = [
|
|||||||
{
|
{
|
||||||
icon: <Star size={22} color="#ffffff" />,
|
icon: <Star size={22} color="#ffffff" />,
|
||||||
title: '真实口碑',
|
title: '真实口碑',
|
||||||
description: '5000+家庭选择,98%满意度,支持老房换窗与整屋升级。'
|
description: '10万+家庭选择,98%满意度,支持老房换窗与整屋升级。'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -220,15 +220,15 @@ const BrochurePage: React.FC = () => {
|
|||||||
|
|
||||||
<View className="brochure-page__stats">
|
<View className="brochure-page__stats">
|
||||||
<View className="brochure-page__stat">
|
<View className="brochure-page__stat">
|
||||||
<Text className="brochure-page__stat-value">10年</Text>
|
<Text className="brochure-page__stat-value">20年</Text>
|
||||||
<Text className="brochure-page__stat-label">质保承诺</Text>
|
<Text className="brochure-page__stat-label">质保承诺(整窗)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="brochure-page__stat">
|
<View className="brochure-page__stat">
|
||||||
<Text className="brochure-page__stat-value">15年</Text>
|
<Text className="brochure-page__stat-value">15年</Text>
|
||||||
<Text className="brochure-page__stat-label">安装经验</Text>
|
<Text className="brochure-page__stat-label">安装经验</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="brochure-page__stat">
|
<View className="brochure-page__stat">
|
||||||
<Text className="brochure-page__stat-value">5000+</Text>
|
<Text className="brochure-page__stat-value">10万+</Text>
|
||||||
<Text className="brochure-page__stat-label">家庭选择</Text>
|
<Text className="brochure-page__stat-label">家庭选择</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
.bestsellers-list {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bestsellers-item {
|
||||||
|
display: flex;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.985);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 左侧图片 ═══ */
|
||||||
|
&__img-wrap {
|
||||||
|
width: 140px;
|
||||||
|
min-height: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 右侧信息 ═══ */
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 12px 10px 14px;
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 底部操作栏 ═══ */
|
||||||
|
&__bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__share-btn {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buy-btn {
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: linear-gradient(135deg, #2563eb, #3b82f6);
|
||||||
|
border-radius: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 空状态 ═══ */
|
||||||
|
.bestsellers-empty {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,20 +18,15 @@ const BestSellers = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理分享点击
|
|
||||||
const handleShare = (item: ShopGoods) => {
|
const handleShare = (item: ShopGoods) => {
|
||||||
setGoods(item);
|
setGoods(item);
|
||||||
|
|
||||||
// 显示分享选项菜单
|
|
||||||
Taro.showActionSheet({
|
Taro.showActionSheet({
|
||||||
itemList: ['分享给好友', '分享到朋友圈'],
|
itemList: ['分享给好友', '分享到朋友圈'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.tapIndex === 0) {
|
if (res.tapIndex === 0) {
|
||||||
// 分享给好友 - 触发转发
|
|
||||||
Taro.showShareMenu({
|
Taro.showShareMenu({
|
||||||
withShareTicket: true,
|
withShareTicket: true,
|
||||||
success: () => {
|
success: () => {
|
||||||
// 提示用户点击右上角分享
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请点击右上角分享给好友',
|
title: '请点击右上角分享给好友',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -40,7 +35,6 @@ const BestSellers = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (res.tapIndex === 1) {
|
} else if (res.tapIndex === 1) {
|
||||||
// 分享到朋友圈
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请点击右上角分享到朋友圈',
|
title: '请点击右上角分享到朋友圈',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -58,32 +52,22 @@ const BestSellers = () => {
|
|||||||
reload()
|
reload()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 分享给好友
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: goods?.name || '精选商品',
|
title: goods?.name || '精选商品',
|
||||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
||||||
imageUrl: goods?.image, // 分享图片
|
imageUrl: goods?.image,
|
||||||
success: function (res: any) {
|
success: function (res: any) {
|
||||||
console.log('分享成功', res);
|
console.log('分享成功', res);
|
||||||
Taro.showToast({
|
Taro.showToast({ title: '分享成功', icon: 'success', duration: 2000 });
|
||||||
title: '分享成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
fail: function (res: any) {
|
fail: function (res: any) {
|
||||||
console.log('分享失败', res);
|
console.log('分享失败', res);
|
||||||
Taro.showToast({
|
Taro.showToast({ title: '分享失败', icon: 'none', duration: 2000 });
|
||||||
title: '分享失败',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 分享到朋友圈
|
|
||||||
useShareTimeline(() => {
|
useShareTimeline(() => {
|
||||||
return {
|
return {
|
||||||
title: `${goods?.name || '精选商品'} - 南南佐顿门窗`,
|
title: `${goods?.name || '精选商品'} - 南南佐顿门窗`,
|
||||||
@@ -93,49 +77,62 @@ const BestSellers = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View className="bestsellers-list">
|
||||||
<View className={'py-1 px-4'}>
|
{list?.map((item, index) => (
|
||||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
<View
|
||||||
{list?.map((item, index) => {
|
key={index}
|
||||||
return (
|
className="bestsellers-item"
|
||||||
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}
|
||||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
|
>
|
||||||
radius="10px 10px 0 0" height="180"
|
{/* 左侧商品图 */}
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
<View className="bestsellers-item__img-wrap">
|
||||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
<Image
|
||||||
<View>
|
src={item.image}
|
||||||
<View className={'car-no text-sm'}>{item.name}</View>
|
mode="aspectFill"
|
||||||
<View className={'flex justify-between text-xs py-1'}>
|
lazyLoad={false}
|
||||||
<Text className={'text-orange-500'}>{item.comments}</Text>
|
className="bestsellers-item__img"
|
||||||
<Text className={'text-gray-400'}>已售 {item.sales}</Text>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View className={'flex justify-between items-center py-2'}>
|
|
||||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
{/* 右侧信息区 */}
|
||||||
<Text className={'text-xs'}>¥</Text>
|
<View className="bestsellers-item__info">
|
||||||
<Text className={'font-bold text-2xl'}>{item.price}</Text>
|
<Text className="text-base font-semibold text-gray-900 leading-snug">{item.name}</Text>
|
||||||
</View>
|
|
||||||
<View className={'buy-btn'}>
|
<View className="bestsellers-item__tags">
|
||||||
<View className={'cart-icon flex items-center'}>
|
<Text className="text-sm text-amber-500 bg-amber-50 px-2 rounded">{item.comments}</Text>
|
||||||
<View
|
<Text className="text-sm text-gray-400">已售 {item.sales}</Text>
|
||||||
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
|
</View>
|
||||||
onClick={() => handleShare(item)}
|
|
||||||
>
|
<View className="bestsellers-item__bottom">
|
||||||
<Share size={20}/>
|
{/* 价格 */}
|
||||||
</View>
|
<View className="bestsellers-item__price-wrap">
|
||||||
</View>
|
<Text className="text-15 font-semibold text-red-500">¥</Text>
|
||||||
<Text className={'text-white pl-4 pr-5'}
|
<Text className="text-28 font-extrabold text-red-500 leading-none">{item.price}</Text>
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
</View>
|
||||||
</Text>
|
|
||||||
</View>
|
{/* 操作按钮 */}
|
||||||
</View>
|
<View className="bestsellers-item__actions">
|
||||||
</View>
|
<View
|
||||||
|
className="bestsellers-item__share-btn"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleShare(item) }}
|
||||||
|
>
|
||||||
|
<Share size={18} color="#9ca3af" />
|
||||||
|
</View>
|
||||||
|
<View className="bestsellers-item__buy-btn">
|
||||||
|
<Text className="text-base font-semibold text-white">购买</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
</View>
|
||||||
})}
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
))}
|
||||||
</>
|
|
||||||
|
{list.length === 0 && (
|
||||||
|
<View className="bestsellers-empty">
|
||||||
|
<Text className="text-base text-gray-400">暂无热销商品</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default BestSellers
|
export default BestSellers
|
||||||
|
|||||||
110
src/pages/index/CatalogShowcase.scss
Normal file
110
src/pages/index/CatalogShowcase.scss
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
.catalog-card {
|
||||||
|
position: relative;
|
||||||
|
margin: 24px 16px 8px;
|
||||||
|
padding: 24px 20px;
|
||||||
|
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #3b82f6 100%);
|
||||||
|
border-radius: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(37, 99, 235, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 装饰圆 ═══ */
|
||||||
|
&__deco {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&--1 {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
top: -40px;
|
||||||
|
right: -30px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--2 {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
bottom: -20px;
|
||||||
|
left: 20%;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 内容区 ═══ */
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
align-self: flex-start;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 右侧书本图形 ═══ */
|
||||||
|
&__right {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__book {
|
||||||
|
width: 142px;
|
||||||
|
height: 180px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 4px 10px 10px 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__book-spine {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__book-pages {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__book-page {
|
||||||
|
width: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&:nth-child(1) { height: 28px; }
|
||||||
|
&:nth-child(2) { height: 40px; }
|
||||||
|
&:nth-child(3) { height: 32px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/pages/index/CatalogShowcase.tsx
Normal file
70
src/pages/index/CatalogShowcase.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { ArrowRight } from '@nutui/icons-react-taro'
|
||||||
|
import './CatalogShowcase.scss'
|
||||||
|
|
||||||
|
const CATALOG_URL = 'https://book.yunzhan365.com/mdfy/tjcs/mobile/index.html'
|
||||||
|
|
||||||
|
function CatalogShowcase() {
|
||||||
|
const handleViewCatalog = () => {
|
||||||
|
Taro.setClipboardData({
|
||||||
|
data: CATALOG_URL,
|
||||||
|
success: () => {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '链接已复制',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '链接已复制到剪贴板,请前往浏览器打开查看品牌画册',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了'
|
||||||
|
})
|
||||||
|
}, 2100)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
Taro.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="catalog-card" onClick={handleViewCatalog}>
|
||||||
|
{/* 装饰性背景元素 */}
|
||||||
|
<View className="catalog-card__deco catalog-card__deco--1" />
|
||||||
|
<View className="catalog-card__deco catalog-card__deco--2" />
|
||||||
|
|
||||||
|
<View className="catalog-card__content">
|
||||||
|
<View className="catalog-card__left">
|
||||||
|
<Text className="text-xs text-gray-400 tracking-widest">BRAND CATALOG</Text>
|
||||||
|
<Text className="text-2xl font-bold text-white mb-1">品牌画册</Text>
|
||||||
|
<Text className="text-xs text-gray-300 leading-relaxed" style={{
|
||||||
|
width: '90%'
|
||||||
|
}}>
|
||||||
|
了解南南佐顿门窗的完整产品线与定制方案
|
||||||
|
</Text>
|
||||||
|
<View className="catalog-card__cta">
|
||||||
|
<Text className="text-base font-semibold text-white px-4">点击查看画册</Text>
|
||||||
|
<ArrowRight size={16} color="#ffffff" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="catalog-card__right">
|
||||||
|
<View className="catalog-card__book">
|
||||||
|
<View className="catalog-card__book-spine" />
|
||||||
|
<View className="catalog-card__book-pages">
|
||||||
|
<View className="catalog-card__book-page" />
|
||||||
|
<View className="catalog-card__book-page" />
|
||||||
|
<View className="catalog-card__book-page" />
|
||||||
|
</View>
|
||||||
|
<Text className="text-lg font-bold text-gray-300 px-1">2026</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CatalogShowcase
|
||||||
@@ -1,143 +1,68 @@
|
|||||||
.contact-section {
|
.contact-section {
|
||||||
background: #ffffff;
|
margin-top: 0;
|
||||||
padding: 24px 16px;
|
padding: 32px 20px 24px;
|
||||||
margin: 16px 0;
|
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||||
border-radius: 16px;
|
border-radius: 24px 24px 0 0;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
color: #ffffff;
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
/* ═══ 引言区 ═══ */
|
||||||
text-align: center;
|
&__intro {
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
&-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #64748b;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
/* ═══ 操作按钮区 ═══ */
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px 18px;
|
||||||
background: #f8fafc;
|
border-radius: 14px;
|
||||||
border-radius: 12px;
|
transition: opacity 0.2s;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: #f1f5f9;
|
opacity: 0.8;
|
||||||
transform: scale(0.98);
|
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__action--primary {
|
||||||
width: 40px;
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
height: 40px;
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.35);
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&--blue {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--green {
|
|
||||||
background: rgba(16, 185, 129, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--orange {
|
|
||||||
background: rgba(245, 158, 11, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--cyan {
|
|
||||||
background: rgba(6, 182, 212, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__action--secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
margin-left: 14px;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
&__item-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #64748b;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item-value {
|
|
||||||
display: block;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══ 底部信息 ═══ */
|
||||||
&__footer {
|
&__footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
|
||||||
|
|
||||||
&__footer-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
line-height: 1.6;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式适配
|
|
||||||
@media (max-width: 375px) {
|
|
||||||
.contact-section {
|
|
||||||
padding: 20px 12px;
|
|
||||||
margin: 12px 0;
|
|
||||||
|
|
||||||
&__grid {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
Phone,
|
Phone,
|
||||||
Message
|
Message
|
||||||
@@ -8,59 +8,6 @@ import {
|
|||||||
import './ContactSection.scss'
|
import './ContactSection.scss'
|
||||||
|
|
||||||
const ContactSection: React.FC = () => {
|
const ContactSection: React.FC = () => {
|
||||||
const contactItems = [
|
|
||||||
{
|
|
||||||
icon: <Phone size={20} color="#3b82f6" />,
|
|
||||||
title: '联系电话',
|
|
||||||
value: '13367810229',
|
|
||||||
action: 'call',
|
|
||||||
colorClass: 'contact-item--blue'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Message size={20} color="#10b981" />,
|
|
||||||
title: '在线咨询',
|
|
||||||
value: '点击立即咨询',
|
|
||||||
action: 'chat',
|
|
||||||
colorClass: 'contact-item--green'
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// icon: <Location size={20} color="#f59e0b" />,
|
|
||||||
// title: '门店地址',
|
|
||||||
// value: '上海市浦东新区XX路888号',
|
|
||||||
// action: 'map',
|
|
||||||
// colorClass: 'contact-item--orange'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// icon: <Share size={20} color="#06b6d4" />,
|
|
||||||
// title: '关注我们',
|
|
||||||
// value: '南南佐顿门窗',
|
|
||||||
// action: 'wechat',
|
|
||||||
// colorClass: 'contact-item--cyan'
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleAction = (action: string) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'call':
|
|
||||||
handleCallPhone()
|
|
||||||
break
|
|
||||||
case 'chat':
|
|
||||||
handleOnlineChat()
|
|
||||||
break
|
|
||||||
case 'map':
|
|
||||||
console.log('查看地图')
|
|
||||||
// TODO: 跳转到地图导航
|
|
||||||
break
|
|
||||||
case 'wechat':
|
|
||||||
console.log('关注公众号')
|
|
||||||
// TODO: 显示公众号二维码
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拨打电话功能
|
|
||||||
const handleCallPhone = () => {
|
const handleCallPhone = () => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '拨打电话',
|
title: '拨打电话',
|
||||||
@@ -76,10 +23,7 @@ const ContactSection: React.FC = () => {
|
|||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('拨打电话失败:', err)
|
console.error('拨打电话失败:', err)
|
||||||
Taro.showToast({
|
Taro.showToast({title: '拨打电话失败', icon: 'none'})
|
||||||
title: '拨打电话失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -87,85 +31,46 @@ const ContactSection: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在线咨询功能
|
|
||||||
const handleOnlineChat = () => {
|
|
||||||
// 检查是否已登录
|
|
||||||
Taro.getStorage({
|
|
||||||
key: 'userInfo',
|
|
||||||
success: (_) => {
|
|
||||||
// 用户已登录,跳转到聊天页面
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/pages/user/chat/conversation/index',
|
|
||||||
success: () => {
|
|
||||||
console.log('跳转到聊天页面成功')
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('跳转失败:', err)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '跳转失败,请稍后重试',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
// 用户未登录,提示登录
|
|
||||||
Taro.showModal({
|
|
||||||
title: '登录提示',
|
|
||||||
content: '需要登录后才能在线咨询,是否立即登录?',
|
|
||||||
confirmText: '去登录',
|
|
||||||
cancelText: '稍后再说',
|
|
||||||
success: (loginRes) => {
|
|
||||||
if (loginRes.confirm) {
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/pages/passport/login',
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('跳转到登录页失败:', err)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '跳转失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="contact-section px-4">
|
<View className={'px-2'}>
|
||||||
<View className="contact-section__title flex flex-col">
|
<View className="contact-section">
|
||||||
<Text className="text-lg font-semibold text-gray-800">联系我们</Text>
|
{/* 顶部引言区 */}
|
||||||
<Text className="text-sm text-gray-500">随时为您提供专业服务</Text>
|
<View className="contact-section__intro">
|
||||||
</View>
|
<Text className="block text-2xl font-bold text-white mb-1">联系我们</Text>
|
||||||
|
<Text className="block text-base text-white">
|
||||||
|
随时为您提供专业的门窗定制咨询服务
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View className="contact-section__grid">
|
{/* 操作按钮 — 全宽醒目 */}
|
||||||
{contactItems.map((item, index) => (
|
<View className="contact-section__actions">
|
||||||
<View
|
<View className="contact-section__action contact-section__action--primary" onClick={handleCallPhone}>
|
||||||
key={index}
|
<Phone size={22} color="#ffffff"/>
|
||||||
className="contact-section__item"
|
<View className="contact-section__action-text">
|
||||||
onClick={() => handleAction(item.action)}
|
<Text className="text-17 font-semibold text-white">电话咨询</Text>
|
||||||
>
|
<Text className="text-sm text-white">13367810229</Text>
|
||||||
<View className={`contact-section__icon ${item.colorClass}`}>
|
|
||||||
{item.icon}
|
|
||||||
</View>
|
|
||||||
<View className="contact-section__content flex flex-col">
|
|
||||||
<Text className="text-xs text-gray-500">{item.title}</Text>
|
|
||||||
<Text className="text-sm font-semibold text-gray-800 truncate">{item.value}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="contact-section__footer">
|
<button className="contact-section__action contact-section__action--secondary" open-type="contact">
|
||||||
<Text className="text-xs text-gray-500">
|
<Message size={22} color="#3b82f6"/>
|
||||||
营业时间:周一至周日 8:30-18:00
|
<View className="contact-section__action-text">
|
||||||
</Text>
|
<Text className="text-17 font-semibold text-white">在线咨询</Text>
|
||||||
<Text className="text-xs text-gray-500">
|
<Text className="text-sm text-white">点击立即咨询</Text>
|
||||||
节假日照常营业,欢迎随时咨询
|
</View>
|
||||||
</Text>
|
</button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部信息 */}
|
||||||
|
<View className="contact-section__footer">
|
||||||
|
<Text className="block text-sm text-white mb-1">
|
||||||
|
营业时间:周一至周日 8:30 - 18:00
|
||||||
|
</Text>
|
||||||
|
<Text className="block text-xs text-white">
|
||||||
|
节假日照常营业,欢迎随时咨询
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,32 +1,44 @@
|
|||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {Grid} from '@nutui/nutui-react-taro'
|
import {Grid} from '@nutui/nutui-react-taro'
|
||||||
import {Avatar} from '@nutui/nutui-react-taro'
|
import {Avatar} from '@nutui/nutui-react-taro'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {listCmsNavigation} from "@/api/cms/cmsNavigation";
|
|
||||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
|
||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 硬编码首页菜单数据
|
||||||
|
const menuList: MenuItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '推荐客户',
|
||||||
|
icon: 'https://oss.wsdns.cn/20260330/5f54527123864193b0a2078f812b117f.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
|
||||||
|
path: '/dealer/customer/add'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '客户列表',
|
||||||
|
icon: 'https://oss.wsdns.cn/20260330/24485bb4684d4ae2a64cc7dd49ec4d3d.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
|
||||||
|
path: '/dealer/customer/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '邀请好友',
|
||||||
|
icon: 'https://oss.wsdns.cn/20260330/64cac0d5cbe645af8a574a257cd00302.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
|
||||||
|
path: '/dealer/team/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '个人中心',
|
||||||
|
icon: 'https://oss.wsdns.cn/20260330/6b198116f2d94b1e942c55ebe2f73728.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
|
||||||
|
path: '/pages/user/user'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const MyGrid = () => {
|
const MyGrid = () => {
|
||||||
const [list, setList] = useState<CmsNavigation[]>([])
|
|
||||||
const reload = async () => {
|
|
||||||
// 读取首页菜单
|
|
||||||
const home = await listCmsNavigation({model: 'index'});
|
|
||||||
const homeId = home[0].navigationId;
|
|
||||||
if(homeId){
|
|
||||||
const menu = await listCmsNavigation({home: 0, parentId: homeId, hide: 0})
|
|
||||||
setList(menu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload().then()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (list.length == 0) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'p-0'}>
|
<View className={'p-0'}>
|
||||||
@@ -36,8 +48,8 @@ const MyGrid = () => {
|
|||||||
'--nutui-grid-border-color': 'transparent',
|
'--nutui-grid-border-color': 'transparent',
|
||||||
}}>
|
}}>
|
||||||
{
|
{
|
||||||
list.map((item) => (
|
menuList.map((item) => (
|
||||||
<Grid.Item key={item.navigationId} onClick={() => navTo(`${item.path}`,true)}>
|
<Grid.Item key={item.id} onClick={() => navTo(`${item.path}`,true)}>
|
||||||
<Avatar src={item.icon} className={'mb-1'} shape="square" style={{
|
<Avatar src={item.icon} className={'mb-1'} shape="square" style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
}}/>
|
}}/>
|
||||||
|
|||||||
@@ -1,101 +1,54 @@
|
|||||||
.trust-section {
|
.trust-section {
|
||||||
background: #ffffff;
|
padding: 32px 0 24px;
|
||||||
padding: 24px 16px;
|
background: #f5f5f7;
|
||||||
margin: 16px 0;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
&__header {
|
||||||
text-align: center;
|
padding: 0 20px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
&-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #64748b;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__grid {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
/* 横向滑动容器 */
|
||||||
width: 56px;
|
&__scroll {
|
||||||
height: 56px;
|
width: 100%;
|
||||||
border-radius: 50%;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__track {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 20px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单张渐变卡片 */
|
||||||
|
&__card {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 200px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 14px;
|
||||||
|
|
||||||
&--blue {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--green {
|
|
||||||
background: rgba(16, 185, 129, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--orange {
|
|
||||||
background: rgba(245, 158, 11, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item-title {
|
&__card-highlight-row {
|
||||||
font-size: 15px;
|
display: flex;
|
||||||
font-weight: 600;
|
align-items: baseline;
|
||||||
color: #1e293b;
|
gap: 6px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 6px;
|
||||||
}
|
|
||||||
|
|
||||||
&__item-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式适配
|
|
||||||
@media (max-width: 375px) {
|
|
||||||
.trust-section {
|
|
||||||
padding: 20px 12px;
|
|
||||||
margin: 12px 0;
|
|
||||||
|
|
||||||
&__grid {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
People,
|
People,
|
||||||
@@ -10,43 +10,60 @@ import './TrustSection.scss'
|
|||||||
const TrustSection: React.FC = () => {
|
const TrustSection: React.FC = () => {
|
||||||
const trustItems = [
|
const trustItems = [
|
||||||
{
|
{
|
||||||
icon: <ShieldCheck size={24} color="#3b82f6" />,
|
icon: <ShieldCheck size={28} color="#ffffff" />,
|
||||||
title: '品质保障',
|
title: '品质保障',
|
||||||
description: '10年质保,德国进口五金',
|
highlight: '20年',
|
||||||
colorClass: '--blue'
|
description: '质保承诺(整窗)',
|
||||||
|
subDescription: '德国进口五金配件',
|
||||||
|
bg: 'linear-gradient(135deg, #3b82f6, #2563eb)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <People size={24} color="#10b981" />,
|
icon: <People size={28} color="#ffffff" />,
|
||||||
title: '专业团队',
|
title: '专业团队',
|
||||||
description: '15年安装经验,持证上岗',
|
highlight: '15年',
|
||||||
colorClass: '--green'
|
description: '安装经验',
|
||||||
|
subDescription: '全员持证上岗',
|
||||||
|
bg: 'linear-gradient(135deg, #10b981, #059669)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Star size={24} color="#f59e0b" />,
|
icon: <Star size={28} color="#ffffff" />,
|
||||||
title: '客户好评',
|
title: '客户好评',
|
||||||
description: '5000+家庭选择,98%满意度',
|
highlight: '98%',
|
||||||
colorClass: '--orange'
|
description: '满意度',
|
||||||
|
subDescription: '10万+家庭信赖之选',
|
||||||
|
bg: 'linear-gradient(135deg, #f59e0b, #d97706)',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="trust-section">
|
<View className="trust-section">
|
||||||
<View className="trust-section__title flex flex-col">
|
<View className="trust-section__header">
|
||||||
<Text className="text-lg font-semibold text-gray-800">南南佐顿 · 品质之选</Text>
|
<Text className="text-xs font-semibold text-gray-400 tracking-wider uppercase mb-1">WHY CHOOSE US</Text>
|
||||||
<Text className="text-sm text-gray-500">专业门窗定制安装服务</Text>
|
<Text className="text-xl font-bold text-gray-900">为什么选择南南佐顿</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="trust-section__grid">
|
<ScrollView
|
||||||
{trustItems.map((item, index) => (
|
className="trust-section__scroll"
|
||||||
<View key={index} className="trust-section__item">
|
scrollX
|
||||||
<View className={`trust-section__icon trust-section__icon${item.colorClass}`}>
|
enhanced
|
||||||
{item.icon}
|
showScrollbar={false}
|
||||||
|
>
|
||||||
|
<View className="trust-section__track">
|
||||||
|
{trustItems.map((item, index) => (
|
||||||
|
<View key={index} className="trust-section__card" style={{background: item.bg}}>
|
||||||
|
<View className="trust-section__card-icon">
|
||||||
|
{item.icon}
|
||||||
|
</View>
|
||||||
|
<Text className="text-17 font-semibold text-white mb-2">{item.title}</Text>
|
||||||
|
<View className="trust-section__card-highlight-row">
|
||||||
|
<Text className="text-4xl font-extrabold leading-none text-white">{item.highlight}</Text>
|
||||||
|
<Text className="text-base text-white">{item.description}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-xs text-white mt-1">{item.subDescription}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-base font-semibold text-gray-800">{item.title}</Text>
|
))}
|
||||||
<Text className="text-xs text-gray-500">{item.description}</Text>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,132 @@
|
|||||||
page {
|
page {
|
||||||
//background: url('https://oss.wsdns.cn/20250621/33ca4ca532e647bc918a59d01f5d88a9.jpg?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90') no-repeat top center;
|
background: #f5f5f7;
|
||||||
//background-size: 100%;
|
|
||||||
background: linear-gradient(to bottom, #3b82f6, #ffffff);
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部内容区域统一间距
|
/* ═══ 首页容器 ═══ */
|
||||||
.bottom-content-section {
|
.home-page {
|
||||||
margin: 16px 0;
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
&:first-child {
|
}
|
||||||
margin-top: 0;
|
|
||||||
|
/* ═══ Hero 顶部视觉区 ═══ */
|
||||||
|
|
||||||
|
.hero-banner-wrap {
|
||||||
|
padding: 0 16px;
|
||||||
|
padding-top: 6px;
|
||||||
|
|
||||||
|
/* 让Banner Swiper有圆角 */
|
||||||
|
.nut-swiper {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
.nut-swiper-item {
|
||||||
margin-bottom: 32px;
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy-btn{
|
/* Hero下方的Grid菜单区 */
|
||||||
height: 70px;
|
.hero-grid {
|
||||||
background: linear-gradient(to bottom, #2563eb, #3b82f6);
|
margin: 0 16px;
|
||||||
|
margin-top: -12px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
/* 让NutUI Grid的白色背景更好融入 */
|
||||||
|
.nut-grid {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nut-grid-item__content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 公告条 — 自定义样式替代NutUI NoticeBar ═══ */
|
||||||
|
.notice-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 0 16px 8px;
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f59e0b;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: #92400e;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 热销推荐 ═══ */
|
||||||
|
.section-hot {
|
||||||
|
padding: 20px 0 4px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 通用section间距重置(去掉TrustSection/ContactSection自带的margin) ═══ */
|
||||||
|
.home-page .trust-section,
|
||||||
|
.home-page .contact-section {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 底部购买按钮 ═══ */
|
||||||
|
.buy-btn {
|
||||||
|
height: 64px;
|
||||||
|
background: linear-gradient(135deg, #2563eb, #3b82f6);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
.cart-icon{
|
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.35);
|
||||||
background: linear-gradient(to bottom, #60a5fa, #3b82f6);
|
|
||||||
|
.cart-icon {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 100px 0 0 100px;
|
border-radius: 100px 0 0 100px;
|
||||||
height: 70px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import BestSellers from './BestSellers';
|
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {getShopInfo} from "@/api/layout";
|
import {getShopInfo} from "@/api/layout";
|
||||||
import {NoticeBar} from '@nutui/nutui-react-taro'
|
|
||||||
import {View} from '@tarojs/components'
|
import {View} from '@tarojs/components'
|
||||||
import Menu from "./Menu";
|
|
||||||
import Banner from "./Banner";
|
|
||||||
import './index.scss'
|
|
||||||
import Grid from "@/pages/index/Grid";
|
import Grid from "@/pages/index/Grid";
|
||||||
|
import Banner from "./Banner";
|
||||||
import PopUpAd from "@/pages/index/PopUpAd";
|
import PopUpAd from "@/pages/index/PopUpAd";
|
||||||
import TrustSection from "./TrustSection";
|
import TrustSection from "./TrustSection";
|
||||||
import CaseShowcase from "./CaseShowcase";
|
|
||||||
import ContactSection from "./ContactSection";
|
import ContactSection from "./ContactSection";
|
||||||
import BrochureEntry from "./BrochureEntry";
|
import CatalogShowcase from "./CatalogShowcase";
|
||||||
import {configWebsiteField} from "@/api/cms/cmsWebsiteField";
|
import {configWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||||
import type {Config} from "@/api/cms/cmsWebsiteField/model";
|
import type {Config} from "@/api/cms/cmsWebsiteField/model";
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
// 吸顶状态
|
|
||||||
const [config, setConfig] = useState<Config>()
|
const [config, setConfig] = useState<Config>()
|
||||||
|
|
||||||
|
console.log(config)
|
||||||
|
|
||||||
useShareTimeline(() => {
|
useShareTimeline(() => {
|
||||||
return {
|
return {
|
||||||
title: '南南佐顿门窗 - 网宿软件',
|
title: '南南佐顿门窗 - 网宿软件',
|
||||||
@@ -50,7 +47,6 @@ function Home() {
|
|||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
// 用户点击确认,打开授权设置页面
|
|
||||||
openSetting();
|
openSetting();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,14 +54,11 @@ function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openSetting = () => {
|
const openSetting = () => {
|
||||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
|
||||||
Taro.openSetting({
|
Taro.openSetting({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.authSetting['scope.userInfo']) {
|
if (res.authSetting['scope.userInfo']) {
|
||||||
// 用户授权成功,可以获取用户信息
|
|
||||||
reload();
|
reload();
|
||||||
} else {
|
} else {
|
||||||
// 用户拒绝授权,提示授权失败
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '授权失败',
|
title: '授权失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -80,29 +73,21 @@ function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 获取站点信息
|
getShopInfo().then(() => {})
|
||||||
getShopInfo().then(() => {
|
|
||||||
|
|
||||||
})
|
|
||||||
// 获取配置信息
|
|
||||||
configWebsiteField({}).then(data => {
|
configWebsiteField({}).then(data => {
|
||||||
setConfig(data)
|
setConfig(data)
|
||||||
})
|
})
|
||||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
|
||||||
Taro.getSetting({
|
Taro.getSetting({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.authSetting['scope.userInfo']) {
|
if (res.authSetting['scope.userInfo']) {
|
||||||
// 用户已经授权过,可以直接获取用户信息
|
|
||||||
console.log('用户已经授权过,可以直接获取用户信息')
|
console.log('用户已经授权过,可以直接获取用户信息')
|
||||||
reload();
|
reload();
|
||||||
} else {
|
} else {
|
||||||
// 用户未授权,需要弹出授权窗口
|
|
||||||
console.log('用户未授权,需要弹出授权窗口')
|
console.log('用户未授权,需要弹出授权窗口')
|
||||||
showAuthModal();
|
showAuthModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 获取用户信息
|
|
||||||
Taro.getUserInfo({
|
Taro.getUserInfo({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const avatar = res.userInfo.avatarUrl;
|
const avatar = res.userInfo.avatarUrl;
|
||||||
@@ -113,19 +98,38 @@ function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*<Sticky threshold={0} onChange={() => onSticky(arguments)}>*/}
|
<Header/>
|
||||||
<Header/>
|
<View className={'home-page'}>
|
||||||
{/*</Sticky>*/}
|
|
||||||
<View className={'flex flex-col mt-1'}>
|
{/* ═══ Hero 区域:Banner 融入头部渐变背景 ═══ */}
|
||||||
<Menu/>
|
<View className={'hero-zone my-2'}>
|
||||||
<Banner/>
|
<View className={'hero-banner-wrap'}>
|
||||||
<Grid />
|
<Banner />
|
||||||
<NoticeBar content={config?.NoticeBar || '南南佐顿门窗,专业门窗定制安装服务,10年质保,德国进口五金,5000+家庭选择,98%客户满意度'} />
|
</View>
|
||||||
<BrochureEntry />
|
|
||||||
<BestSellers/>
|
{/* 功能菜单 — 直接铺在Banner下方,无卡片包裹 */}
|
||||||
<TrustSection/>
|
<View className={'hero-grid my-4'}>
|
||||||
{/* <CaseShowcase/> */}
|
<Grid />
|
||||||
<ContactSection/>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ═══ 滚动公告条 — 轻量嵌入 ═══ */}
|
||||||
|
{/*<View className={'notice-strip my-4'}>*/}
|
||||||
|
{/* <View className={'notice-strip__dot'} />*/}
|
||||||
|
{/* <Text className="text-15">*/}
|
||||||
|
{/* {config?.NoticeBar || '南南佐顿门窗,专业门窗定制安装服务,10年质保,德国进口五金,5000+家庭选择'}*/}
|
||||||
|
{/* </Text>*/}
|
||||||
|
{/*</View>*/}
|
||||||
|
|
||||||
|
{/* ═══ 品牌画册 — 沉浸式视觉卡片 ═══ */}
|
||||||
|
<CatalogShowcase />
|
||||||
|
|
||||||
|
{/* ═══ 品质信任区 — 横向滑动卡片 ═══ */}
|
||||||
|
<TrustSection />
|
||||||
|
|
||||||
|
{/* ═══ 联系我们 — 全宽底部 ═══ */}
|
||||||
|
<ContactSection />
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
<PopUpAd />
|
<PopUpAd />
|
||||||
</>
|
</>
|
||||||
|
|||||||
156
src/recommendation/index.scss
Normal file
156
src/recommendation/index.scss
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
.referral-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
padding: 24px 16px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.stats-title {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.form-field-area {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
width: 70px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
padding: 0 16px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-body {
|
||||||
|
.record-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
319
src/recommendation/index.tsx
Normal file
319
src/recommendation/index.tsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import {View, Text, ScrollView, Input, Button} from '@tarojs/components'
|
||||||
|
import {Toast} from '@nutui/nutui-react-taro'
|
||||||
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import {addReferral, getMyReferrals, getMyStats} from '@/api/app/referral'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||||
|
0: {text: '待确认', color: '#ff9800'},
|
||||||
|
1: {text: '有效', color: '#4caf50'},
|
||||||
|
2: {text: '无效', color: '#9e9e9e'},
|
||||||
|
3: {text: '已结算', color: '#2196f3'}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReferralPage: React.FC = () => {
|
||||||
|
const {dealerUser} = useDealerUser()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
customerCompany: '',
|
||||||
|
requirement: '',
|
||||||
|
remarks: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
validCount: 0,
|
||||||
|
settledCount: 0,
|
||||||
|
pendingAmount: '0.00',
|
||||||
|
referralCode: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 记录列表
|
||||||
|
const [records, setRecords] = useState<any[]>([])
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!dealerUser?.userId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// 获取统计
|
||||||
|
const statsRes = await getMyStats()
|
||||||
|
if (statsRes.data.code === 0) {
|
||||||
|
setStats(statsRes.data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
const listRes = await getMyReferrals({pageNum: 1, pageSize: 10})
|
||||||
|
if (listRes.data.code === 0) {
|
||||||
|
setRecords(listRes.data.data.list || [])
|
||||||
|
setPage(1)
|
||||||
|
setHasMore(listRes.data.data.list?.length === 10)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载失败', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData()
|
||||||
|
}, [dealerUser])
|
||||||
|
|
||||||
|
// 输入处理
|
||||||
|
const handleInput = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({...prev, [field]: value}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!formData.customerName.trim()) {
|
||||||
|
Toast.text('请输入客户姓名')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!formData.customerPhone.trim()) {
|
||||||
|
Toast.text('请输入客户电话')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(formData.customerPhone)) {
|
||||||
|
Toast.text('请输入正确的手机号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交报备
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true)
|
||||||
|
const res = await addReferral(formData)
|
||||||
|
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
Toast.text('报备成功!')
|
||||||
|
setFormData({
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
customerCompany: '',
|
||||||
|
requirement: '',
|
||||||
|
remarks: ''
|
||||||
|
})
|
||||||
|
loadData()
|
||||||
|
} else {
|
||||||
|
Toast.text(res.data.message || '报备失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Toast.text(error.message || '报备失败')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拨打电话
|
||||||
|
const handleCall = (phone: string) => {
|
||||||
|
if (phone) {
|
||||||
|
Taro.makePhoneCall({phoneNumber: phone})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (!hasMore || loading) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextPage = page + 1
|
||||||
|
const res = await getMyReferrals({pageNum: nextPage, pageSize: 10})
|
||||||
|
if (res.data.code === 0 && res.data.data.list) {
|
||||||
|
setRecords(prev => [...prev, ...res.data.data.list])
|
||||||
|
setPage(nextPage)
|
||||||
|
setHasMore(res.data.data.list.length === 10)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载更多失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="referral-page">
|
||||||
|
{/* 头部统计 */}
|
||||||
|
<View className="stats-section" style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'}}>
|
||||||
|
<View className="stats-title">
|
||||||
|
<Text className="text-white text-lg font-bold">我的推荐奖励</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stats-grid">
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">{stats.totalCount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>总推荐</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">{stats.pendingCount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>待确认</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">{stats.validCount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>有效</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-item">
|
||||||
|
<Text className="stat-value text-white">¥{stats.pendingAmount}</Text>
|
||||||
|
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}>待结算</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 报备表单 */}
|
||||||
|
<View className="form-section">
|
||||||
|
<View className="section-title">
|
||||||
|
<Text className="font-bold text-gray-800">推荐新客户</Text>
|
||||||
|
</View>
|
||||||
|
<View className="form-card">
|
||||||
|
<View className="form-field">
|
||||||
|
<Text className="form-label">客户姓名</Text>
|
||||||
|
<Input
|
||||||
|
className="form-input"
|
||||||
|
placeholder="请输入客户姓名"
|
||||||
|
value={formData.customerName}
|
||||||
|
onInput={(e) => handleInput('customerName', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="form-field">
|
||||||
|
<Text className="form-label">联系电话</Text>
|
||||||
|
<Input
|
||||||
|
className="form-input"
|
||||||
|
type="number"
|
||||||
|
maxlength={11}
|
||||||
|
placeholder="请输入客户电话"
|
||||||
|
value={formData.customerPhone}
|
||||||
|
onInput={(e) => handleInput('customerPhone', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="form-field">
|
||||||
|
<Text className="form-label">公司名称</Text>
|
||||||
|
<Input
|
||||||
|
className="form-input"
|
||||||
|
placeholder="请输入公司名称(选填)"
|
||||||
|
value={formData.customerCompany}
|
||||||
|
onInput={(e) => handleInput('customerCompany', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="form-field form-field-area">
|
||||||
|
<Text className="form-label">需求描述</Text>
|
||||||
|
<Input
|
||||||
|
className="form-input"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请描述客户需求(选填)"
|
||||||
|
value={formData.requirement}
|
||||||
|
onInput={(e) => handleInput('requirement', e.detail.value)}
|
||||||
|
style={{height: '80px', textAlign: 'left'}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="submit-btn">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={submitting}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
提交报备
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="tips">
|
||||||
|
<Text className="text-gray-500 text-sm">
|
||||||
|
报备成功后,业务员会尽快联系您的客户。成交后您将获得相应佣金奖励。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 推荐记录 */}
|
||||||
|
<View className="records-section">
|
||||||
|
<View className="section-title">
|
||||||
|
<Text className="font-bold text-gray-800">我的推荐记录</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{records.length === 0 ? (
|
||||||
|
<View className="empty-state">
|
||||||
|
<Text className="text-gray-400">暂无推荐记录</Text>
|
||||||
|
<Text className="text-gray-400 text-sm">快去推荐客户吧</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
scrollY
|
||||||
|
onScrollToLower={loadMore}
|
||||||
|
style={{height: '300px'}}
|
||||||
|
>
|
||||||
|
{records.map((item) => {
|
||||||
|
const statusInfo = STATUS_MAP[item.referralStatus] || STATUS_MAP[0]
|
||||||
|
return (
|
||||||
|
<View key={item.id} className="record-card">
|
||||||
|
<View className="record-header">
|
||||||
|
<View className="customer-info">
|
||||||
|
<Text className="font-bold text-gray-800">{item.customerName}</Text>
|
||||||
|
<Text
|
||||||
|
className="text-blue-500 text-sm ml-2"
|
||||||
|
onClick={() => handleCall(item.customerPhone)}
|
||||||
|
>
|
||||||
|
{item.customerPhone}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className="status-tag"
|
||||||
|
style={{backgroundColor: statusInfo.color + '20', color: statusInfo.color}}
|
||||||
|
>
|
||||||
|
{statusInfo.text}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="record-body">
|
||||||
|
<View className="record-row">
|
||||||
|
<Text className="text-gray-500 text-sm">推荐时间</Text>
|
||||||
|
<Text className="text-gray-700 text-sm">{item.createTime}</Text>
|
||||||
|
</View>
|
||||||
|
{parseFloat(item.referralFee) > 0 && (
|
||||||
|
<View className="record-row">
|
||||||
|
<Text className="text-gray-500 text-sm">奖励金额</Text>
|
||||||
|
<Text className="text-red-500 font-bold text-sm">
|
||||||
|
¥{item.referralFee}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!hasMore && records.length > 0 && (
|
||||||
|
<View className="no-more">
|
||||||
|
<Text className="text-gray-400 text-sm">没有更多了</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部安全区 */}
|
||||||
|
<View className="h-20"></View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReferralPage
|
||||||
@@ -3,6 +3,7 @@ import {ArrowRight} from '@nutui/icons-react-taro'
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {ConfigProvider} from '@nutui/nutui-react-taro'
|
import {ConfigProvider} from '@nutui/nutui-react-taro'
|
||||||
import Taro, {getCurrentInstance} from '@tarojs/taro'
|
import Taro, {getCurrentInstance} from '@tarojs/taro'
|
||||||
|
import {View, Text} from '@tarojs/components'
|
||||||
import {getUserInfo} from "@/api/layout";
|
import {getUserInfo} from "@/api/layout";
|
||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import { TextArea } from '@nutui/nutui-react-taro'
|
import { TextArea } from '@nutui/nutui-react-taro'
|
||||||
@@ -34,7 +35,7 @@ interface InputEvent {
|
|||||||
}
|
}
|
||||||
function Profile() {
|
function Profile() {
|
||||||
const formId = Number(router?.params.id)
|
const formId = Number(router?.params.id)
|
||||||
const {user, updateUser} = useUser()
|
const {user, updateUser, loading} = useUser()
|
||||||
|
|
||||||
const [sex, setSex] = useState<DictData[]>()
|
const [sex, setSex] = useState<DictData[]>()
|
||||||
const [FormData, setFormData] = useState<User>(
|
const [FormData, setFormData] = useState<User>(
|
||||||
@@ -49,16 +50,27 @@ function Profile() {
|
|||||||
comments: undefined
|
comments: undefined
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const [pageLoading, setPageLoading] = useState(true)
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
// 获取数据字典
|
// 获取数据字典
|
||||||
pageDictData({limit: 200}).then(res => {
|
pageDictData({limit: 200}).then(res => {
|
||||||
setSex(res?.list.filter((item) => item.dictCode === 'sex'))
|
setSex(res?.list.filter((item) => item.dictCode === 'sex'))
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('获取数据字典失败:', err)
|
||||||
})
|
})
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
getUserInfo().then((data) => {
|
getUserInfo().then((data) => {
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
setFormData(data);
|
setFormData(data);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('获取用户信息失败:', err)
|
||||||
|
Taro.showToast({
|
||||||
|
title: '获取用户信息失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
setPageLoading(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +152,12 @@ function Profile() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等待 useUser 初始化完成后再加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload()
|
if (!loading) {
|
||||||
}, []);
|
reload()
|
||||||
|
}
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
// 监听 useUser hook 中的用户信息变化,同步更新表单数据
|
// 监听 useUser hook 中的用户信息变化,同步更新表单数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -151,6 +166,15 @@ function Profile() {
|
|||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
// 加载中显示
|
||||||
|
if (loading || pageLoading) {
|
||||||
|
return (
|
||||||
|
<View className={'flex justify-center items-center h-screen'}>
|
||||||
|
<Text>加载中...</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'p-4'}>
|
<div className={'p-4'}>
|
||||||
@@ -223,14 +247,14 @@ function Profile() {
|
|||||||
label="备注信息"
|
label="备注信息"
|
||||||
name="comments"
|
name="comments"
|
||||||
initialValue={FormData.comments}
|
initialValue={FormData.comments}
|
||||||
rules={[{message: '备注信息'}]}
|
rules={[{message: '请输入备注信息'}]}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder={'个性签名'}
|
name="comments"
|
||||||
value={FormData?.comments}
|
placeholder={'个性签名'}
|
||||||
onChange={(value) => setFormData({...FormData, comments: value})}
|
value={FormData?.comments}
|
||||||
/>
|
onChange={(value) => setFormData({...FormData, comments: value})}
|
||||||
<Input placeholder={'个性签名'} />
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ module.exports = {
|
|||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
darkMode: 'media', // or 'media' or 'class'
|
darkMode: 'media', // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
fontSize: {
|
||||||
|
'15': '15px',
|
||||||
|
'17': '17px',
|
||||||
|
'28': '28px',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user