Compare commits

...

15 Commits

Author SHA1 Message Date
fc778e9de6 feat(user): 优化用户信息加载及表单体验
- 新增页面加载状态,加载中显示提示
- 等待 useUser 初始化完成后再加载用户数据
- 添加获取数据字典和用户信息的异常处理及错误提示
- 同步更新备注信息表单控件,修正校验提示文案
- 修正备注信息输入框的name属性,确保表单数据绑定正确
2026-04-29 17:07:14 +08:00
0ae33997ee feat(user): 优化用户信息加载及表单体验
- 新增页面加载状态,加载中显示提示
- 等待 useUser 初始化完成后再加载用户数据
- 添加获取数据字典和用户信息的异常处理及错误提示
- 同步更新备注信息表单控件,修正校验提示文案
- 修正备注信息输入框的name属性,确保表单数据绑定正确
2026-04-29 16:48:05 +08:00
1c7f35b40f fix(dealer): 移除楼层字段优化房号相关逻辑及表单
- 删除楼层相关代码及状态管理
- 更新房号唯一键及展示逻辑,去除楼层字段
- 表单中楼层输入改为普通输入框,禁用编辑状态
- 修正提交及校验逻辑,统一房号字段处理
- 简化编辑模式房号数据回填过程
- 移除小区、楼栋、单元、楼层、房号弹出选择组件及相关逻辑
- 更改提示文案,从“请选择”改为“请填写”房号相关项
2026-04-24 20:26:52 +08:00
a2009c8cea feat(customer): 优化客户列表访问权限及新增备注字段
- 修改客户添加页面,新增“备注”输入框,支持录入客户备注信息
- 调整环境配置,统一开发、生产、测试环境的API_BASE_URL为正式地址
- 在shopDealerApply模型中新增receptionistId字段,用于查询分配的客户
- 优化客户列表权限逻辑,改为登录即可查看,不再限制角色
- 更新接口调用参数,支持查询当前登录用户提交及分配的客户
- 移除对管理员角色的特殊判断,使权限逻辑更简洁
- UI调整,未登录时显示“请先登录”提示而非“没有查看权限”
2026-04-18 10:56:47 +08:00
2ff740fb07 fix(customer): 修正表单必填属性及组件属性拼写错误
- 将小区和房号字段的 required 属性明确设置为 true
- 修复签约时间和合同时间输入框的 readOnly 属性拼写错误
- 调整姓名和手机号表单项位置,避免隐藏字段内嵌套组件
- 在姓名与手机号表单项间添加间距提升布局美观性
2026-04-16 17:31:42 +08:00
b5f66017cf feat(referral): 新增楼栋单元楼层房号精细选择功能
- 将房号唯一键增加楼层字段,修改相关函数支持楼层处理
- 新增楼栋、单元、楼层、房号的选择状态和搜索过滤功能
- 实现楼栋、单元、楼层、房号的选择弹窗和清除按钮
- 表单改用选择控件替代输入框,隐藏字段同步表单数据
- 修改表单校验,验证楼栋、楼层、房号
2026-04-16 17:11:39 +08:00
099855e121 feat(referral): 新增楼栋单元楼层房号精细选择功能
- 将房号唯一键增加楼层字段,修改相关函数支持楼层处理
- 新增楼栋、单元、楼层、房号的选择状态和搜索过滤功能
- 实现楼栋、单元、楼层、房号的选择弹窗和清除按钮
- 表单改用选择控件替代输入框,隐藏字段同步表单数据
- 修改表单校验,验证楼栋、楼层、房号字段必填
- 编辑模式支持从dealerCode解析回填楼栋、单元、楼层、房号
- 优化房号规范化逻辑,去除楼层相关后缀
- 代码中统一使用规范化后的楼栋单元楼层房号构造唯一
2026-04-16 17:09:58 +08:00
8128e2ffb2 fix(dealer): 更新角色ID及重新登录逻辑
- 将用户角色ID从1848更新为1935
- 注册后延时1.5秒等待权限同步,无额外空行

feat(customer): 新增小区选择功能

- 添加小区选择状态管理及弹出层组件
- 实现小区列表加载与搜索过滤
- 支持小区的选择与清除操作,更新表单地址字段
- 表单回填时设置小区选中状态
- 用自定义Cell替代地址输入框,增加交互体验

fix(index): 修改邀请好友页面路径

- 将邀请好友页面路径由/dealer/qrcode/index更改为/dealer/team/index

fix(qrcode): 更新伙伴计划名称

- 将“南南佐顿门窗伙伴计划”修改为“桂乐淘伙伴计划”

fix(team): 隐藏手机号显示并简化成员信息

- 将手机号显示组件隐藏
- 移除显示UID的文本,仅保留加入时间显示
2026-04-16 15:14:36 +08:00
815678a1de feat(dealer): 优化分享码页面加载和保存功能
- 修改配置文件环境接口地址为本地调试
- 更新分享二维码页面标题为“账户管理中心”,启用分享按钮
- 新增分享小程序功能,支持转发给朋友和分享到朋友圈
- 改进生成小程序码的加载状态及错误处理
- 增加保存小程序码到相册的权限申请和下载容错机制
- 处理保存失败时授权提示和异常提醒
- 显示加载失败及重试按钮,避免未授权用户界面死循环
- 未成为分销商时增加跳转申请页面引导
- 更新邀请文案和页面UI细节优化
- 在useDealerUser钩子中新增无经销商数据自动跳转申请页逻辑
2026-04-16 14:37:25 +08:00
238a652afc feat(dealer): 优化分享码页面加载和保存功能
- 修改配置文件环境接口地址为本地调试
- 更新分享二维码页面标题为“账户管理中心”,启用分享按钮
- 新增分享小程序功能,支持转发给朋友和分享到朋友圈
- 改进生成小程序码的加载状态及错误处理
- 增加保存小程序码到相册的权限申请和下载容错机制
- 处理保存失败时授权提示和异常提醒
- 显示加载失败及重试按钮,避免未授权用户界面死循环
- 未成为分销商时增加跳转申请页面引导
- 更新邀请文案和页面UI细节优化
- 在useDealerUser钩子中新增无经销商数据自动跳转申请页逻辑
2026-04-16 14:22:23 +08:00
d2cdd42846 feat(referral): 添加推荐客户功能及相关接口
- 新增小程序端推荐客户页面,实现客户信息报备功能
- 添加推荐客户统计与推荐记录展示,支持分页加载更多
- 实现手机号格式校验及报备表单提交逻辑
- 新增拨打客户电话功能
- 在分销商首页添加“推荐客户”入口菜单项
- 新增推荐客户相关API接口,包括报备、查询列表、统计及状态更新
- 完善推荐客户页面样式,提升用户体验
2026-04-14 11:54:07 +08:00
0a72306d6a feat(referral): 添加推荐客户功能及相关接口
- 新增小程序端推荐客户页面,实现客户信息报备功能
- 添加推荐客户统计与推荐记录展示,支持分页加载更多
- 实现手机号格式校验及报备表单提交逻辑
- 新增拨打客户电话功能
- 在分销商首页添加“推荐客户”入口菜单项
- 新增推荐客户相关API接口,包括报备、查询列表、统计及状态更新
- 完善推荐客户页面样式,提升用户体验
2026-04-14 10:06:17 +08:00
128563bfeb feat(home): 将首页菜单改为硬编码
- 首页4个功能按钮改为硬编码,删除后端接口请求
- 菜单项包括:我要推荐、客户列表、邀请好友、个人中心
- 菜单图标使用OSS直链,减少接口延迟
- Grid组件中map数据源由接口返回改为本地硬编码数据
- ContactSection中在线咨询功能改用button标签并简化逻辑
- 增加ContactSection按钮样式覆盖,去除默认边框和样式
2026-04-09 14:04:08 +08:00
1cd535d517 feat(home): 将首页菜单改为硬编码
- 首页4个功能按钮改为硬编码,删除后端接口请求
- 菜单项包括:我要推荐、客户列表、邀请好友、个人中心
- 菜单图标使用OSS直链,减少接口延迟
- Grid组件中map数据源由接口返回改为本地硬编码数据
- ContactSection中在线咨询功能改用button标签并简化逻辑
- 增加ContactSection按钮样式覆盖,去除默认边框和样式
2026-04-09 13:57:22 +08:00
5f4ea47300 feat(home): 将首页菜单改为硬编码
- 首页4个功能按钮改为硬编码,删除后端接口请求
- 菜单项包括:我要推荐、客户列表、邀请好友、个人中心
- 菜单图标使用OSS直链,减少接口延迟
- Grid组件中map数据源由接口返回改为本地硬编码数据
- ContactSection中在线咨询功能改用button标签并简化逻辑
- 增加ContactSection按钮样式覆盖,去除默认边框和样式
2026-04-09 13:52:30 +08:00
28 changed files with 3178 additions and 165 deletions

View File

@@ -11,7 +11,29 @@
"usedAt": 1775709039214, "usedAt": 1775709039214,
"industryId": "all" "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": 1775713542885 "lastUpdated": 1776332994640
} }

View File

@@ -24,6 +24,11 @@
## Grid.tsx 硬编码改造 (13:48) ## Grid.tsx 硬编码改造 (13:48)
- 将首页4个功能按钮从后端接口请求改为硬编码 - 将首页4个功能按钮从后端接口请求改为硬编码
- 菜单项:我要推荐、客户列表、邀请好友、个人中心 - 菜单项:推荐客户、客户列表、邀请好友、个人中心
- 图片使用OSS直链避免接口延迟 - 图片使用OSS直链避免接口延迟
- 对应页面路径保持不变 - 对应页面路径保持不变
## 客户列表显示接待人员 (13:57)
- 在客户卡片中增加接待人员信息显示
- 字段来源receptionistName来自 ShopDealerApply model
- 仅当接待人员存在时显示

View 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` |

112
src/api/app/referral.ts Normal file
View File

@@ -0,0 +1,112 @@
/**
* 推荐客户 APIapp 模块)
* 小程序端调用
*/
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}`
)
}

View File

@@ -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
View 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}`
)
}

View File

@@ -66,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用于查询分配给自己的客户
} }

View File

@@ -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));
} }

View File

@@ -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"
] ]
}, },
// { // {

View File

@@ -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)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -602,26 +602,7 @@ 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'}}>
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
<Input placeholder="幸福里" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="buildingNo" label="楼栋号" required>
<Input placeholder="3" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="unitNo" label="单元号">
<Input placeholder="1" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="roomNo" label="房号" required>
<Input placeholder="1201" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="realName" label="姓名" initialValue={FormData?.realName} required>
<Input placeholder="张三" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="mobile" label="手机号" initialValue={FormData?.mobile} required>
<Input placeholder="手机号" disabled={isEditMode} maxLength={11}/>
</Form.Item>
{/* 接待人员选择 */} {/* 接待人员选择 */}
<Cell <Cell
title="接待人员" title="接待人员"
@@ -647,6 +628,28 @@ const AddShopDealerApply = () => {
} }
onClick={openReceptionistPicker} onClick={openReceptionistPicker}
/> />
<View className={'bg-gray-100 h-2'}></View>
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
<Input placeholder="幸福里" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="buildingNo" label="楼栋号" required>
<Input placeholder="3" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="unitNo" label="单元号">
<Input placeholder="1" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="roomNo" label="房号" required>
<Input placeholder="1201" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="realName" label="姓名" initialValue={FormData?.realName} required>
<Input placeholder="张三" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="mobile" label="手机号" initialValue={FormData?.mobile} required>
<Input placeholder="手机号" disabled={isEditMode} maxLength={11}/>
</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>

View File

@@ -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'}}

View File

@@ -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">

View File

@@ -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
}) })

View File

@@ -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 let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
}) try {
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({ Taro.showToast({
title: '保存成功', title: '保存成功',
icon: 'success' 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>
{/* 推广说明 */} {/* 推广说明 */}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '推荐客户赚佣金'
})

View 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;
}
}

View 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

View File

@@ -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>
<Text className="text-xs text-gray-500">UID{member.userId}</Text>
</Text>
<Text className="text-xs text-gray-500"> <Text className="text-xs text-gray-500">
{member.joinTime} {member.joinTime}
</Text> </Text>
</Space>
</View> </View>
</View> </View>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -14,9 +14,9 @@ interface MenuItem {
const menuList: MenuItem[] = [ const menuList: MenuItem[] = [
{ {
id: 1, id: 1,
title: '我要推荐', title: '推荐客户',
icon: 'https://oss.wsdns.cn/20260330/5f54527123864193b0a2078f812b117f.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90', icon: 'https://oss.wsdns.cn/20260330/5f54527123864193b0a2078f812b117f.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
path: '/dealer/qrcode/index' path: '/dealer/customer/add'
}, },
{ {
id: 2, id: 2,
@@ -28,13 +28,13 @@ const menuList: MenuItem[] = [
id: 3, id: 3,
title: '邀请好友', title: '邀请好友',
icon: 'https://oss.wsdns.cn/20260330/64cac0d5cbe645af8a574a257cd00302.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90', icon: 'https://oss.wsdns.cn/20260330/64cac0d5cbe645af8a574a257cd00302.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
path: '/dealer/qrcode/index' path: '/dealer/team/index'
}, },
{ {
id: 4, id: 4,
title: '个人中心', title: '个人中心',
icon: 'https://oss.wsdns.cn/20260330/6b198116f2d94b1e942c55ebe2f73728.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90', icon: 'https://oss.wsdns.cn/20260330/6b198116f2d94b1e942c55ebe2f73728.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90',
path: '/user/wallet/wallet' path: '/pages/user/user'
} }
]; ];

View File

@@ -12,8 +12,8 @@ const TrustSection: React.FC = () => {
{ {
icon: <ShieldCheck size={28} color="#ffffff" />, icon: <ShieldCheck size={28} color="#ffffff" />,
title: '品质保障', title: '品质保障',
highlight: '10年', highlight: '20年',
description: '质保承诺', description: '质保承诺(整窗)',
subDescription: '德国进口五金配件', subDescription: '德国进口五金配件',
bg: 'linear-gradient(135deg, #3b82f6, #2563eb)', bg: 'linear-gradient(135deg, #3b82f6, #2563eb)',
}, },
@@ -30,7 +30,7 @@ const TrustSection: React.FC = () => {
title: '客户好评', title: '客户好评',
highlight: '98%', highlight: '98%',
description: '满意度', description: '满意度',
subDescription: '5000+家庭信赖之选', subDescription: '10万+家庭信赖之选',
bg: 'linear-gradient(135deg, #f59e0b, #d97706)', bg: 'linear-gradient(135deg, #f59e0b, #d97706)',
} }
] ]

View File

@@ -3,7 +3,7 @@ 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 {View, Text} from '@tarojs/components' import {View} from '@tarojs/components'
import Grid from "@/pages/index/Grid"; import Grid from "@/pages/index/Grid";
import Banner from "./Banner"; import Banner from "./Banner";
import PopUpAd from "@/pages/index/PopUpAd"; import PopUpAd from "@/pages/index/PopUpAd";
@@ -17,6 +17,8 @@ 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: '南南佐顿门窗 - 网宿软件',

View 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;
}
}

View 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

View File

@@ -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(() => {
if (!loading) {
reload() 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
name="comments"
placeholder={'个性签名'} placeholder={'个性签名'}
value={FormData?.comments} value={FormData?.comments}
onChange={(value) => setFormData({...FormData, comments: value})} onChange={(value) => setFormData({...FormData, comments: value})}
/> />
<Input placeholder={'个性签名'} />
</Form.Item> </Form.Item>
</Form> </Form>
</ConfigProvider> </ConfigProvider>