Compare commits
9 Commits
f14e190a79
...
099855e121
| Author | SHA1 | Date | |
|---|---|---|---|
| 099855e121 | |||
| 8128e2ffb2 | |||
| 815678a1de | |||
| 238a652afc | |||
| d2cdd42846 | |||
| 0a72306d6a | |||
| 128563bfeb | |||
| 1cd535d517 | |||
| 5f4ea47300 |
@@ -11,7 +11,29 @@
|
||||
"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": 1775713542885
|
||||
"lastUpdated": 1776330360601
|
||||
}
|
||||
@@ -27,3 +27,8 @@
|
||||
- 菜单项:我要推荐、客户列表、邀请好友、个人中心
|
||||
- 图片使用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` |
|
||||
@@ -2,22 +2,22 @@
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
// API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
// API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '南南佐顿门窗',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
// API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
|
||||
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>>(
|
||||
'/cms/cms-navigation/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
if (res.code === 0) {
|
||||
return res.data ?? null;
|
||||
}
|
||||
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}`
|
||||
)
|
||||
}
|
||||
@@ -94,8 +94,8 @@ export async function getShopDealerReferee(id: number) {
|
||||
const res = await request.get<ApiResult<ShopDealerReferee>>(
|
||||
'/shop/shop-dealer-referee/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
if (res.code === 0) {
|
||||
return res.data ?? null;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
@@ -107,8 +107,8 @@ export async function getShopDealerRefereeByUserId(userId: number) {
|
||||
const res = await request.get<ApiResult<ShopDealerReferee>>(
|
||||
'/shop/shop-dealer-referee/getByUserId/' + userId
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
if (res.code === 0) {
|
||||
return res.data ?? null;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -72,7 +72,14 @@ export default defineAppConfig({
|
||||
"customer/trading",
|
||||
"wechat/index",
|
||||
"bank/index",
|
||||
"bank/add"
|
||||
"bank/add",
|
||||
"referral/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "recommendation",
|
||||
"pages": [
|
||||
"index"
|
||||
]
|
||||
},
|
||||
// {
|
||||
|
||||
@@ -207,7 +207,7 @@ const AddUserAddress = () => {
|
||||
if (roles.length > 0) {
|
||||
await updateUserRole({
|
||||
...roles[0],
|
||||
roleId: 1848
|
||||
roleId: 1935
|
||||
})
|
||||
}
|
||||
|
||||
@@ -231,16 +231,20 @@ const AddUserAddress = () => {
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
// 注册成功后等待1.5秒,让权限同步生效
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
} 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({
|
||||
title: '注册失败,请重试',
|
||||
title: error?.message || '注册失败,请重试',
|
||||
icon: 'error'
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from "@/utils/dateUtils";
|
||||
import {ShopDealerUser} from "@/api/shop/shopDealerUser/model";
|
||||
import {getShopDealerUser, pageShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
import {listDictData} from "@/api/system/dict-data";
|
||||
import type {DictData} from "@/api/system/dict-data/model";
|
||||
|
||||
const AddShopDealerApply = () => {
|
||||
const {params} = useRouter();
|
||||
@@ -30,20 +32,22 @@ const AddShopDealerApply = () => {
|
||||
const DUP_CHECK_MAX_PAGES = 50;
|
||||
|
||||
// 房号信息:用 dealerCode 存储唯一键,dealerName 存储展示文案
|
||||
const buildHouseKey = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
||||
const buildHouseKey = (community: string, buildingNo: string, unitNo: string | undefined, floorNo: string | undefined, roomNo: string) => {
|
||||
const c = (community || '').trim();
|
||||
const b = (buildingNo || '').trim();
|
||||
const u = (unitNo || '').trim();
|
||||
const f = (floorNo || '').trim();
|
||||
const r = (roomNo || '').trim();
|
||||
return [c, b, u, r].join('|');
|
||||
return [c, b, u, f, r].join('|');
|
||||
};
|
||||
|
||||
const buildHouseDisplay = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
||||
const buildHouseDisplay = (community: string, buildingNo: string, unitNo: string | undefined, floorNo: string | undefined, roomNo: string) => {
|
||||
const c = (community || '').trim();
|
||||
const b = (buildingNo || '').trim();
|
||||
const u = (unitNo || '').trim();
|
||||
const f = (floorNo || '').trim();
|
||||
const r = (roomNo || '').trim();
|
||||
return `${c}${b ? `${b}栋` : ''}${u ? `${u}单元` : ''}${r ? `${r}号` : ''}`;
|
||||
return `${c}${b ? `${b}栋` : ''}${u ? `${u}单元` : ''}${f ? `${f}楼` : ''}${r ? `${r}号` : ''}`;
|
||||
};
|
||||
|
||||
const parseHouseKey = (key?: string) => {
|
||||
@@ -52,7 +56,8 @@ const AddShopDealerApply = () => {
|
||||
community: parts[0] || '',
|
||||
buildingNo: parts[1] || '',
|
||||
unitNo: parts[2] || '',
|
||||
roomNo: parts[3] || '',
|
||||
floorNo: parts[3] || '',
|
||||
roomNo: parts[4] || '',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -69,6 +74,41 @@ const AddShopDealerApply = () => {
|
||||
const [receptionistLoading, setReceptionistLoading] = useState<boolean>(false)
|
||||
const [selectedReceptionist, setSelectedReceptionist] = useState<ShopDealerUser | null>(null)
|
||||
|
||||
// 小区选择状态
|
||||
const [showCommunityPicker, setShowCommunityPicker] = useState<boolean>(false)
|
||||
const [communitySearch, setCommunitySearch] = useState<string>('')
|
||||
const [communityList, setCommunityList] = useState<DictData[]>([])
|
||||
const [communityLoading, setCommunityLoading] = useState<boolean>(false)
|
||||
const [selectedCommunity, setSelectedCommunity] = useState<DictData | null>(null)
|
||||
|
||||
// 楼栋选择状态
|
||||
const [showBuildingPicker, setShowBuildingPicker] = useState<boolean>(false)
|
||||
const [buildingSearch, setBuildingSearch] = useState<string>('')
|
||||
const [buildingList, setBuildingList] = useState<DictData[]>([])
|
||||
const [buildingLoading, setBuildingLoading] = useState<boolean>(false)
|
||||
const [selectedBuilding, setSelectedBuilding] = useState<DictData | null>(null)
|
||||
|
||||
// 单元选择状态
|
||||
const [showUnitPicker, setShowUnitPicker] = useState<boolean>(false)
|
||||
const [unitSearch, setUnitSearch] = useState<string>('')
|
||||
const [unitList, setUnitList] = useState<DictData[]>([])
|
||||
const [unitLoading, setUnitLoading] = useState<boolean>(false)
|
||||
const [selectedUnit, setSelectedUnit] = useState<DictData | null>(null)
|
||||
|
||||
// 楼层选择状态
|
||||
const [showFloorPicker, setShowFloorPicker] = useState<boolean>(false)
|
||||
const [floorSearch, setFloorSearch] = useState<string>('')
|
||||
const [floorList, setFloorList] = useState<DictData[]>([])
|
||||
const [floorLoading, setFloorLoading] = useState<boolean>(false)
|
||||
const [selectedFloor, setSelectedFloor] = useState<DictData | null>(null)
|
||||
|
||||
// 房号选择状态
|
||||
const [showRoomPicker, setShowRoomPicker] = useState<boolean>(false)
|
||||
const [roomSearch, setRoomSearch] = useState<string>('')
|
||||
const [roomList, setRoomList] = useState<DictData[]>([])
|
||||
const [roomLoading, setRoomLoading] = useState<boolean>(false)
|
||||
const [selectedRoom, setSelectedRoom] = useState<DictData | null>(null)
|
||||
|
||||
// 获取审核状态文字
|
||||
const getApplyStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
@@ -83,8 +123,6 @@ const AddShopDealerApply = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(getApplyStatusText)
|
||||
|
||||
// 处理签约时间选择
|
||||
const handleApplyTimeConfirm = (param: string) => {
|
||||
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
|
||||
@@ -196,6 +234,278 @@ const AddShopDealerApply = () => {
|
||||
setSelectedReceptionist(null)
|
||||
}
|
||||
|
||||
// 加载小区列表
|
||||
const loadCommunityList = async (keyword?: string) => {
|
||||
setCommunityLoading(true)
|
||||
try {
|
||||
const list = await listDictData({ dictCode: 'xiaoqu' })
|
||||
// 过滤搜索关键词
|
||||
if (keyword) {
|
||||
setCommunityList(list.filter((item: DictData) =>
|
||||
(item.dictDataName || '').includes(keyword) ||
|
||||
(item.label || '').includes(keyword)
|
||||
))
|
||||
} else {
|
||||
setCommunityList(list)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载小区列表失败:', e)
|
||||
} finally {
|
||||
setCommunityLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开小区选择
|
||||
const openCommunityPicker = () => {
|
||||
setCommunitySearch('')
|
||||
loadCommunityList()
|
||||
setShowCommunityPicker(true)
|
||||
}
|
||||
|
||||
// 搜索小区
|
||||
const handleCommunitySearch = (val: string) => {
|
||||
setCommunitySearch(val)
|
||||
loadCommunityList(val)
|
||||
}
|
||||
|
||||
// 选择小区
|
||||
const handleSelectCommunity = (item: DictData) => {
|
||||
setSelectedCommunity(item)
|
||||
setShowCommunityPicker(false)
|
||||
// 更新表单数据
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
address: item.dictDataName || item.label || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 清除小区
|
||||
const handleClearCommunity = () => {
|
||||
setSelectedCommunity(null)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
address: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载楼栋列表
|
||||
const loadBuildingList = async (keyword?: string) => {
|
||||
setBuildingLoading(true)
|
||||
try {
|
||||
const list = await listDictData({ dictCode: 'building' })
|
||||
if (keyword) {
|
||||
setBuildingList(list.filter((item: DictData) =>
|
||||
(item.dictDataName || '').includes(keyword) ||
|
||||
(item.label || '').includes(keyword)
|
||||
))
|
||||
} else {
|
||||
setBuildingList(list)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载楼栋列表失败:', e)
|
||||
} finally {
|
||||
setBuildingLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开楼栋选择
|
||||
const openBuildingPicker = () => {
|
||||
setBuildingSearch('')
|
||||
loadBuildingList()
|
||||
setShowBuildingPicker(true)
|
||||
}
|
||||
|
||||
// 搜索楼栋
|
||||
const handleBuildingSearch = (val: string) => {
|
||||
setBuildingSearch(val)
|
||||
loadBuildingList(val)
|
||||
}
|
||||
|
||||
// 选择楼栋
|
||||
const handleSelectBuilding = (item: DictData) => {
|
||||
setSelectedBuilding(item)
|
||||
setShowBuildingPicker(false)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
buildingNo: item.dictDataName || item.label || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 清除楼栋
|
||||
const handleClearBuilding = () => {
|
||||
setSelectedBuilding(null)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
buildingNo: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载单元列表
|
||||
const loadUnitList = async (keyword?: string) => {
|
||||
setUnitLoading(true)
|
||||
try {
|
||||
const list = await listDictData({ dictCode: 'unit' })
|
||||
if (keyword) {
|
||||
setUnitList(list.filter((item: DictData) =>
|
||||
(item.dictDataName || '').includes(keyword) ||
|
||||
(item.label || '').includes(keyword)
|
||||
))
|
||||
} else {
|
||||
setUnitList(list)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载单元列表失败:', e)
|
||||
} finally {
|
||||
setUnitLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开单元选择
|
||||
const openUnitPicker = () => {
|
||||
setUnitSearch('')
|
||||
loadUnitList()
|
||||
setShowUnitPicker(true)
|
||||
}
|
||||
|
||||
// 搜索单元
|
||||
const handleUnitSearch = (val: string) => {
|
||||
setUnitSearch(val)
|
||||
loadUnitList(val)
|
||||
}
|
||||
|
||||
// 选择单元
|
||||
const handleSelectUnit = (item: DictData) => {
|
||||
setSelectedUnit(item)
|
||||
setShowUnitPicker(false)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
unitNo: item.dictDataName || item.label || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 清除单元
|
||||
const handleClearUnit = () => {
|
||||
setSelectedUnit(null)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
unitNo: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载楼层列表
|
||||
const loadFloorList = async (keyword?: string) => {
|
||||
setFloorLoading(true)
|
||||
try {
|
||||
const list = await listDictData({ dictCode: 'floor' })
|
||||
if (keyword) {
|
||||
setFloorList(list.filter((item: DictData) =>
|
||||
(item.dictDataName || '').includes(keyword) ||
|
||||
(item.label || '').includes(keyword)
|
||||
))
|
||||
} else {
|
||||
setFloorList(list)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载楼层列表失败:', e)
|
||||
} finally {
|
||||
setFloorLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开楼层选择
|
||||
const openFloorPicker = () => {
|
||||
setFloorSearch('')
|
||||
loadFloorList()
|
||||
setShowFloorPicker(true)
|
||||
}
|
||||
|
||||
// 搜索楼层
|
||||
const handleFloorSearch = (val: string) => {
|
||||
setFloorSearch(val)
|
||||
loadFloorList(val)
|
||||
}
|
||||
|
||||
// 选择楼层
|
||||
const handleSelectFloor = (item: DictData) => {
|
||||
setSelectedFloor(item)
|
||||
setShowFloorPicker(false)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
floorNo: item.dictDataName || item.label || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 清除楼层
|
||||
const handleClearFloor = () => {
|
||||
setSelectedFloor(null)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
floorNo: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载房号列表
|
||||
const loadRoomList = async (keyword?: string) => {
|
||||
setRoomLoading(true)
|
||||
try {
|
||||
const list = await listDictData({ dictCode: 'room' })
|
||||
if (keyword) {
|
||||
setRoomList(list.filter((item: DictData) =>
|
||||
(item.dictDataName || '').includes(keyword) ||
|
||||
(item.label || '').includes(keyword)
|
||||
))
|
||||
} else {
|
||||
setRoomList(list)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载房号列表失败:', e)
|
||||
} finally {
|
||||
setRoomLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开房号选择
|
||||
const openRoomPicker = () => {
|
||||
setRoomSearch('')
|
||||
loadRoomList()
|
||||
setShowRoomPicker(true)
|
||||
}
|
||||
|
||||
// 搜索房号
|
||||
const handleRoomSearch = (val: string) => {
|
||||
setRoomSearch(val)
|
||||
loadRoomList(val)
|
||||
}
|
||||
|
||||
// 选择房号
|
||||
const handleSelectRoom = (item: DictData) => {
|
||||
setSelectedRoom(item)
|
||||
setShowRoomPicker(false)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
roomNo: item.dictDataName || item.label || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 清除房号
|
||||
const handleClearRoom = () => {
|
||||
setSelectedRoom(null)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
roomNo: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
// 计算保护期过期时间(15天后)
|
||||
const calculateExpirationTime = (): string => {
|
||||
@@ -269,13 +579,14 @@ const AddShopDealerApply = () => {
|
||||
return s.replace(/\s+/g, '').toUpperCase();
|
||||
};
|
||||
|
||||
const normalizeHouseNoPart = (raw: string, kind: 'building' | 'unit' | 'room') => {
|
||||
const normalizeHouseNoPart = (raw: string, kind: 'building' | 'unit' | 'floor' | 'room') => {
|
||||
let s = toHalfWidth(normalizeText(raw)).toUpperCase();
|
||||
s = s.replace(/\s+/g, '');
|
||||
|
||||
// 去掉常见后缀/装饰词
|
||||
if (kind === 'building') s = s.replace(/(号楼|栋|幢|楼)$/g, '');
|
||||
if (kind === 'unit') s = s.replace(/(单元)$/g, '');
|
||||
if (kind === 'floor') s = s.replace(/(楼|层)$/g, '');
|
||||
if (kind === 'room') s = s.replace(/(室|房|号)$/g, '');
|
||||
|
||||
// 只保留数字与字母,统一分隔符差异(如 12-01 / 12#01)
|
||||
@@ -290,12 +601,13 @@ const AddShopDealerApply = () => {
|
||||
return s;
|
||||
};
|
||||
|
||||
const buildHouseKeyNormalized = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
||||
const buildHouseKeyNormalized = (community: string, buildingNo: string, unitNo: string | undefined, floorNo: string | undefined, roomNo: string) => {
|
||||
const c = normalizeCommunity(community);
|
||||
const b = normalizeHouseNoPart(buildingNo, 'building');
|
||||
const u = normalizeHouseNoPart(unitNo || '', 'unit');
|
||||
const f = normalizeHouseNoPart(floorNo || '', 'floor');
|
||||
const r = normalizeHouseNoPart(roomNo, 'room');
|
||||
return [c, b, u, r].join('|');
|
||||
return [c, b, u, f, r].join('|');
|
||||
};
|
||||
|
||||
const getNormalizedHouseKeyFromApply = (apply: ShopDealerApply) => {
|
||||
@@ -304,6 +616,7 @@ const AddShopDealerApply = () => {
|
||||
parsed.community || apply.address || '',
|
||||
parsed.buildingNo || '',
|
||||
parsed.unitNo || '',
|
||||
parsed.floorNo || '',
|
||||
parsed.roomNo || ''
|
||||
);
|
||||
};
|
||||
@@ -347,11 +660,15 @@ const AddShopDealerApply = () => {
|
||||
return;
|
||||
}
|
||||
if (!values.buildingNo || values.buildingNo.trim() === '') {
|
||||
Taro.showToast({title: '请填写楼栋号', icon: 'error'});
|
||||
Taro.showToast({title: '请选择楼栋', icon: 'error'});
|
||||
return;
|
||||
}
|
||||
if (!values.floorNo || values.floorNo.trim() === '') {
|
||||
Taro.showToast({title: '请选择楼层', icon: 'error'});
|
||||
return;
|
||||
}
|
||||
if (!values.roomNo || values.roomNo.trim() === '') {
|
||||
Taro.showToast({title: '请填写房号', icon: 'error'});
|
||||
Taro.showToast({title: '请选择房号', icon: 'error'});
|
||||
return;
|
||||
}
|
||||
if (!values.realName || values.realName.trim() === '') {
|
||||
@@ -406,10 +723,10 @@ const AddShopDealerApply = () => {
|
||||
? reporterDealerUser.refereeId
|
||||
: undefined;
|
||||
|
||||
const houseKeyRaw = buildHouseKey(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
const houseKeyNormalized = buildHouseKeyNormalized(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
const houseKeyRaw = buildHouseKey(values.address, values.buildingNo, values.unitNo, values.floorNo, values.roomNo);
|
||||
const houseKeyNormalized = buildHouseKeyNormalized(values.address, values.buildingNo, values.unitNo, values.floorNo, values.roomNo);
|
||||
const houseKey = houseKeyNormalized || houseKeyRaw;
|
||||
const houseDisplay = buildHouseDisplay(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
const houseDisplay = buildHouseDisplay(values.address, values.buildingNo, values.unitNo, values.floorNo, values.roomNo);
|
||||
|
||||
// 新增报备:提交前检查房号是否已报备(按 小区+楼栋+单元+房号 判断,且做规范化)
|
||||
if (!isEditMode) {
|
||||
@@ -486,8 +803,8 @@ const AddShopDealerApply = () => {
|
||||
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
|
||||
|
||||
// 准备提交的数据
|
||||
// 避免把表单里的楼栋/单元/房号等临时字段原样提交给后端
|
||||
const {buildingNo, unitNo, roomNo, ...restValues} = values;
|
||||
// 避免把表单里的楼栋/单元/楼层/房号等临时字段原样提交给后端
|
||||
const {buildingNo, unitNo, floorNo, roomNo, ...restValues} = values;
|
||||
const submitData = {
|
||||
...restValues,
|
||||
type: 4,
|
||||
@@ -574,18 +891,55 @@ const AddShopDealerApply = () => {
|
||||
})
|
||||
}, []); // 依赖用户ID,当用户变化时重新加载
|
||||
|
||||
// 编辑模式下,从 dealerCode 反解出楼栋/单元/房号,回填表单(只读展示)
|
||||
// 编辑模式下,从 dealerCode 反解出楼栋/单元/楼层/房号,回填表单(只读展示)
|
||||
useEffect(() => {
|
||||
if (!formRef.current || !FormData) return;
|
||||
const parsed = parseHouseKey(FormData.dealerCode);
|
||||
const communityValue = parsed.community || FormData.address || '';
|
||||
formRef.current.setFieldsValue({
|
||||
address: parsed.community || FormData.address,
|
||||
address: communityValue,
|
||||
buildingNo: parsed.buildingNo,
|
||||
unitNo: parsed.unitNo,
|
||||
floorNo: parsed.floorNo,
|
||||
roomNo: parsed.roomNo,
|
||||
realName: FormData.realName,
|
||||
mobile: FormData.mobile
|
||||
});
|
||||
// 回填小区选中状态
|
||||
if (communityValue) {
|
||||
setSelectedCommunity({
|
||||
dictDataName: communityValue,
|
||||
label: communityValue
|
||||
} as DictData)
|
||||
}
|
||||
// 回填楼栋选中状态
|
||||
if (parsed.buildingNo) {
|
||||
setSelectedBuilding({
|
||||
dictDataName: parsed.buildingNo,
|
||||
label: parsed.buildingNo
|
||||
} as DictData)
|
||||
}
|
||||
// 回填单元选中状态
|
||||
if (parsed.unitNo) {
|
||||
setSelectedUnit({
|
||||
dictDataName: parsed.unitNo,
|
||||
label: parsed.unitNo
|
||||
} as DictData)
|
||||
}
|
||||
// 回填楼层选中状态
|
||||
if (parsed.floorNo) {
|
||||
setSelectedFloor({
|
||||
dictDataName: parsed.floorNo,
|
||||
label: parsed.floorNo
|
||||
} as DictData)
|
||||
}
|
||||
// 回填房号选中状态
|
||||
if (parsed.roomNo) {
|
||||
setSelectedRoom({
|
||||
dictDataName: parsed.roomNo,
|
||||
label: parsed.roomNo
|
||||
} as DictData)
|
||||
}
|
||||
}, [FormData]);
|
||||
|
||||
if (loading) {
|
||||
@@ -604,17 +958,153 @@ const AddShopDealerApply = () => {
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
|
||||
<Input placeholder="幸福里" disabled={isEditMode}/>
|
||||
<Cell
|
||||
title="小区"
|
||||
required
|
||||
extra={
|
||||
<View className="flex items-center">
|
||||
{selectedCommunity ? (
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm text-gray-800 mr-2">
|
||||
{selectedCommunity.dictDataName || selectedCommunity.label}
|
||||
</Text>
|
||||
{!isEditMode && (
|
||||
<View
|
||||
onClick={(e) => { e.stopPropagation(); handleClearCommunity(); }}
|
||||
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={isEditMode ? undefined : openCommunityPicker}
|
||||
/>
|
||||
{/* 隐藏字段,通过 ref.setFieldsValue 设置 */}
|
||||
<Form.Item name="address" style={{display: 'none'}}>
|
||||
<View />
|
||||
</Form.Item>
|
||||
<Form.Item name="buildingNo" label="楼栋号" required>
|
||||
<Input placeholder="3" disabled={isEditMode}/>
|
||||
{/* 楼栋选择 */}
|
||||
<Cell
|
||||
title="楼栋"
|
||||
extra={
|
||||
<View className="flex items-center">
|
||||
{selectedBuilding ? (
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm text-gray-800 mr-2">{selectedBuilding.dictDataName || selectedBuilding.label}</Text>
|
||||
{!isEditMode && (
|
||||
<View
|
||||
onClick={(e) => { e.stopPropagation(); handleClearBuilding(); }}
|
||||
className="flex items-center px-1"
|
||||
>
|
||||
<Del size={14} color="#999"/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-sm text-gray-400">请选择</Text>
|
||||
)}
|
||||
{!isEditMode && <ArrowRight size={14} color="#ccc"/>}
|
||||
</View>
|
||||
}
|
||||
onClick={isEditMode ? undefined : openBuildingPicker}
|
||||
/>
|
||||
{/* 隐藏字段,通过 ref.setFieldsValue 设置 */}
|
||||
<Form.Item name="buildingNo" style={{display: 'none'}}>
|
||||
<View />
|
||||
</Form.Item>
|
||||
<Form.Item name="unitNo" label="单元号">
|
||||
<Input placeholder="1" disabled={isEditMode}/>
|
||||
{/* 单元选择 */}
|
||||
<Cell
|
||||
title="单元"
|
||||
extra={
|
||||
<View className="flex items-center">
|
||||
{selectedUnit ? (
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm text-gray-800 mr-2">{selectedUnit.dictDataName || selectedUnit.label}</Text>
|
||||
{!isEditMode && (
|
||||
<View
|
||||
onClick={(e) => { e.stopPropagation(); handleClearUnit(); }}
|
||||
className="flex items-center px-1"
|
||||
>
|
||||
<Del size={14} color="#999"/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-sm text-gray-400">请选择</Text>
|
||||
)}
|
||||
{!isEditMode && <ArrowRight size={14} color="#ccc"/>}
|
||||
</View>
|
||||
}
|
||||
onClick={isEditMode ? undefined : openUnitPicker}
|
||||
/>
|
||||
{/* 隐藏字段,通过 ref.setFieldsValue 设置 */}
|
||||
<Form.Item name="unitNo" style={{display: 'none'}}>
|
||||
<View />
|
||||
</Form.Item>
|
||||
<Form.Item name="roomNo" label="房号" required>
|
||||
<Input placeholder="1201" disabled={isEditMode}/>
|
||||
{/* 楼层选择 */}
|
||||
<Cell
|
||||
title="楼层"
|
||||
extra={
|
||||
<View className="flex items-center">
|
||||
{selectedFloor ? (
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm text-gray-800 mr-2">{selectedFloor.dictDataName || selectedFloor.label}</Text>
|
||||
{!isEditMode && (
|
||||
<View
|
||||
onClick={(e) => { e.stopPropagation(); handleClearFloor(); }}
|
||||
className="flex items-center px-1"
|
||||
>
|
||||
<Del size={14} color="#999"/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-sm text-gray-400">请选择</Text>
|
||||
)}
|
||||
{!isEditMode && <ArrowRight size={14} color="#ccc"/>}
|
||||
</View>
|
||||
}
|
||||
onClick={isEditMode ? undefined : openFloorPicker}
|
||||
/>
|
||||
{/* 隐藏字段,通过 ref.setFieldsValue 设置 */}
|
||||
<Form.Item name="floorNo" style={{display: 'none'}}>
|
||||
<View />
|
||||
</Form.Item>
|
||||
{/* 房号选择 */}
|
||||
<Cell
|
||||
title="房号"
|
||||
required
|
||||
extra={
|
||||
<View className="flex items-center">
|
||||
{selectedRoom ? (
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm text-gray-800 mr-2">{selectedRoom.dictDataName || selectedRoom.label}</Text>
|
||||
{!isEditMode && (
|
||||
<View
|
||||
onClick={(e) => { e.stopPropagation(); handleClearRoom(); }}
|
||||
className="flex items-center px-1"
|
||||
>
|
||||
<Del size={14} color="#999"/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-sm text-gray-400">请选择</Text>
|
||||
)}
|
||||
{!isEditMode && <ArrowRight size={14} color="#ccc"/>}
|
||||
</View>
|
||||
}
|
||||
onClick={isEditMode ? undefined : openRoomPicker}
|
||||
/>
|
||||
{/* 隐藏字段,通过 ref.setFieldsValue 设置 */}
|
||||
<Form.Item name="roomNo" style={{display: 'none'}}>
|
||||
<View />
|
||||
</Form.Item>
|
||||
<Form.Item name="realName" label="姓名" initialValue={FormData?.realName} required>
|
||||
<Input placeholder="张三" disabled={isEditMode}/>
|
||||
@@ -650,32 +1140,24 @@ const AddShopDealerApply = () => {
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
|
||||
<Input placeholder="(元/兆瓦时)" disabled={false}/>
|
||||
<Input placeholder="(元/兆瓦时)" />
|
||||
</Form.Item>
|
||||
<Form.Item name="applyTime" label="签约时间" initialValue={FormData?.applyTime}>
|
||||
<View
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => setShowApplyTimePicker(true)}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<CalendarIcon size={16} color="#999" className="mr-2"/>
|
||||
<Text style={{color: applyTime ? '#333' : '#999'}}>
|
||||
{applyTime ? formatDateForDisplay(applyTime) : '请选择签约时间'}
|
||||
</Text>
|
||||
</View>
|
||||
<View onClick={() => setShowApplyTimePicker(true)}>
|
||||
<Input
|
||||
placeholder="点击选择签约时间"
|
||||
readonly
|
||||
value={applyTime ? formatDateForDisplay(applyTime) : ''}
|
||||
/>
|
||||
</View>
|
||||
</Form.Item>
|
||||
<Form.Item name="contractTime" label="合同日期" initialValue={FormData?.contractTime}>
|
||||
<View
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => setShowContractTimePicker(true)}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<CalendarIcon size={16} color="#999" className="mr-2"/>
|
||||
<Text style={{color: contractTime ? '#333' : '#999'}}>
|
||||
{contractTime ? formatDateForDisplay(contractTime) : '请选择合同生效起止时间'}
|
||||
</Text>
|
||||
</View>
|
||||
<View onClick={() => setShowContractTimePicker(true)}>
|
||||
<Input
|
||||
placeholder="点击选择合同生效起止时间"
|
||||
readonly
|
||||
value={contractTime ? formatDateForDisplay(contractTime) : ''}
|
||||
/>
|
||||
</View>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
@@ -762,6 +1244,267 @@ const AddShopDealerApply = () => {
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 小区选择弹出层 */}
|
||||
<Popup
|
||||
visible={showCommunityPicker}
|
||||
position="bottom"
|
||||
round
|
||||
onClose={() => setShowCommunityPicker(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={() => setShowCommunityPicker(false)}>
|
||||
<Text className="text-sm text-blue-500">取消</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 搜索框 */}
|
||||
<View className="px-3 py-2">
|
||||
<SearchBar
|
||||
value={communitySearch}
|
||||
placeholder="搜索小区名称"
|
||||
onChange={handleCommunitySearch}
|
||||
/>
|
||||
</View>
|
||||
{/* 列表 */}
|
||||
<View className="flex-1 overflow-y-auto">
|
||||
{communityLoading ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Loading>加载中</Loading>
|
||||
</View>
|
||||
) : communityList.length === 0 ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Text className="text-sm text-gray-400">暂无小区数据</Text>
|
||||
</View>
|
||||
) : (
|
||||
communityList.map((item, index) => (
|
||||
<Cell
|
||||
key={item.dictDataId || index}
|
||||
title={item.dictDataName || item.label || ''}
|
||||
description={item.comments || ''}
|
||||
extra={
|
||||
selectedCommunity?.dictDataId === item.dictDataId ? (
|
||||
<Text className="text-sm text-blue-500">已选</Text>
|
||||
) : null
|
||||
}
|
||||
onClick={() => handleSelectCommunity(item)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 楼栋选择弹出层 */}
|
||||
<Popup
|
||||
visible={showBuildingPicker}
|
||||
position="bottom"
|
||||
round
|
||||
onClose={() => setShowBuildingPicker(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={() => setShowBuildingPicker(false)}>
|
||||
<Text className="text-sm text-blue-500">取消</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 搜索框 */}
|
||||
<View className="px-3 py-2">
|
||||
<SearchBar
|
||||
value={buildingSearch}
|
||||
placeholder="搜索楼栋号"
|
||||
onChange={handleBuildingSearch}
|
||||
/>
|
||||
</View>
|
||||
{/* 列表 */}
|
||||
<View className="flex-1 overflow-y-auto">
|
||||
{buildingLoading ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Loading>加载中</Loading>
|
||||
</View>
|
||||
) : buildingList.length === 0 ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Text className="text-sm text-gray-400">暂无楼栋数据</Text>
|
||||
</View>
|
||||
) : (
|
||||
buildingList.map((item, index) => (
|
||||
<Cell
|
||||
key={item.dictDataId || index}
|
||||
title={item.dictDataName || item.label || ''}
|
||||
extra={
|
||||
selectedBuilding?.dictDataId === item.dictDataId ? (
|
||||
<Text className="text-sm text-blue-500">已选</Text>
|
||||
) : null
|
||||
}
|
||||
onClick={() => handleSelectBuilding(item)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 单元选择弹出层 */}
|
||||
<Popup
|
||||
visible={showUnitPicker}
|
||||
position="bottom"
|
||||
round
|
||||
onClose={() => setShowUnitPicker(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={() => setShowUnitPicker(false)}>
|
||||
<Text className="text-sm text-blue-500">取消</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 搜索框 */}
|
||||
<View className="px-3 py-2">
|
||||
<SearchBar
|
||||
value={unitSearch}
|
||||
placeholder="搜索单元号"
|
||||
onChange={handleUnitSearch}
|
||||
/>
|
||||
</View>
|
||||
{/* 列表 */}
|
||||
<View className="flex-1 overflow-y-auto">
|
||||
{unitLoading ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Loading>加载中</Loading>
|
||||
</View>
|
||||
) : unitList.length === 0 ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Text className="text-sm text-gray-400">暂无单元数据</Text>
|
||||
</View>
|
||||
) : (
|
||||
unitList.map((item, index) => (
|
||||
<Cell
|
||||
key={item.dictDataId || index}
|
||||
title={item.dictDataName || item.label || ''}
|
||||
extra={
|
||||
selectedUnit?.dictDataId === item.dictDataId ? (
|
||||
<Text className="text-sm text-blue-500">已选</Text>
|
||||
) : null
|
||||
}
|
||||
onClick={() => handleSelectUnit(item)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 楼层选择弹出层 */}
|
||||
<Popup
|
||||
visible={showFloorPicker}
|
||||
position="bottom"
|
||||
round
|
||||
onClose={() => setShowFloorPicker(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={() => setShowFloorPicker(false)}>
|
||||
<Text className="text-sm text-blue-500">取消</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 搜索框 */}
|
||||
<View className="px-3 py-2">
|
||||
<SearchBar
|
||||
value={floorSearch}
|
||||
placeholder="搜索楼层"
|
||||
onChange={handleFloorSearch}
|
||||
/>
|
||||
</View>
|
||||
{/* 列表 */}
|
||||
<View className="flex-1 overflow-y-auto">
|
||||
{floorLoading ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Loading>加载中</Loading>
|
||||
</View>
|
||||
) : floorList.length === 0 ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Text className="text-sm text-gray-400">暂无楼层数据</Text>
|
||||
</View>
|
||||
) : (
|
||||
floorList.map((item, index) => (
|
||||
<Cell
|
||||
key={item.dictDataId || index}
|
||||
title={item.dictDataName || item.label || ''}
|
||||
extra={
|
||||
selectedFloor?.dictDataId === item.dictDataId ? (
|
||||
<Text className="text-sm text-blue-500">已选</Text>
|
||||
) : null
|
||||
}
|
||||
onClick={() => handleSelectFloor(item)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 房号选择弹出层 */}
|
||||
<Popup
|
||||
visible={showRoomPicker}
|
||||
position="bottom"
|
||||
round
|
||||
onClose={() => setShowRoomPicker(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={() => setShowRoomPicker(false)}>
|
||||
<Text className="text-sm text-blue-500">取消</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 搜索框 */}
|
||||
<View className="px-3 py-2">
|
||||
<SearchBar
|
||||
value={roomSearch}
|
||||
placeholder="搜索房号"
|
||||
onChange={handleRoomSearch}
|
||||
/>
|
||||
</View>
|
||||
{/* 列表 */}
|
||||
<View className="flex-1 overflow-y-auto">
|
||||
{roomLoading ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Loading>加载中</Loading>
|
||||
</View>
|
||||
) : roomList.length === 0 ? (
|
||||
<View className="flex justify-center items-center py-8">
|
||||
<Text className="text-sm text-gray-400">暂无房号数据</Text>
|
||||
</View>
|
||||
) : (
|
||||
roomList.map((item, index) => (
|
||||
<Cell
|
||||
key={item.dictDataId || index}
|
||||
title={item.dictDataName || item.label || ''}
|
||||
extra={
|
||||
selectedRoom?.dictDataId === item.dictDataId ? (
|
||||
<Text className="text-sm text-blue-500">已选</Text>
|
||||
) : null
|
||||
}
|
||||
onClick={() => handleSelectRoom(item)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 审核状态显示(仅在编辑模式下显示) */}
|
||||
{isEditMode && (
|
||||
<CellGroup>
|
||||
|
||||
@@ -465,6 +465,13 @@ const CustomerIndex = () => {
|
||||
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
|
||||
</View>
|
||||
|
||||
{/* 接待人员 */}
|
||||
{customer.receptionistName && (
|
||||
<View className="flex items-center my-1">
|
||||
<Text className="text-xs text-gray-500">接待人员:{customer.receptionistName}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 显示 comments 字段 */}
|
||||
<Space className="flex items-center">
|
||||
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
QrCode,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
People,
|
||||
Service
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {useThemeStyles} from '@/hooks/useTheme'
|
||||
@@ -250,6 +251,14 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</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')}>
|
||||
<View className="text-center">
|
||||
<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({
|
||||
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 {Button, Loading} from '@nutui/nutui-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 {generateInviteCode} from '@/api/invite'
|
||||
// import type {InviteStats} from '@/api/invite'
|
||||
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
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 [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 () => {
|
||||
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setCodeLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
|
||||
// 清空之前的二维码
|
||||
setMiniProgramCodeUrl('')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setCodeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
}, [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 () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
if (saving) return
|
||||
setSaving(true)
|
||||
Taro.showLoading({title: '保存中...'})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
const hasPermission = await ensureWriteAlbumPermission()
|
||||
if (!hasPermission) return
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
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({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
const errMsg = error?.errMsg || error?.message
|
||||
if (errMsg?.includes('cancel')) {
|
||||
Taro.showToast({title: '已取消', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAlbumAuthError(errMsg)) {
|
||||
const modal = await Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
confirmText: '去设置'
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
}
|
||||
} else {
|
||||
Taro.showToast({
|
||||
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||||
await Taro.showModal({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
content: errMsg || '保存失败,请稍后重试',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
|
||||
//
|
||||
// const inviteText = `🎉 邀请您加入我的团队!
|
||||
//
|
||||
// 扫描小程序码或搜索"九云售电云"小程序,即可享受优质商品和服务!
|
||||
// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
|
||||
//
|
||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
// 🎁 新用户专享优惠等你来拿
|
||||
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
|
||||
// })
|
||||
// }
|
||||
|
||||
if (!dealerUser) {
|
||||
if (dealerLoading) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<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 (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部卡片 */}
|
||||
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
|
||||
}}></View>
|
||||
|
||||
<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>
|
||||
</View>
|
||||
</View>
|
||||
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* 小程序码展示区 */}
|
||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||
<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">
|
||||
<Loading/>
|
||||
<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>
|
||||
<View className="text-sm text-gray-500 mb-4">
|
||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||
自购省 | 分享赚 | 好友惠
|
||||
</View>
|
||||
|
||||
|
||||
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
disabled={!miniProgramCodeUrl || codeLoading || saving}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
{/* 推广说明 */}
|
||||
|
||||
3
src/dealer/referral/index.config.ts
Normal file
3
src/dealer/referral/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"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>
|
||||
{/* 显示手机号(仅本级可见) */}
|
||||
{showPhone && member.phone && (
|
||||
<Text className="text-sm text-gray-500" onClick={(e) => {
|
||||
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
makePhoneCall(member.phone || '');
|
||||
}}>
|
||||
@@ -334,14 +334,9 @@ const DealerTeam: React.FC = () => {
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Space>
|
||||
<Text>
|
||||
<Text className="text-xs text-gray-500">UID:{member.userId}</Text>
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">
|
||||
加入时间:{member.joinTime}
|
||||
</Text>
|
||||
</Space>
|
||||
<Text className="text-xs text-gray-500">
|
||||
加入时间:{member.joinTime}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -46,9 +46,17 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
||||
setDealerUser(dealer)
|
||||
} else {
|
||||
setDealerUser(null)
|
||||
// 没有经销商记录,跳转到申请加入页面
|
||||
Taro.redirectTo({ url: '/dealer/apply/add' })
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
// 如果错误消息是"操作成功"(接口返回成功但无数据),也跳转到申请页面
|
||||
if (errorMessage === '操作成功' || errorMessage === '查询成功') {
|
||||
setDealerUser(null)
|
||||
Taro.redirectTo({ url: '/dealer/apply/add' })
|
||||
return
|
||||
}
|
||||
setError(errorMessage)
|
||||
setDealerUser(null)
|
||||
} finally {
|
||||
|
||||
@@ -16,7 +16,7 @@ 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/qrcode/index'
|
||||
path: '/dealer/customer/add'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -28,13 +28,13 @@ const menuList: MenuItem[] = [
|
||||
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/qrcode/index'
|
||||
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: '/user/wallet/wallet'
|
||||
path: '/pages/user/user'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {View} from '@tarojs/components'
|
||||
import Grid from "@/pages/index/Grid";
|
||||
import Banner from "./Banner";
|
||||
import PopUpAd from "@/pages/index/PopUpAd";
|
||||
@@ -17,6 +17,8 @@ import './index.scss'
|
||||
function Home() {
|
||||
const [config, setConfig] = useState<Config>()
|
||||
|
||||
console.log(config)
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '南南佐顿门窗 - 网宿软件',
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user