refactor(doctor): 重构医生模块为经销商模块并优化相关功能
- 将 doctor 目录重命名为 dealer 目录 - 更新页面标题从'会员注册'为'注册会员' - 删除银行卡管理、患者报备和订单消息功能 - 重命名组件 AddDoctor 为 AddUserAddress - 添加用户角色管理和默认角色写入逻辑 - 优化注册成功后跳转至用户中心页面 - 更新应用配置中的页面路径和子包结构 - 添加经销商资金管理、团队管理和二维码推广功能 - 更新租户信息配置,增加租户名称和版权信息 - 优化文章列表组件的类型定义和渲染方式 - 修复广告轮播图数据加载和图片兼容性问题
This commit is contained in:
209
README_QR_LOGIN.md
Normal file
209
README_QR_LOGIN.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 微信小程序扫码登录功能实现
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
- ✅ **完整的后端API** - Java Spring Boot实现
|
||||
- ✅ **多种前端集成方式** - 按钮、弹窗、页面
|
||||
- ✅ **智能二维码解析** - 支持URL、JSON、纯token格式
|
||||
- ✅ **安全可靠** - Token有效期控制,防重复使用
|
||||
- ✅ **用户体验优秀** - 实时状态反馈,错误处理完善
|
||||
- ✅ **微信深度集成** - 自动获取用户信息
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
### 后端 (Java)
|
||||
```
|
||||
auto/
|
||||
├── controller/QrLoginController.java # REST API控制器
|
||||
├── service/QrLoginService.java # 业务接口
|
||||
├── service/impl/QrLoginServiceImpl.java # 业务实现
|
||||
└── dto/ # 数据传输对象
|
||||
├── QrLoginData.java
|
||||
├── QrLoginConfirmRequest.java
|
||||
├── QrLoginStatusResponse.java
|
||||
└── QrLoginGenerateResponse.java
|
||||
```
|
||||
|
||||
### 前端 (小程序)
|
||||
```
|
||||
src/
|
||||
├── api/qr-login/index.ts # API接口层
|
||||
├── hooks/useQRLogin.ts # 业务逻辑Hook
|
||||
├── components/ # 组件层
|
||||
│ ├── QRLoginButton.tsx # 扫码按钮组件
|
||||
│ ├── QRLoginScanner.tsx # 扫码器组件
|
||||
│ ├── QRScanModal.tsx # 扫码弹窗组件
|
||||
│ └── QRLoginDemo.tsx # 演示组件
|
||||
└── pages/ # 页面层
|
||||
├── qr-login/index.tsx # 扫码登录页面
|
||||
├── qr-confirm/index.tsx # 登录确认页面
|
||||
└── qr-test/index.tsx # 功能测试页面
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 后端配置
|
||||
|
||||
确保Java后端服务正常运行,API接口可访问:
|
||||
- `POST /api/qr-login/generate` - 生成扫码token
|
||||
- `GET /api/qr-login/status/{token}` - 检查登录状态
|
||||
- `POST /api/qr-login/confirm` - 确认登录
|
||||
- `POST /api/qr-login/scan/{token}` - 扫码操作
|
||||
|
||||
### 2. 前端使用
|
||||
|
||||
#### 最简单的使用方式:
|
||||
```tsx
|
||||
import QRLoginButton from '@/components/QRLoginButton';
|
||||
|
||||
<QRLoginButton />
|
||||
```
|
||||
|
||||
#### 弹窗方式:
|
||||
```tsx
|
||||
import QRScanModal from '@/components/QRScanModal';
|
||||
|
||||
<QRScanModal
|
||||
visible={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSuccess={(result) => console.log('登录成功', result)}
|
||||
/>
|
||||
```
|
||||
|
||||
#### 页面跳转方式:
|
||||
```tsx
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
Taro.navigateTo({
|
||||
url: '/passport/qr-login/index'
|
||||
});
|
||||
```
|
||||
|
||||
## 🔧 支持的二维码格式
|
||||
|
||||
系统智能识别多种二维码格式:
|
||||
|
||||
1. **URL格式**:`https://mp.websoft.top/qr-confirm?qrCodeKey=token123`
|
||||
2. **JSON格式**:`{"token": "token123", "type": "qr-login"}`
|
||||
3. **简单格式**:`qr-login:token123` 或直接 `token123`
|
||||
|
||||
## 📱 页面说明
|
||||
|
||||
### 1. 扫码登录页面 (`/passport/qr-login/index`)
|
||||
- 完整的扫码登录功能
|
||||
- 用户信息显示
|
||||
- 登录历史记录
|
||||
- 使用说明和安全提示
|
||||
|
||||
### 2. 登录确认页面 (`/passport/qr-confirm/index`)
|
||||
- 处理二维码跳转确认
|
||||
- 支持URL参数:`qrCodeKey` 或 `token`
|
||||
- 用户确认界面
|
||||
|
||||
### 3. 功能测试页面 (`/passport/qr-test/index`)
|
||||
- 演示各种集成方式
|
||||
- 功能测试和调试
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
### 1. 添加扫码按钮到现有页面
|
||||
|
||||
```tsx
|
||||
import QRLoginButton from '@/components/QRLoginButton';
|
||||
|
||||
const MyPage = () => {
|
||||
return (
|
||||
<View>
|
||||
<QRLoginButton
|
||||
text="扫码登录"
|
||||
onSuccess={(result) => {
|
||||
// 处理登录成功
|
||||
console.log('用户登录成功:', result);
|
||||
}}
|
||||
onError={(error) => {
|
||||
// 处理登录失败
|
||||
console.error('登录失败:', error);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 自定义扫码逻辑
|
||||
|
||||
```tsx
|
||||
import { useQRLogin } from '@/hooks/useQRLogin';
|
||||
|
||||
const MyComponent = () => {
|
||||
const {
|
||||
startScan,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
result,
|
||||
error
|
||||
} = useQRLogin();
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={isLoading}
|
||||
onClick={startScan}
|
||||
>
|
||||
{isLoading ? '扫码中...' : '扫码登录'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 🔒 安全注意事项
|
||||
|
||||
1. **用户登录验证**:使用前确保用户已在小程序中登录
|
||||
2. **Token有效期**:二维码5分钟有效期,过期自动失效
|
||||
3. **权限申请**:确保小程序已申请摄像头权限
|
||||
4. **来源验证**:只扫描来自官方网站的登录二维码
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: 提示"请先登录小程序"
|
||||
A: 用户需要先在小程序中完成登录,获取用户ID和访问令牌。
|
||||
|
||||
### Q: 提示"无效的登录二维码"
|
||||
A: 检查二维码格式是否正确,或者二维码是否已过期。
|
||||
|
||||
### Q: 扫码失败
|
||||
A: 检查摄像头权限,确保二维码清晰可见。
|
||||
|
||||
### Q: 网络请求失败
|
||||
A: 检查网络连接和API接口地址配置。
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [详细使用指南](docs/QR_LOGIN_USAGE.md)
|
||||
- [API接口文档](src/api/qr-login/index.ts)
|
||||
- [组件API文档](docs/QR_LOGIN_USAGE.md#组件api)
|
||||
|
||||
## 🎉 测试功能
|
||||
|
||||
访问测试页面验证功能:
|
||||
```
|
||||
/pages/qr-test/index
|
||||
```
|
||||
|
||||
该页面包含所有集成方式的演示和测试功能。
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请检查:
|
||||
1. 后端API服务是否正常运行
|
||||
2. 小程序用户是否已登录
|
||||
3. 网络连接是否正常
|
||||
4. 二维码格式是否正确
|
||||
|
||||
---
|
||||
|
||||
**开发者**: 科技小王子
|
||||
**更新时间**: 2025-09-20
|
||||
@@ -2,9 +2,13 @@ import { API_BASE_URL } from './env'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '5';
|
||||
// 租户名称
|
||||
export const TenantName = '网宿软件';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
export const Version = 'v3.0.8';
|
||||
// 版权信息
|
||||
export const Copyright = 'WebSoft Inc.';
|
||||
|
||||
// java -jar CertificateDownloader.jar -k 0kF5OlPr482EZwtn9zGufUcqa7ovgxRL -m 1723321338 -f ./apiclient_key.pem -s 2B933F7C35014A1C363642623E4A62364B34C4EB -o ./
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
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',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: 'WebSoft Inc.',
|
||||
API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
APP_NAME: '网宿软件',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
||||
API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "template-5",
|
||||
"description": "WebSoft Inc.",
|
||||
"appid": "wx4cd177c96383371d",
|
||||
"description": "网宿软件",
|
||||
"appid": "wx541db955e7a62709",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "template-5",
|
||||
"description": "WebSoft Inc.",
|
||||
"projectname": "mp-react",
|
||||
"description": "网宿软件",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
|
||||
@@ -237,7 +237,7 @@ function UserCard() {
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<span className={'text-sm text-gray-500'}>礼品卡</span>
|
||||
<span className={'text-sm text-gray-500'}>水票</span>
|
||||
<span className={'text-xl'}>{giftCount}</span>
|
||||
</div>
|
||||
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
|
||||
|
||||
@@ -36,7 +36,7 @@ const UserCell = () => {
|
||||
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
|
||||
}}
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/doctor/index', true)}>
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>开通会员</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>享优惠</Text>
|
||||
|
||||
@@ -117,7 +117,7 @@ export const STATUS_COLOR_MAP = {
|
||||
// 申请售后
|
||||
export const applyAfterSale = async (params: AfterSaleApplyParams): Promise<AfterSaleDetailResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
const response = await request<AfterSaleDetailResponse>({
|
||||
url: '/api/after-sale/apply',
|
||||
method: 'POST',
|
||||
data: params
|
||||
@@ -136,7 +136,7 @@ export const getAfterSaleDetail = async (params: {
|
||||
afterSaleId?: string
|
||||
}): Promise<AfterSaleDetailResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
const response = await request<AfterSaleDetailResponse>({
|
||||
url: '/api/after-sale/detail',
|
||||
method: 'GET',
|
||||
data: params
|
||||
@@ -154,7 +154,7 @@ export const getAfterSaleDetail = async (params: {
|
||||
// 查询售后列表
|
||||
export const getAfterSaleList = async (params: AfterSaleListParams): Promise<AfterSaleListResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
const response = await request<AfterSaleListResponse>({
|
||||
url: '/api/after-sale/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
@@ -170,7 +170,7 @@ export const getAfterSaleList = async (params: AfterSaleListParams): Promise<Aft
|
||||
// 撤销售后申请
|
||||
export const cancelAfterSale = async (afterSaleId: string): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const response = await request({
|
||||
const response = await request<{ success: boolean; message?: string }>({
|
||||
url: '/api/after-sale/cancel',
|
||||
method: 'POST',
|
||||
data: { afterSaleId }
|
||||
@@ -312,10 +312,8 @@ export const getAfterSaleSteps = (type: AfterSaleType, status: AfterSaleStatus)
|
||||
|
||||
// 根据类型调整步骤
|
||||
if (type === 'return' || type === 'exchange') {
|
||||
baseSteps.splice(2, 1,
|
||||
{ title: '寄回商品', description: '请将商品寄回指定地址' },
|
||||
{ title: '处理中', description: '收到商品,正在处理' }
|
||||
)
|
||||
baseSteps.splice(2, 0, { title: '等待收货', description: '等待用户寄回商品' })
|
||||
baseSteps.splice(3, 0, { title: '确认收货', description: '商家确认收到退回商品' })
|
||||
}
|
||||
|
||||
return baseSteps
|
||||
|
||||
@@ -204,19 +204,6 @@ export async function getByIds(params?: CmsArticleParam) {
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code查询文章
|
||||
*/
|
||||
export async function getCmsArticleByCode(code: string) {
|
||||
const res = await request.get<ApiResult<CmsArticle>>(
|
||||
'/cms/cms-article/getByCode/' + code
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code查询文章
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsNavigation, CmsNavigationParam } from './model';
|
||||
import type {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
|
||||
/**
|
||||
* 分页查询网站导航记录表
|
||||
@@ -123,11 +122,12 @@ export async function getNavigationByPath(params: CmsNavigationParam) {
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据code查询导航
|
||||
*/
|
||||
export async function getByCode(code: string) {
|
||||
const res = await request.get<ApiResult<CmsArticle>>(
|
||||
const res = await request.get<ApiResult<CmsNavigation>>(
|
||||
'/cms/cms-navigation/getByCode/' + code
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
@@ -135,3 +135,4 @@ export async function getByCode(code: string) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,5 @@ export interface CmsNavigationParam extends PageParam {
|
||||
parentId?: number;
|
||||
hide?: number;
|
||||
model?: string;
|
||||
home?: number;
|
||||
position?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageParam } from '@/api';
|
||||
import type { PageParam } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 应用参数
|
||||
|
||||
@@ -111,7 +111,6 @@ export interface InviteRecordParam {
|
||||
*/
|
||||
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
|
||||
try {
|
||||
// return 'http://127.0.0.1:9200/api/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
||||
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
||||
// 由于接口直接返回图片buffer,我们直接构建完整的URL
|
||||
return `${BaseUrl}${url}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageParam } from '@/api';
|
||||
import type { PageParam } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 分销商申请记录表
|
||||
@@ -10,14 +10,6 @@ export interface ShopDealerApply {
|
||||
userId?: number;
|
||||
// 姓名
|
||||
realName?: string;
|
||||
// 分销商名称
|
||||
dealerName?: string;
|
||||
// 分销商编号
|
||||
dealerCode?: string;
|
||||
// 详细地址
|
||||
address?: string;
|
||||
// 金额
|
||||
money?: number;
|
||||
// 手机号
|
||||
mobile?: string;
|
||||
// 推荐人用户ID
|
||||
@@ -25,9 +17,7 @@ export interface ShopDealerApply {
|
||||
// 申请方式(10需后台审核 20无需审核)
|
||||
applyType?: number;
|
||||
// 申请时间
|
||||
applyTime?: string;
|
||||
// 签单时间
|
||||
contractTime?: string;
|
||||
applyTime?: number;
|
||||
// 审核状态 (10待审核 20审核通过 30驳回)
|
||||
applyStatus?: number;
|
||||
// 审核时间
|
||||
@@ -40,14 +30,6 @@ export interface ShopDealerApply {
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 过期时间
|
||||
expirationTime?: string;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 昵称
|
||||
nickName?: string;
|
||||
// 推荐人名称
|
||||
refereeName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,10 +37,7 @@ export interface ShopDealerApply {
|
||||
*/
|
||||
export interface ShopDealerApplyParam extends PageParam {
|
||||
applyId?: number;
|
||||
type?: number;
|
||||
dealerName?: string;
|
||||
mobile?: string;
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
applyStatus?: number; // 申请状态筛选 (10待审核 20审核通过 30驳回)
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 分销商提现银行卡
|
||||
*/
|
||||
export interface ShopDealerBank {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 分销商用户ID
|
||||
userId?: number;
|
||||
// 开户行名称
|
||||
bankName?: string;
|
||||
// 银行开户名
|
||||
bankAccount?: string;
|
||||
// 银行卡号
|
||||
bankCard?: string;
|
||||
// 申请状态 (10待审核 20审核通过 30驳回)
|
||||
applyStatus?: number;
|
||||
// 审核时间
|
||||
auditTime?: number;
|
||||
// 驳回原因
|
||||
rejectReason?: string;
|
||||
// 是否默认
|
||||
isDefault?: boolean;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 类型
|
||||
type?: string;
|
||||
// 名称
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现银行卡搜索条件
|
||||
*/
|
||||
export interface ShopDealerBankParam extends PageParam {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
isDefault?: boolean;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -31,5 +31,11 @@ export interface ShopDealerCapital {
|
||||
*/
|
||||
export interface ShopDealerCapitalParam extends PageParam {
|
||||
id?: number;
|
||||
// 仅查询当前分销商的收益/资金明细
|
||||
userId?: number;
|
||||
// 可选:按订单过滤
|
||||
orderId?: number;
|
||||
// 可选:资金流动类型过滤
|
||||
flowType?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface ShopDealerOrder {
|
||||
id?: number;
|
||||
// 买家用户ID
|
||||
userId?: number;
|
||||
nickname?: string;
|
||||
// 订单编号(部分接口会直接返回订单号字符串)
|
||||
orderNo?: string;
|
||||
// 订单ID
|
||||
orderId?: number;
|
||||
// 订单总金额(不含运费)
|
||||
@@ -47,5 +50,7 @@ export interface ShopDealerOrderParam extends PageParam {
|
||||
secondUserId?: number;
|
||||
thirdUserId?: number;
|
||||
userId?: number;
|
||||
// 数据权限/资源ID(通常传当前登录用户ID)
|
||||
resourceId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,21 @@ import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
|
||||
|
||||
// WeChat transfer v3: backend may return `package_info` for MiniProgram to open the
|
||||
// "confirm receipt" page via `wx.requestMerchantTransfer`.
|
||||
export type ShopDealerWithdrawCreateResult =
|
||||
| string
|
||||
| {
|
||||
package_info?: string;
|
||||
packageInfo?: string;
|
||||
[k: string]: any;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// When applyStatus=20, user can "receive" (WeChat confirm receipt flow).
|
||||
export type ShopDealerWithdrawReceiveResult = ShopDealerWithdrawCreateResult;
|
||||
|
||||
/**
|
||||
* 分页查询分销商提现明细表
|
||||
*/
|
||||
@@ -33,11 +48,40 @@ export async function listShopDealerWithdraw(params?: ShopDealerWithdrawParam) {
|
||||
/**
|
||||
* 添加分销商提现明细表
|
||||
*/
|
||||
export async function addShopDealerWithdraw(data: ShopDealerWithdraw) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise<ShopDealerWithdrawCreateResult> {
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
'/shop/shop-dealer-withdraw',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
// Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
|
||||
return res.data ?? res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
|
||||
*/
|
||||
export async function receiveShopDealerWithdraw(id: number): Promise<ShopDealerWithdrawReceiveResult> {
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
'/shop/shop-dealer-withdraw/receive/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data ?? res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
|
||||
*/
|
||||
export async function receiveSuccessShopDealerWithdraw(id: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-withdraw/receive-success/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 礼品卡
|
||||
* 水票
|
||||
*/
|
||||
export interface ShopGift {
|
||||
// 礼品卡ID
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import {OrderGoods} from "@/api/system/orderGoods/model";
|
||||
import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
|
||||
|
||||
/**
|
||||
* 订单
|
||||
@@ -27,6 +27,14 @@ export interface ShopOrder {
|
||||
merchantName?: string;
|
||||
// 商户编号
|
||||
merchantCode?: string;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 使用的优惠券id
|
||||
couponId?: number;
|
||||
// 使用的会员卡id
|
||||
@@ -61,6 +69,8 @@ export interface ShopOrder {
|
||||
sendStartTime?: string;
|
||||
// 配送结束时间
|
||||
sendEndTime?: string;
|
||||
// 配送员送达拍照(选填)
|
||||
sendEndImg?: string;
|
||||
// 发货店铺id
|
||||
expressMerchantId?: number;
|
||||
// 发货店铺
|
||||
@@ -146,7 +156,7 @@ export interface ShopOrder {
|
||||
// 是否已收到赠品
|
||||
hasTakeGift?: string;
|
||||
// 订单商品项
|
||||
orderGoods?: OrderGoods[];
|
||||
orderGoods?: ShopOrderGoods[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +175,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -197,6 +215,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -223,6 +249,12 @@ export interface ShopOrderParam extends PageParam {
|
||||
payType?: number;
|
||||
isInvoice?: boolean;
|
||||
userId?: number;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 配送员用户ID
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
keywords?: string;
|
||||
deliveryStatus?: number;
|
||||
statusFilter?: number;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopUser, ShopUserParam } from './model';
|
||||
import type { ShopStore, ShopStoreParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询用户记录表
|
||||
* 分页查询门店
|
||||
*/
|
||||
export async function pageShopUser(params: ShopUserParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopUser>>>(
|
||||
'/shop/shop-user/page',
|
||||
{
|
||||
export async function pageShopStore(params: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
|
||||
'/shop/shop-store/page',
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
@@ -19,14 +17,12 @@ export async function pageShopUser(params: ShopUserParam) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户记录表列表
|
||||
* 查询门店列表
|
||||
*/
|
||||
export async function listShopUser(params?: ShopUserParam) {
|
||||
const res = await request.get<ApiResult<ShopUser[]>>(
|
||||
'/shop/shop-user',
|
||||
{
|
||||
export async function listShopStore(params?: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<ShopStore[]>>(
|
||||
'/shop/shop-store',
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
@@ -35,11 +31,11 @@ export async function listShopUser(params?: ShopUserParam) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加用户记录表
|
||||
* 添加门店
|
||||
*/
|
||||
export async function addShopUser(data: ShopUser) {
|
||||
export async function addShopStore(data: ShopStore) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'http://127.0.0.1:9200/api/shop/shop-user',
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -49,11 +45,11 @@ export async function addShopUser(data: ShopUser) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户记录表
|
||||
* 修改门店
|
||||
*/
|
||||
export async function updateShopUser(data: ShopUser) {
|
||||
export async function updateShopStore(data: ShopStore) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-user',
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -63,11 +59,11 @@ export async function updateShopUser(data: ShopUser) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户记录表
|
||||
* 删除门店
|
||||
*/
|
||||
export async function removeShopUser(id?: number) {
|
||||
export async function removeShopStore(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-user/' + id
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
@@ -76,11 +72,11 @@ export async function removeShopUser(id?: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除用户记录表
|
||||
* 批量删除门店
|
||||
*/
|
||||
export async function removeBatchShopUser(data: (number | undefined)[]) {
|
||||
export async function removeBatchShopStore(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-user/batch',
|
||||
'/shop/shop-store/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -92,11 +88,11 @@ export async function removeBatchShopUser(data: (number | undefined)[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据userId查询用户记录表
|
||||
* 根据id查询门店
|
||||
*/
|
||||
export async function getShopUser(userId: number) {
|
||||
const res = await request.get<ApiResult<ShopUser>>(
|
||||
'/shop/shop-user/' + userId
|
||||
export async function getShopStore(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStore>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
63
src/api/shop/shopStore/model/index.ts
Normal file
63
src/api/shop/shopStore/model/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
export interface ShopStore {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 店铺名称
|
||||
name?: string;
|
||||
// 门店地址
|
||||
address?: string;
|
||||
// 手机号码
|
||||
phone?: string;
|
||||
// 邮箱
|
||||
email?: string;
|
||||
// 门店经理
|
||||
managerName?: string;
|
||||
// 门店banner
|
||||
shopBanner?: string;
|
||||
// 所在省份
|
||||
province?: string;
|
||||
// 所在城市
|
||||
city?: string;
|
||||
// 所在辖区
|
||||
region?: string;
|
||||
// 经度和纬度
|
||||
lngAndLat?: string;
|
||||
// 位置
|
||||
location?:string;
|
||||
// 区域
|
||||
district?: string;
|
||||
// 轮廓
|
||||
points?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 默认仓库ID(shop_warehouse.id)
|
||||
warehouseId?: number;
|
||||
// 默认仓库名称(可选)
|
||||
warehouseName?: string;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店搜索条件
|
||||
*/
|
||||
export interface ShopStoreParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopDealerBank, ShopDealerBankParam } from './model';
|
||||
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询分销商银行卡
|
||||
* 分页查询配送员
|
||||
*/
|
||||
export async function pageShopDealerBank(params: ShopDealerBankParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopDealerBank>>>(
|
||||
'/shop/shop-dealer-bank/page',
|
||||
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
|
||||
'/shop/shop-store-rider/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -17,11 +17,11 @@ export async function pageShopDealerBank(params: ShopDealerBankParam) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询分销商银行卡列表
|
||||
* 查询配送员列表
|
||||
*/
|
||||
export async function listShopDealerBank(params?: ShopDealerBankParam) {
|
||||
const res = await request.get<ApiResult<ShopDealerBank[]>>(
|
||||
'/shop/shop-dealer-bank',
|
||||
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreRider[]>>(
|
||||
'/shop/shop-store-rider',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
@@ -31,11 +31,11 @@ export async function listShopDealerBank(params?: ShopDealerBankParam) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分销商银行卡
|
||||
* 添加配送员
|
||||
*/
|
||||
export async function addShopDealerBank(data: ShopDealerBank) {
|
||||
export async function addShopStoreRider(data: ShopStoreRider) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank',
|
||||
'/shop/shop-store-rider',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -45,11 +45,11 @@ export async function addShopDealerBank(data: ShopDealerBank) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改分销商银行卡
|
||||
* 修改配送员
|
||||
*/
|
||||
export async function updateShopDealerBank(data: ShopDealerBank) {
|
||||
export async function updateShopStoreRider(data: ShopStoreRider) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank',
|
||||
'/shop/shop-store-rider',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -59,11 +59,11 @@ export async function updateShopDealerBank(data: ShopDealerBank) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分销商银行卡
|
||||
* 删除配送员
|
||||
*/
|
||||
export async function removeShopDealerBank(id?: number) {
|
||||
export async function removeShopStoreRider(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank/' + id
|
||||
'/shop/shop-store-rider/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
@@ -72,11 +72,11 @@ export async function removeShopDealerBank(id?: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除分销商银行卡
|
||||
* 批量删除配送员
|
||||
*/
|
||||
export async function removeBatchShopDealerBank(data: (number | undefined)[]) {
|
||||
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank/batch',
|
||||
'/shop/shop-store-rider/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -88,11 +88,11 @@ export async function removeBatchShopDealerBank(data: (number | undefined)[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询分销商银行卡
|
||||
* 根据id查询配送员
|
||||
*/
|
||||
export async function getShopDealerBank(id: number) {
|
||||
const res = await request.get<ApiResult<ShopDealerBank>>(
|
||||
'/shop/shop-dealer-bank/' + id
|
||||
export async function getShopStoreRider(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreRider>>(
|
||||
'/shop/shop-store-rider/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
67
src/api/shop/shopStoreRider/model/index.ts
Normal file
67
src/api/shop/shopStoreRider/model/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 配送员
|
||||
*/
|
||||
export interface ShopStoreRider {
|
||||
// 主键ID
|
||||
id?: string;
|
||||
// 配送点ID(shop_dealer.id)
|
||||
dealerId?: number;
|
||||
// 骑手编号(可选)
|
||||
riderNo?: string;
|
||||
// 姓名
|
||||
realName?: string;
|
||||
// 手机号
|
||||
mobile?: string;
|
||||
// 头像
|
||||
avatar?: string;
|
||||
// 身份证号(可选)
|
||||
idCardNo?: string;
|
||||
// 状态:1启用;0禁用
|
||||
status?: number;
|
||||
// 接单状态:0休息/下线;1在线;2忙碌
|
||||
workStatus?: number;
|
||||
// 是否开启自动派单:1是;0否
|
||||
autoDispatchEnabled?: number;
|
||||
// 派单优先级(同小区多骑手时可用,值越大越优先)
|
||||
dispatchPriority?: number;
|
||||
// 最大同时配送单数(0表示不限制)
|
||||
maxOnhandOrders?: number;
|
||||
// 是否计算工资(提成):1计算;0不计算(如三方配送点可设0)
|
||||
commissionCalcEnabled?: number;
|
||||
// 水每桶提成金额(元/桶)
|
||||
waterBucketUnitFee?: string;
|
||||
// 其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表)
|
||||
otherGoodsCommissionType?: number;
|
||||
// 其他商品提成值:固定金额(元)或比例(%)
|
||||
otherGoodsCommissionValue?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配送员搜索条件
|
||||
*/
|
||||
export interface ShopStoreRiderParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
// 配送点/门店ID(后端可能用 dealerId 或 storeId)
|
||||
dealerId?: number;
|
||||
storeId?: number;
|
||||
status?: number;
|
||||
workStatus?: number;
|
||||
autoDispatchEnabled?: number;
|
||||
}
|
||||
101
src/api/shop/shopStoreUser/index.ts
Normal file
101
src/api/shop/shopStoreUser/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStoreUser, ShopStoreUserParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询店员
|
||||
*/
|
||||
export async function pageShopStoreUser(params: ShopStoreUserParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
|
||||
'/shop/shop-store-user/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询店员列表
|
||||
*/
|
||||
export async function listShopStoreUser(params?: ShopStoreUserParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreUser[]>>(
|
||||
'/shop/shop-store-user',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加店员
|
||||
*/
|
||||
export async function addShopStoreUser(data: ShopStoreUser) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改店员
|
||||
*/
|
||||
export async function updateShopStoreUser(data: ShopStoreUser) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除店员
|
||||
*/
|
||||
export async function removeShopStoreUser(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除店员
|
||||
*/
|
||||
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询店员
|
||||
*/
|
||||
export async function getShopStoreUser(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreUser>>(
|
||||
'/shop/shop-store-user/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 店员
|
||||
*/
|
||||
export interface ShopStoreUser {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 配送点ID(shop_dealer.id)
|
||||
storeId?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店员搜索条件
|
||||
*/
|
||||
export interface ShopStoreUserParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
storeId?: number;
|
||||
userId?: number;
|
||||
isDelete?: number;
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 用户记录表
|
||||
*/
|
||||
export interface ShopUser {
|
||||
// 用户id
|
||||
userId?: number;
|
||||
// 用户类型 0个人用户 1企业用户 2其他
|
||||
type?: number;
|
||||
// 账号
|
||||
username?: string;
|
||||
// 密码
|
||||
password?: string;
|
||||
// 昵称
|
||||
nickname?: string;
|
||||
// 手机号
|
||||
phone?: string;
|
||||
// 性别 1男 2女
|
||||
sex?: number;
|
||||
// 职务
|
||||
position?: string;
|
||||
// 注册来源客户端 (APP、H5、MP-WEIXIN等)
|
||||
platform?: string;
|
||||
// 邮箱
|
||||
email?: string;
|
||||
// 邮箱是否验证, 0否, 1是
|
||||
emailVerified?: number;
|
||||
// 别名
|
||||
alias?: string;
|
||||
// 真实姓名
|
||||
realName?: string;
|
||||
// 证件号码
|
||||
idCard?: string;
|
||||
// 出生日期
|
||||
birthday?: string;
|
||||
// 所在国家
|
||||
country?: string;
|
||||
// 所在省份
|
||||
province?: string;
|
||||
// 所在城市
|
||||
city?: string;
|
||||
// 所在辖区
|
||||
region?: string;
|
||||
// 街道地址
|
||||
address?: string;
|
||||
// 经度
|
||||
longitude?: string;
|
||||
// 纬度
|
||||
latitude?: string;
|
||||
// 用户可用余额
|
||||
balance?: string;
|
||||
// 已提现金额
|
||||
cashedMoney?: string;
|
||||
// 用户可用积分
|
||||
points?: number;
|
||||
// 用户总支付的金额
|
||||
payMoney?: string;
|
||||
// 实际消费的金额(不含退款)
|
||||
expendMoney?: string;
|
||||
// 密码
|
||||
payPassword?: string;
|
||||
// 会员等级ID
|
||||
gradeId?: number;
|
||||
// 行业分类
|
||||
category?: string;
|
||||
// 个人简介
|
||||
introduction?: string;
|
||||
// 机构id
|
||||
organizationId?: number;
|
||||
// 会员分组ID
|
||||
groupId?: number;
|
||||
// 头像
|
||||
avatar?: string;
|
||||
// 背景图
|
||||
bgImage?: string;
|
||||
// 用户编码
|
||||
userCode?: string;
|
||||
// 是否已实名认证
|
||||
certification?: number;
|
||||
// 年龄
|
||||
age?: number;
|
||||
// 是否线下会员
|
||||
offline?: string;
|
||||
// 关注数
|
||||
followers?: number;
|
||||
// 粉丝数
|
||||
fans?: number;
|
||||
// 点赞数
|
||||
likes?: number;
|
||||
// 评论数
|
||||
commentNumbers?: number;
|
||||
// 是否推荐
|
||||
recommend?: number;
|
||||
// 微信openid
|
||||
openid?: string;
|
||||
// 微信公众号openid
|
||||
officeOpenid?: string;
|
||||
// 微信unionID
|
||||
unionid?: string;
|
||||
// 客户端ID
|
||||
clientId?: string;
|
||||
// 不允许办卡
|
||||
notAllowVip?: string;
|
||||
// 是否管理员
|
||||
isAdmin?: string;
|
||||
// 是否企业管理员
|
||||
isOrganizationAdmin?: string;
|
||||
// 累计登录次数
|
||||
loginNum?: number;
|
||||
// 企业ID
|
||||
companyId?: number;
|
||||
// 可管理的场馆
|
||||
merchants?: string;
|
||||
// 商户ID
|
||||
merchantId?: number;
|
||||
// 商户名称
|
||||
merchantName?: string;
|
||||
// 商户头像
|
||||
merchantAvatar?: string;
|
||||
// 第三方系统的用户ID
|
||||
uid?: number;
|
||||
// 专家角色
|
||||
expertType?: string;
|
||||
// 过期时间
|
||||
expireTime?: number;
|
||||
// 最后结算时间
|
||||
settlementTime?: string;
|
||||
// 资质
|
||||
aptitude?: string;
|
||||
// 行业类型(父级)
|
||||
industryParent?: string;
|
||||
// 行业类型(子级)
|
||||
industryChild?: string;
|
||||
// 头衔
|
||||
title?: string;
|
||||
// 安装的产品ID
|
||||
templateId?: number;
|
||||
// 插件安装状态(仅对超超管判断) 0未安装 1已安装
|
||||
installed?: number;
|
||||
// 特长
|
||||
speciality?: string;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 状态, 0在线, 1离线
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 注册时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 上传证件1
|
||||
uploadImg1?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户记录表搜索条件
|
||||
*/
|
||||
export interface ShopUserParam extends PageParam {
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 收货地址
|
||||
|
||||
101
src/api/shop/shopWarehouse/index.ts
Normal file
101
src/api/shop/shopWarehouse/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopWarehouse, ShopWarehouseParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询仓库
|
||||
*/
|
||||
export async function pageShopWarehouse(params: ShopWarehouseParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopWarehouse>>>(
|
||||
'/shop/shop-warehouse/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仓库列表
|
||||
*/
|
||||
export async function listShopWarehouse(params?: ShopWarehouseParam) {
|
||||
const res = await request.get<ApiResult<ShopWarehouse[]>>(
|
||||
'/shop/shop-warehouse',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加仓库
|
||||
*/
|
||||
export async function addShopWarehouse(data: ShopWarehouse) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改仓库
|
||||
*/
|
||||
export async function updateShopWarehouse(data: ShopWarehouse) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除仓库
|
||||
*/
|
||||
export async function removeShopWarehouse(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除仓库
|
||||
*/
|
||||
export async function removeBatchShopWarehouse(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询仓库
|
||||
*/
|
||||
export async function getShopWarehouse(id: number) {
|
||||
const res = await request.get<ApiResult<ShopWarehouse>>(
|
||||
'/shop/shop-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
53
src/api/shop/shopWarehouse/model/index.ts
Normal file
53
src/api/shop/shopWarehouse/model/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 仓库
|
||||
*/
|
||||
export interface ShopWarehouse {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 仓库名称
|
||||
name?: string;
|
||||
// 唯一标识
|
||||
code?: string;
|
||||
// 类型 中心仓,区域仓,门店仓
|
||||
type?: string;
|
||||
// 仓库地址
|
||||
address?: string;
|
||||
// 真实姓名
|
||||
realName?: string;
|
||||
// 联系电话
|
||||
phone?: string;
|
||||
// 省份
|
||||
province?: string;
|
||||
// 城市
|
||||
city: undefined,
|
||||
// 区域
|
||||
region: undefined,
|
||||
// 经纬度
|
||||
lngAndLat?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库搜索条件
|
||||
*/
|
||||
export interface ShopWarehouseParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import {SERVER_API_URL} from "@/utils/server";
|
||||
export async function listDictionaries(params?: DictParam) {
|
||||
const res = await request.get<ApiResult<Dict[]>>(
|
||||
SERVER_API_URL + '/system/dict',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
|
||||
@@ -30,3 +30,18 @@ export async function updateUserRole(data: UserRole) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户角色
|
||||
* 说明:部分后端实现为 POST 新增、PUT 修改;这里补齐 API 以便新用户无角色时可以创建默认角色。
|
||||
*/
|
||||
export async function addUserRole(data: UserRole) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/system/user-role',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,15 @@ export interface UserOrderStats {
|
||||
total: number
|
||||
}
|
||||
|
||||
// 用户卡片统计(个人中心头部:余额/积分/优惠券/水票)
|
||||
export interface UserCardStats {
|
||||
balance: string
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
lastUpdateTime?: string
|
||||
}
|
||||
|
||||
// 用户完整数据
|
||||
export interface UserDashboard {
|
||||
balance: UserBalance
|
||||
@@ -108,6 +117,17 @@ export async function getUserOrderStats() {
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户卡片统计(一次性返回余额/积分/可用优惠券/未使用礼品卡数量)
|
||||
*/
|
||||
export async function getUserCardStats() {
|
||||
const res = await request.get<ApiResult<UserCardStats>>('/user/card/stats')
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data
|
||||
}
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户完整仪表板数据(一次性获取所有数据)
|
||||
*/
|
||||
|
||||
@@ -4,14 +4,13 @@ export default {
|
||||
'pages/cart/cart',
|
||||
'pages/find/find',
|
||||
'pages/user/user',
|
||||
'pages/category/category'
|
||||
'pages/category/index'
|
||||
],
|
||||
"subpackages": [
|
||||
{
|
||||
"root": "passport",
|
||||
"pages": [
|
||||
"login",
|
||||
"register",
|
||||
"forget",
|
||||
"setting",
|
||||
"agreement",
|
||||
@@ -34,12 +33,6 @@ export default {
|
||||
"index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "gift",
|
||||
"pages": [
|
||||
"index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "user",
|
||||
"pages": [
|
||||
@@ -63,7 +56,9 @@ export default {
|
||||
"gift/index",
|
||||
"gift/redeem",
|
||||
"gift/detail",
|
||||
"gift/add",
|
||||
"store/verification",
|
||||
"store/orders/index",
|
||||
"theme/index",
|
||||
"poster/poster",
|
||||
"chat/conversation/index",
|
||||
@@ -73,20 +68,17 @@ export default {
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "doctor",
|
||||
"root": "dealer",
|
||||
"pages": [
|
||||
"index",
|
||||
"apply/add",
|
||||
"withdraw/index",
|
||||
"orders/index",
|
||||
"orders/add",
|
||||
"capital/index",
|
||||
"team/index",
|
||||
"qrcode/index",
|
||||
"invite-stats/index",
|
||||
"info",
|
||||
"customer/index",
|
||||
"customer/add",
|
||||
"customer/trading",
|
||||
"info"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -97,7 +89,21 @@ export default {
|
||||
'goodsDetail/index',
|
||||
'orderConfirm/index',
|
||||
'orderConfirmCart/index',
|
||||
'search/index'
|
||||
'comments/index',
|
||||
'search/index']
|
||||
},
|
||||
{
|
||||
"root": "store",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "rider",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -126,12 +132,6 @@ export default {
|
||||
selectedIconPath: "assets/tabbar/home-active.png",
|
||||
text: "首页",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/category/category",
|
||||
iconPath: "assets/tabbar/category.png",
|
||||
selectedIconPath: "assets/tabbar/category-active.png",
|
||||
text: "分类",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/cart/cart",
|
||||
iconPath: "assets/tabbar/cart.png",
|
||||
@@ -154,6 +154,9 @@ export default {
|
||||
permission: {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
},
|
||||
"scope.writePhotosAlbum": {
|
||||
"desc": "用于保存小程序码到相册,方便分享给好友"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -1,25 +1,16 @@
|
||||
import {Image, Cell} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
|
||||
const ArticleList = (props: any) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'px-3'}>
|
||||
{props.data.map((item: any, index: number) => {
|
||||
<div className={'px-3'}>
|
||||
{props.data.map((item: CmsArticle, index: number) => {
|
||||
return (
|
||||
<Cell
|
||||
title={
|
||||
<View>
|
||||
<View className="text-base font-medium mb-1">{item.title}</View>
|
||||
{item.comments && (
|
||||
<Text className="text-sm text-gray-500 leading-relaxed">
|
||||
{item.comments}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
title={item.title}
|
||||
extra={
|
||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
|
||||
}
|
||||
@@ -28,7 +19,7 @@ const ArticleList = (props: any) => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function Category() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_WebSoft Inc.`,
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
|
||||
@@ -17,7 +17,7 @@ function Detail() {
|
||||
const reload = async () => {
|
||||
const item = await getCmsArticle(Number(params.id))
|
||||
|
||||
if (item) {
|
||||
if (item && item.content) {
|
||||
item.content = wxParse(item.content)
|
||||
setItem(item)
|
||||
Taro.setNavigationBarTitle({
|
||||
@@ -43,6 +43,10 @@ function Detail() {
|
||||
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
|
||||
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
|
||||
<View className={'content p-4'}>
|
||||
{/*如果有视频就显示视频 视频沾满宽度*/}
|
||||
{item?.video && <View className={'w-full'}>
|
||||
<video src={item?.video} controls={true} width={'100%'}></video>
|
||||
</View>}
|
||||
<RichText nodes={item?.content}/>
|
||||
</View>
|
||||
<Line height={44}/>
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GiftCardProps {
|
||||
faceValue?: string
|
||||
/** 商品原价 */
|
||||
originalPrice?: string
|
||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
/** 礼品卡类型:10-礼品劵 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
type?: number
|
||||
/** 状态:0-未使用 1-已使用 2-失效 */
|
||||
status?: number
|
||||
@@ -112,10 +112,10 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
|
||||
title: '礼品卡类型说明',
|
||||
icon: <Gift size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'🎁 实物礼品卡:需到指定地址领取商品',
|
||||
'🎁 礼品劵:需到指定地址领取商品',
|
||||
'💻 虚拟礼品卡:自动发放到账户余额',
|
||||
'🛎️ 服务礼品卡:联系客服预约服务',
|
||||
'⏰ 注意查看有效期,过期无法使用'
|
||||
|
||||
@@ -28,10 +28,10 @@ const GiftCardShare: React.FC<GiftCardShareProps> = ({
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (giftCard.type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +1,162 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Image, Tabs, Empty, Sticky} from '@nutui/nutui-react-taro'
|
||||
import {Share} from '@nutui/icons-react-taro'
|
||||
import {View, Text} from '@tarojs/components';
|
||||
import Taro from "@tarojs/taro";
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {pageShopGoods} from "@/api/shop/shopGoods";
|
||||
import {Avatar, Cell, Space, Tabs, Button, TabPane, Swiper} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState, CSSProperties, useRef} from "react";
|
||||
import {BszxPay} from "@/api/bszx/bszxPay/model";
|
||||
import {InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import dayjs from "dayjs";
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
import {copyText} from "@/utils/common";
|
||||
|
||||
const BestSellers = (props: {onStickyChange?: (isSticky: boolean) => void}) => {
|
||||
const [tab1value, setTab1value] = useState<string | number>('0')
|
||||
const [list, setList] = useState<ShopGoods[]>([])
|
||||
const [goods, setGoods] = useState<ShopGoods>()
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
const InfiniteUlStyle: CSSProperties = {
|
||||
marginTop: '84px',
|
||||
height: '82vh',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}
|
||||
const tabs = [
|
||||
{
|
||||
index: 0,
|
||||
key: '全部',
|
||||
title: '全部'
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
key: '已上架',
|
||||
title: '已上架'
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
key: '已下架',
|
||||
title: '已下架'
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
key: '已售罄',
|
||||
title: '已售罄'
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
key: '警戒库存',
|
||||
title: '警戒库存'
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
key: '回收站',
|
||||
title: '回收站'
|
||||
},
|
||||
]
|
||||
|
||||
const reload = () => {
|
||||
pageShopGoods({}).then(res => {
|
||||
setList(res?.list || []);
|
||||
function GoodsList(props: any) {
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const swiperRef = useRef<React.ElementRef<typeof Swiper> | null>(null)
|
||||
const [tabIndex, setTabIndex] = useState<string | number>(0)
|
||||
|
||||
console.log(props.statusBarHeight, 'ppp')
|
||||
const reload = async () => {
|
||||
pageShopOrder({page}).then(res => {
|
||||
let newList: BszxPay[] | undefined = []
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
newList = list?.concat(res.list)
|
||||
setHasMore(true)
|
||||
} else {
|
||||
newList = res?.list
|
||||
setHasMore(false)
|
||||
}
|
||||
setList(newList || []);
|
||||
})
|
||||
}
|
||||
|
||||
// 处理分享点击
|
||||
const handleShare = (item: ShopGoods) => {
|
||||
setGoods(item);
|
||||
|
||||
console.log(goods)
|
||||
|
||||
// 显示分享选项菜单
|
||||
Taro.showActionSheet({
|
||||
itemList: ['分享给好友'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
// 分享给好友 - 触发转发
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
// 提示用户点击右上角分享
|
||||
Taro.showToast({
|
||||
title: '请点击右上角分享给好友',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('显示分享菜单失败', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理粘性布局状态变化
|
||||
const onStickyChange = (isSticky: boolean) => {
|
||||
setStickyStatus(isSticky)
|
||||
// 通知父组件粘性状态变化
|
||||
props.onStickyChange?.(isSticky)
|
||||
console.log('Tabs 粘性状态:', isSticky ? '已固定' : '取消固定')
|
||||
}
|
||||
|
||||
// 获取小程序系统信息
|
||||
const getSystemInfo = () => {
|
||||
const systemInfo = Taro.getSystemInfoSync()
|
||||
// 状态栏高度 + 导航栏高度 (一般为44px)
|
||||
return (systemInfo.statusBarHeight || 0) + 44
|
||||
const reloadMore = async () => {
|
||||
setPage(page + 1)
|
||||
reload().then();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
setPage(2)
|
||||
reload().then()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'py-3'} style={{paddingTop: '0'}}>
|
||||
{/* Tabs粘性布局组件 */}
|
||||
<Sticky
|
||||
threshold={getSystemInfo()}
|
||||
onChange={onStickyChange}
|
||||
style={{
|
||||
zIndex: 999,
|
||||
backgroundColor: stickyStatus ? '#ffffff' : 'transparent',
|
||||
boxShadow: stickyStatus ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
marginTop: stickyStatus ? '0' : '-12px'
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={tab1value}
|
||||
className={'w-full'}
|
||||
onChange={(value) => {
|
||||
setTab1value(value)
|
||||
align={'left'}
|
||||
className={'fixed left-0'}
|
||||
style={{ top: '84px'}}
|
||||
value={tabIndex}
|
||||
onChange={(page) => {
|
||||
swiperRef.current?.to(page)
|
||||
setTabIndex(page)
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingTop: stickyStatus ? '0' : '8px',
|
||||
paddingBottom: stickyStatus ? '8px' : '0',
|
||||
}}
|
||||
activeType="smile"
|
||||
>
|
||||
<Tabs.TabPane title="今日主推">
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="即将到期">
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="活动预告">
|
||||
</Tabs.TabPane>
|
||||
{
|
||||
tabs?.map((item, index) => {
|
||||
return <TabPane key={index} title={item.title}></TabPane>
|
||||
})
|
||||
}
|
||||
</Tabs>
|
||||
</Sticky>
|
||||
<div style={InfiniteUlStyle} id="scroll">
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
|
||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2 mt-2'}>
|
||||
{/* 今日主推 */}
|
||||
{tab1value == '0' && list?.map((item, index) => {
|
||||
return (
|
||||
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
|
||||
radius="10px 10px 0 0" height="180"
|
||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
||||
<View>
|
||||
<View className={'car-no text-sm'}>{item.name}</View>
|
||||
<View className={'flex justify-between text-xs py-1'}>
|
||||
<Text className={'text-orange-500'}>{item.comments}</Text>
|
||||
<Text className={'text-gray-400'}>已售 {item.sales}</Text>
|
||||
</View>
|
||||
<View className={'flex justify-between items-center py-2'}>
|
||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
||||
<Text className={'text-xs'}>¥</Text>
|
||||
<Text className={'font-bold text-2xl'}>{item.price}</Text>
|
||||
</View>
|
||||
<View className={'buy-btn'}>
|
||||
<View className={'cart-icon items-center hidden'}>
|
||||
<View
|
||||
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
|
||||
onClick={() => handleShare(item)}
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
<>
|
||||
没有更多了
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Share size={20}/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className={'text-white pl-4 pr-5'}
|
||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{list?.map(item => {
|
||||
return (
|
||||
<Cell style={{padding: '16px'}}>
|
||||
<Space direction={'vertical'} className={'w-full flex flex-col'}>
|
||||
<div className={'order-no flex justify-between'}>
|
||||
<span className={'text-gray-700 font-bold text-sm'}
|
||||
onClick={() => copyText(`${item.orderNo}`)}>{item.orderNo}</span>
|
||||
<span className={'text-orange-500'}>待付款</span>
|
||||
</div>
|
||||
<div
|
||||
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
|
||||
<div className={'goods-info'}>
|
||||
<div className={'flex items-center'}>
|
||||
<div className={'flex items-center'}>
|
||||
<Avatar
|
||||
src='34'
|
||||
size={'45'}
|
||||
shape={'square'}
|
||||
/>
|
||||
<div className={'ml-2'}>{item.realName}</div>
|
||||
</div>
|
||||
<div className={'text-gray-400 text-xs'}>{item.totalNum}件商品</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={' w-full text-right'}>实付金额:¥{item.payPrice}</div>
|
||||
<Space className={'btn flex justify-end'}>
|
||||
<Button size={'small'}>取消订单</Button>
|
||||
<Button size={'small'}>发货</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* 即将到期 */}
|
||||
{tab1value == '1' && (
|
||||
<Empty
|
||||
size={'small'}
|
||||
description="暂无即将到期的商品"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 活动预告 */}
|
||||
{tab1value == '2' && (
|
||||
<Empty
|
||||
size={'small'}
|
||||
description="暂无活动预告"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default BestSellers
|
||||
|
||||
export default GoodsList
|
||||
|
||||
@@ -81,7 +81,7 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
|
||||
{qrContent ? (
|
||||
<View className={'flex flex-col justify-center'}>
|
||||
<img
|
||||
src={`https://cms-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
|
||||
src={`https://mp-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
|
||||
alt="二维码"
|
||||
style={{width: '200px', height: '200px'}}
|
||||
className="mx-auto"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '会员注册',
|
||||
navigationBarTitleText: '注册会员',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -10,7 +10,9 @@ import {updateUser} from "@/api/system/user";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
||||
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
import {listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||
import {addUserRole, listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||
import { listRoles } from "@/api/system/role";
|
||||
import type { UserRole } from "@/api/system/userRole/model";
|
||||
|
||||
// 类型定义
|
||||
interface ChooseAvatarEvent {
|
||||
@@ -25,8 +27,8 @@ interface InputEvent {
|
||||
};
|
||||
}
|
||||
|
||||
const AddDoctor = () => {
|
||||
const {user, loginUser} = useUser()
|
||||
const AddUserAddress = () => {
|
||||
const {user, loginUser, fetchUserInfo} = useUser()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<User>()
|
||||
const formRef = useRef<any>(null)
|
||||
@@ -127,7 +129,7 @@ const AddDoctor = () => {
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
const submitSucceed = async (values: User) => {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!values.phone && !FormData?.phone) {
|
||||
@@ -176,12 +178,27 @@ const AddDoctor = () => {
|
||||
}
|
||||
console.log(values,FormData)
|
||||
|
||||
const roles = await listUserRole({userId: user?.userId})
|
||||
if (!user?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息缺失,请先登录',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let roles: UserRole[] = [];
|
||||
try {
|
||||
roles = await listUserRole({userId: user.userId})
|
||||
console.log(roles, 'roles...')
|
||||
} catch (e) {
|
||||
// 新用户/权限限制时可能查不到角色列表,不影响基础注册流程
|
||||
console.warn('查询用户角色失败,将尝试直接写入默认角色:', e)
|
||||
roles = []
|
||||
}
|
||||
|
||||
// 准备提交的数据
|
||||
await updateUser({
|
||||
userId: user?.userId,
|
||||
userId: user.userId,
|
||||
nickname: values.realName || FormData?.nickname,
|
||||
phone: values.phone || FormData?.phone,
|
||||
avatar: values.avatar || FormData?.avatar,
|
||||
@@ -189,17 +206,52 @@ const AddDoctor = () => {
|
||||
});
|
||||
|
||||
await addShopDealerUser({
|
||||
userId: user?.userId,
|
||||
userId: user.userId,
|
||||
realName: values.realName || FormData?.nickname,
|
||||
mobile: values.phone || FormData?.phone,
|
||||
refereeId: values.refereeId || FormData?.refereeId
|
||||
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
|
||||
})
|
||||
|
||||
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 user 角色
|
||||
try {
|
||||
// 1) 先尝试通过 roleCode=user 查询角色ID(避免硬编码)
|
||||
// 2) 取不到就回退到旧的默认ID(1848)
|
||||
let userRoleId: number | undefined;
|
||||
try {
|
||||
// 注意:当前 request.get 的封装不支持 axios 风格的 { params: ... },
|
||||
// 某些自动生成的 API 可能无法按参数过滤;这里直接取全量再本地查找更稳。
|
||||
const roleList = await listRoles();
|
||||
userRoleId = roleList?.find(r => r.roleCode === 'user')?.roleId;
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
if (!userRoleId) userRoleId = 1848;
|
||||
|
||||
const baseRolePayload = {
|
||||
userId: user.userId,
|
||||
tenantId: Number(TenantId),
|
||||
roleId: userRoleId
|
||||
};
|
||||
|
||||
// 后端若已创建 user-role 记录则更新;否则尝试“无id更新”触发创建(多数实现会 upsert)
|
||||
if (roles.length > 0) {
|
||||
await updateUserRole({
|
||||
...roles[0],
|
||||
roleId: 1848
|
||||
})
|
||||
roleId: userRoleId
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await addUserRole(baseRolePayload);
|
||||
} catch (_) {
|
||||
// 兼容后端仅支持 PUT upsert 的情况
|
||||
await updateUserRole(baseRolePayload);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新一次用户信息,确保 roles 写回本地缓存,避免“我的”页显示为空/不一致
|
||||
await fetchUserInfo();
|
||||
} catch (e) {
|
||||
console.warn('写入默认角色失败(不影响注册成功):', e)
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +261,8 @@ const AddDoctor = () => {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
|
||||
Taro.switchTab({ url: '/pages/user/user' });
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
@@ -382,9 +435,9 @@ const AddDoctor = () => {
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="邀请人ID" disabled={true}/>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
{/* <Input placeholder="邀请人ID" disabled={false}/>*/}
|
||||
{/*</Form.Item>*/}
|
||||
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
||||
<View className="flex items-center justify-between">
|
||||
<Input
|
||||
@@ -430,4 +483,4 @@ const AddDoctor = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDoctor;
|
||||
export default AddUserAddress;
|
||||
4
src/dealer/capital/index.config.ts
Normal file
4
src/dealer/capital/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '收益明细'
|
||||
})
|
||||
|
||||
2
src/dealer/capital/index.scss
Normal file
2
src/dealer/capital/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Intentionally empty: styling is done via utility classes. */
|
||||
|
||||
199
src/dealer/capital/index.tsx
Normal file
199
src/dealer/capital/index.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {View, Text, ScrollView} from '@tarojs/components'
|
||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {pageShopDealerCapital} from '@/api/shop/shopDealerCapital'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const DealerCapital: React.FC = () => {
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [records, setRecords] = useState<ShopDealerCapital[]>([])
|
||||
|
||||
const getFlowTypeText = (flowType?: number) => {
|
||||
switch (flowType) {
|
||||
case 10:
|
||||
return '佣金收入'
|
||||
case 20:
|
||||
return '提现支出'
|
||||
case 30:
|
||||
return '转账支出'
|
||||
case 40:
|
||||
return '转账收入'
|
||||
default:
|
||||
return '资金变动'
|
||||
}
|
||||
}
|
||||
|
||||
const getFlowTypeTag = (flowType?: number) => {
|
||||
// 收入:success;支出:danger;其它:default
|
||||
if (flowType === 10 || flowType === 40) return 'success'
|
||||
if (flowType === 20 || flowType === 30) return 'danger'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const formatMoney = (flowType?: number, money?: string) => {
|
||||
const isIncome = flowType === 10 || flowType === 40
|
||||
const isExpense = flowType === 20 || flowType === 30
|
||||
const sign = isIncome ? '+' : isExpense ? '-' : ''
|
||||
return `${sign}${money || '0.00'}`
|
||||
}
|
||||
|
||||
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true)
|
||||
} else if (page === 1) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
|
||||
const result = await pageShopDealerCapital({
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
// 只显示与当前登录用户相关的收益明细
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
const list = result?.list || []
|
||||
if (page === 1) {
|
||||
setRecords(list)
|
||||
} else {
|
||||
setRecords(prev => [...prev, ...list])
|
||||
}
|
||||
|
||||
setHasMore(list.length === PAGE_SIZE)
|
||||
setCurrentPage(page)
|
||||
} catch (error) {
|
||||
console.error('获取收益明细失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取收益明细失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await fetchRecords(1, true)
|
||||
}
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (!loadingMore && hasMore) {
|
||||
await fetchRecords(currentPage + 1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchRecords(1)
|
||||
}
|
||||
}, [fetchRecords, dealerUser?.userId])
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshing}
|
||||
pullingText="下拉刷新"
|
||||
canReleaseText="释放刷新"
|
||||
refreshingText="刷新中..."
|
||||
completeText="刷新完成"
|
||||
>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className="h-screen"
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={50}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading && records.length === 0 ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : records.length > 0 ? (
|
||||
<>
|
||||
{records.map((item) => (
|
||||
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
{item.describe || '收益明细'}
|
||||
</Text>
|
||||
<Tag type={getFlowTypeTag(item.flowType)}>
|
||||
{getFlowTypeText(item.flowType)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center mb-1">
|
||||
<Text className="text-sm text-gray-400">
|
||||
佣金收入
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-sm font-semibold ${
|
||||
item.flowType === 10 || item.flowType === 40 ? 'text-green-600' :
|
||||
item.flowType === 20 || item.flowType === 30 ? 'text-red-500' :
|
||||
'text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{formatMoney(item.flowType, item.money)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
{/*用户:{item.userId ?? '-'}*/}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{item.createTime || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{loadingMore && (
|
||||
<View className="text-center py-4">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
||||
</View>
|
||||
)}
|
||||
{!hasMore && records.length > 0 && (
|
||||
<View className="text-center py-4">
|
||||
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无收益明细"/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerCapital
|
||||
3
src/dealer/index.config.ts
Normal file
3
src/dealer/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销中心'
|
||||
})
|
||||
@@ -3,18 +3,15 @@ import {View, Text} from '@tarojs/components'
|
||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
User,
|
||||
UserAdd,
|
||||
Edit,
|
||||
Comment,
|
||||
QrCode,
|
||||
Notice,
|
||||
Orderlist,
|
||||
Health,
|
||||
PickedUp
|
||||
Shopping,
|
||||
Dongdong,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
import {gradientUtils} from '@/styles/gradients'
|
||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerIndex: React.FC = () => {
|
||||
@@ -33,10 +30,10 @@ const DealerIndex: React.FC = () => {
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
// const formatMoney = (money?: string) => {
|
||||
// if (!money) return '0.00'
|
||||
// return parseFloat(money).toFixed(2)
|
||||
// }
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
@@ -58,18 +55,18 @@ const DealerIndex: React.FC = () => {
|
||||
|
||||
console.log(getGradientBackground(),'getGradientBackground()')
|
||||
|
||||
// if (error) {
|
||||
// return (
|
||||
// <View className="p-4">
|
||||
// <View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
// <Text className="text-red-600">{error}</Text>
|
||||
// </View>
|
||||
// <Button type="primary" onClick={refresh}>
|
||||
// 重试
|
||||
// </Button>
|
||||
// </View>
|
||||
// )
|
||||
// }
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<Text className="text-red-600">{error}</Text>
|
||||
</View>
|
||||
<Button type="primary" onClick={refresh}>
|
||||
重试
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
@@ -106,12 +103,12 @@ const DealerIndex: React.FC = () => {
|
||||
<View className="flex-1 flex-col">
|
||||
<View className="text-white text-lg font-bold mb-1" style={{
|
||||
}}>
|
||||
{dealerUser?.realName || '医生名称'}
|
||||
{dealerUser?.realName || '分销商'}
|
||||
</View>
|
||||
<View className="text-sm" style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}}>
|
||||
医生编号: {dealerUser.userId}
|
||||
ID: {dealerUser.userId}
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-right hidden">
|
||||
@@ -128,9 +125,80 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 佣金统计卡片 */}
|
||||
{dealerUser && (
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-3">
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.money)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.freezeMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.totalMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 团队统计 */}
|
||||
{dealerUser && (
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="font-semibold text-gray-800">我的邀请</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm flex items-center"
|
||||
onClick={() => navigateToPage('/dealer/team/index')}
|
||||
>
|
||||
<Text>查看详情</Text>
|
||||
<ArrowRight size="12"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-purple-500 mb-1">
|
||||
{dealerUser.firstNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">一级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-indigo-500 mb-1">
|
||||
{dealerUser.secondNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">二级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-pink-500 mb-1">
|
||||
{dealerUser.thirdNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">三级成员</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 功能导航 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="font-semibold mb-4 text-gray-800">管理工具</View>
|
||||
<View className="font-semibold mb-4 text-gray-800">分销工具</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
columns={4}
|
||||
@@ -141,70 +209,37 @@ const DealerIndex: React.FC = () => {
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="患者管理" onClick={() => navigateToPage('/doctor/customer/index')}>
|
||||
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<PickedUp color="#3b82f6" size="20"/>
|
||||
<Shopping color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'在线开方'} onClick={() => navigateToPage('/doctor/orders/add')}>
|
||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Edit color="#10b981" size="20"/>
|
||||
<Purse color="#10b981" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'咨询管理'} onClick={() => navigateToPage('/doctor/team/index')}>
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Comment color="#8b5cf6" size="20"/>
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'处方管理'} onClick={() => navigateToPage('/doctor/orders/index')}>
|
||||
<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">
|
||||
<Orderlist color="#f59e0b" size="20"/>
|
||||
<Dongdong color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'复诊提醒'}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Notice size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/doctor/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<UserAdd size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/doctor/qrcode/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<QrCode size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'医生认证'} onClick={() => navigateToPage('/doctor/apply/add')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Health size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
</Grid>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
@@ -217,7 +252,7 @@ const DealerIndex: React.FC = () => {
|
||||
{/* border: 'none'*/}
|
||||
{/* } as React.CSSProperties}*/}
|
||||
{/*>*/}
|
||||
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/doctor/invite-stats/index')}>*/}
|
||||
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <Presentation color="#6366f1" size="20"/>*/}
|
||||
@@ -15,7 +15,7 @@ const DealerInfo: React.FC = () => {
|
||||
// 跳转到申请页面
|
||||
const navigateToApply = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/pages/doctor/apply/add'
|
||||
url: '/pages/dealer/apply/add'
|
||||
})
|
||||
}
|
||||
|
||||
3
src/dealer/orders/index.config.ts
Normal file
3
src/dealer/orders/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销订单'
|
||||
})
|
||||
@@ -24,7 +24,8 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
// 获取订单数据
|
||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
// 需要当前登录用户ID(用于 resourceId 参数)
|
||||
if (!dealerUser || !dealerUser.userId) return
|
||||
|
||||
try {
|
||||
if (isRefresh) {
|
||||
@@ -37,14 +38,17 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
const result = await pageShopDealerOrder({
|
||||
page,
|
||||
limit: 10
|
||||
limit: 10,
|
||||
// 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
|
||||
resourceId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const newOrders = result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
// 优先使用接口返回的订单号;没有则降级展示 orderId
|
||||
orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
|
||||
customerName: `${order.nickname}${order.userId}`,
|
||||
userCommission: order.firstMoney || '0.00'
|
||||
}))
|
||||
|
||||
@@ -102,37 +106,51 @@ const DealerOrders: React.FC = () => {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const handleGoCapital = () => {
|
||||
Taro.navigateTo({url: '/dealer/capital/index'})
|
||||
}
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View
|
||||
key={order.id}
|
||||
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
|
||||
onClick={handleGoCapital}
|
||||
>
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
订单号:{order.orderNo}
|
||||
订单号:{order.orderNo || '-'}
|
||||
</Text>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center mb-1">
|
||||
<Text className="text-sm text-gray-400">
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<Text className="text-sm text-orange-500 font-semibold">
|
||||
我的佣金:¥{order.userCommission}
|
||||
</Text>
|
||||
</View>
|
||||
{/*<View className="flex justify-between items-center mb-1">*/}
|
||||
{/* <Text className="text-sm text-gray-400">*/}
|
||||
{/* 订单金额:¥{order.orderPrice || '0.00'}*/}
|
||||
{/* </Text>*/}
|
||||
{/*</View>*/}
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
客户:{order.customerName}
|
||||
{order.createTime}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{order.createTime}
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
<PullToRefresh
|
||||
@@ -171,9 +189,7 @@ const DealerOrders: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无处方" style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}/>
|
||||
<Empty description="暂无分销订单"/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -11,6 +11,7 @@ import {businessGradients} from '@/styles/gradients'
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = 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()
|
||||
@@ -67,6 +68,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 +139,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
|
||||
|
||||
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 +212,7 @@ const DealerQrcode: React.FC = () => {
|
||||
//
|
||||
// const inviteText = `🎉 邀请您加入我的团队!
|
||||
//
|
||||
// 扫描小程序码或搜索"九云售电云"小程序,即可享受优质商品和服务!
|
||||
// 扫描小程序码或搜索"网宿软件"小程序,即可享受优质商品和服务!
|
||||
//
|
||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
// 🎁 新用户专享优惠等你来拿
|
||||
@@ -258,7 +344,7 @@ const DealerQrcode: React.FC = () => {
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
disabled={!miniProgramCodeUrl || loading || saving}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
3
src/dealer/team/index.config.ts
Normal file
3
src/dealer/team/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请推广'
|
||||
})
|
||||
@@ -407,7 +407,7 @@ const DealerTeam: React.FC = () => {
|
||||
<Space className="flex items-center justify-center">
|
||||
<Empty description="您还不是业务人员" style={{
|
||||
backgroundColor: 'transparent'
|
||||
}} actions={[{text: '立即申请', onClick: () => navTo(`/doctor/apply/add`, true)}]}
|
||||
}} actions={[{text: '立即申请', onClick: () => navTo(`/dealer/apply/add`, true)}]}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
@@ -431,7 +431,7 @@ const DealerTeam: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FixedButton text={'立即添加'} onClick={() => navTo(`/doctor/qrcode/index`, true)}/>
|
||||
<FixedButton text={'立即邀请'} onClick={() => navTo(`/dealer/qrcode/index`, true)}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
557
src/dealer/withdraw/index.tsx
Normal file
557
src/dealer/withdraw/index.tsx
Normal file
@@ -0,0 +1,557 @@
|
||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
CellGroup,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty,
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {Wallet} from '@nutui/icons-react-taro'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {
|
||||
pageShopDealerWithdraw,
|
||||
addShopDealerWithdraw,
|
||||
receiveShopDealerWithdraw,
|
||||
receiveSuccessShopDealerWithdraw
|
||||
} from '@/api/shop/shopDealerWithdraw'
|
||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
|
||||
package_info?: string
|
||||
packageInfo?: string
|
||||
package?: string
|
||||
}
|
||||
|
||||
const extractPackageInfo = (result: unknown): string | null => {
|
||||
if (typeof result === 'string') return result
|
||||
if (!result || typeof result !== 'object') return null
|
||||
const r = result as any
|
||||
return (
|
||||
r.package_info ??
|
||||
r.packageInfo ??
|
||||
r.package ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
const canRequestMerchantTransferConfirm = (): boolean => {
|
||||
try {
|
||||
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
|
||||
const env = (Taro as any).getEnv()
|
||||
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
|
||||
}
|
||||
|
||||
const api =
|
||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||
(Taro as any).requestMerchantTransfer
|
||||
|
||||
return typeof api === 'function'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
|
||||
if (!canRequestMerchantTransferConfirm()) {
|
||||
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
|
||||
}
|
||||
|
||||
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
|
||||
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
|
||||
|
||||
const api =
|
||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||
(Taro as any).requestMerchantTransfer
|
||||
|
||||
if (typeof api !== 'function') {
|
||||
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
api({
|
||||
// WeChat API uses `package`, backend returns `package_info`.
|
||||
package: cleanPackageInfo,
|
||||
mchId: '1737910695',
|
||||
appId: 'wxad831ba00ad6a026',
|
||||
success: (res: any) => resolve(res),
|
||||
fail: (err: any) => reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Some backends may return money fields as number; keep internal usage always as string.
|
||||
const normalizeMoneyString = (money: unknown) => {
|
||||
if (money === null || money === undefined || money === '') return '0.00'
|
||||
return typeof money === 'string' ? money : String(money)
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [claimingId, setClaimingId] = useState<number | null>(null)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
// Tab 切换处理函数
|
||||
const handleTabChange = (value: string | number) => {
|
||||
console.log('Tab切换到:', value)
|
||||
setActiveTab(value)
|
||||
|
||||
// 如果切换到提现记录页面,刷新数据
|
||||
if (String(value) === '1') {
|
||||
fetchWithdrawRecords()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取可提现余额
|
||||
const fetchBalance = useCallback(async () => {
|
||||
console.log(dealerUser, 'dealerUser...')
|
||||
try {
|
||||
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
}, [dealerUser])
|
||||
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await pageShopDealerWithdraw({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const processedRecords = result.list.map(record => ({
|
||||
...record,
|
||||
accountDisplay: getAccountDisplay(record)
|
||||
}))
|
||||
setWithdrawRecords(processedRecords)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提现记录失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取提现记录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 格式化账户显示
|
||||
const getAccountDisplay = (record: ShopDealerWithdraw) => {
|
||||
if (record.payType === 10) {
|
||||
return '微信钱包'
|
||||
} else if (record.payType === 20 && record.alipayAccount) {
|
||||
return `支付宝(${record.alipayAccount.slice(-4)})`
|
||||
} else if (record.payType === 30 && record.bankCard) {
|
||||
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
|
||||
}
|
||||
return '未知账户'
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchBalance().then()
|
||||
fetchWithdrawRecords().then()
|
||||
}
|
||||
}, [fetchBalance, fetchWithdrawRecords])
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40:
|
||||
return '已到账'
|
||||
case 20:
|
||||
return '待领取'
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 30:
|
||||
return '已驳回'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40:
|
||||
return 'success'
|
||||
case 20:
|
||||
return 'info'
|
||||
case 10:
|
||||
return 'warning'
|
||||
case 30:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息获取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(String(values.amount))
|
||||
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
Taro.showToast({
|
||||
title: '请输入有效的提现金额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount < 100) {
|
||||
// Taro.showToast({
|
||||
// title: '最低提现金额为100元',
|
||||
// icon: 'error'
|
||||
// })
|
||||
// return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
Taro.showToast({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
// Only support WeChat wallet withdrawals.
|
||||
payType: 10,
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
|
||||
// Security flow:
|
||||
// 1) user submits => applyStatus=10 (待审核)
|
||||
// 2) backend审核通过 => applyStatus=20 (待领取)
|
||||
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
|
||||
|
||||
// 重置表单
|
||||
formRef.current?.resetFields()
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
|
||||
// 切换到提现记录页面
|
||||
setActiveTab('1')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提现申请失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '提现申请失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClaim = async (record: WithdrawRecordWithDetails) => {
|
||||
if (!record?.id) {
|
||||
Taro.showToast({title: '记录不存在', icon: 'error'})
|
||||
return
|
||||
}
|
||||
|
||||
if (record.applyStatus !== 20) {
|
||||
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (record.payType !== 10) {
|
||||
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (claimingId !== null) return
|
||||
|
||||
try {
|
||||
setClaimingId(record.id)
|
||||
|
||||
if (!canRequestMerchantTransferConfirm()) {
|
||||
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
|
||||
}
|
||||
|
||||
const receiveResult = await receiveShopDealerWithdraw(record.id)
|
||||
const packageInfo = extractPackageInfo(receiveResult)
|
||||
if (!packageInfo) {
|
||||
throw new Error('后台未返回 package_info,无法领取,请联系管理员')
|
||||
}
|
||||
|
||||
try {
|
||||
await requestMerchantTransferConfirm(packageInfo)
|
||||
} catch (e: any) {
|
||||
const msg = String(e?.errMsg || e?.message || '')
|
||||
if (/cancel/i.test(msg)) {
|
||||
Taro.showToast({title: '已取消领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
throw new Error(msg || '领取失败,请稍后重试')
|
||||
}
|
||||
|
||||
try {
|
||||
await receiveSuccessShopDealerWithdraw(record.id)
|
||||
Taro.showToast({title: '领取成功', icon: 'success'})
|
||||
} catch (e: any) {
|
||||
console.warn('领取成功,但状态同步失败:', e)
|
||||
Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
|
||||
} finally {
|
||||
await handleRefresh()
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('领取失败:', e)
|
||||
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
|
||||
} finally {
|
||||
setClaimingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
formRef.current?.setFieldsValue({amount})
|
||||
}
|
||||
|
||||
const setAllAmount = () => {
|
||||
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: unknown) => {
|
||||
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
<View>
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
{/* 装饰背景 - 小程序兼容版本 */}
|
||||
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
right: '-12px',
|
||||
top: '-12px'
|
||||
}}></View>
|
||||
|
||||
<View className="flex items-center justify-between relative z-10">
|
||||
<View className={'flex flex-col'}>
|
||||
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
|
||||
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
||||
</View>
|
||||
<View className="p-3 rounded-full" style={{
|
||||
background: 'rgba(255, 255, 255, 0.2)'
|
||||
}}>
|
||||
<Wallet color="white" size="32"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-4 pt-4 relative z-10" style={{
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}>
|
||||
<Text className="text-white text-opacity-80 text-xs">
|
||||
最低提现金额:¥100 | 手续费:免费
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form
|
||||
ref={formRef}
|
||||
onFinish={handleSubmit}
|
||||
labelPosition="top"
|
||||
>
|
||||
<CellGroup>
|
||||
<Form.Item name="amount" label="提现金额" required>
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 快捷金额 */}
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-600 mb-2">快捷金额</Text>
|
||||
<View className="flex flex-wrap gap-2">
|
||||
{quickAmounts.map(amount => (
|
||||
<Button
|
||||
key={amount}
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setQuickAmount(amount)}
|
||||
>
|
||||
{amount}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={setAllAmount}
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现方式:微信钱包(提交后进入“待审核”,审核通过后请到“提现记录”点击“领取到微信零钱”完成收款确认)
|
||||
</Text>
|
||||
</View>
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderWithdrawRecords = () => {
|
||||
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||||
|
||||
return (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View>
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<Space>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
{/*<Text className="text-sm text-gray-500">*/}
|
||||
{/* 提现账户:{record.accountDisplay}*/}
|
||||
{/*</Text>*/}
|
||||
</Space>
|
||||
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
|
||||
{record.applyStatus === 20 && record.payType === 10 && (
|
||||
<View className="flex mb-5 justify-center">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={claimingId === record.id}
|
||||
disabled={claimingId !== null}
|
||||
onClick={() => handleClaim(record)}
|
||||
>
|
||||
立即领取
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>创建时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{record.auditTime}
|
||||
</Text>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
<Text className="block mt-1 text-red-500">
|
||||
驳回原因:{record.rejectReason}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录"/>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
}
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane title="申请提现" value="0">
|
||||
{renderWithdrawForm()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="提现记录" value="1">
|
||||
{renderWithdrawRecords()}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerWithdraw
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '添加银行卡',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -1,142 +0,0 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Loading, CellGroup, Input, Form} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {
|
||||
getShopDealerBank,
|
||||
listShopDealerBank,
|
||||
updateShopDealerBank,
|
||||
addShopDealerBank
|
||||
} from "@/api/shop/shopDealerBank";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
|
||||
|
||||
const AddUserAddress = () => {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ShopDealerBank>()
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 判断是编辑还是新增模式
|
||||
const isEditMode = !!params.id
|
||||
const bankId = params.id ? Number(params.id) : undefined
|
||||
|
||||
const reload = async () => {
|
||||
// 如果是编辑模式,加载地址数据
|
||||
if (isEditMode && bankId) {
|
||||
try {
|
||||
const bank = await getShopDealerBank(bankId)
|
||||
setFormData(bank)
|
||||
} catch (error) {
|
||||
console.error('加载地址失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载地址失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
console.log('.>>>>>>,....')
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
isDefault: true // 新增或编辑的地址都设为默认地址
|
||||
};
|
||||
|
||||
console.log('提交数据:', submitData)
|
||||
|
||||
// 如果是编辑模式,添加id
|
||||
if (isEditMode && bankId) {
|
||||
submitData.id = bankId;
|
||||
}
|
||||
|
||||
// 先处理默认地址逻辑
|
||||
const defaultAddress = await listShopDealerBank({isDefault: true});
|
||||
if (defaultAddress && defaultAddress.length > 0) {
|
||||
// 如果当前编辑的不是默认地址,或者是新增地址,需要取消其他默认地址
|
||||
if (!isEditMode || (isEditMode && defaultAddress[0].id !== bankId)) {
|
||||
await updateShopDealerBank({
|
||||
...defaultAddress[0],
|
||||
isDefault: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopDealerBank(submitData);
|
||||
} else {
|
||||
await addShopDealerBank(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}失败`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 动态设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: isEditMode ? '编辑银行卡' : '添加银行卡'
|
||||
});
|
||||
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [isEditMode]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="bankName" label="开户行名称" initialValue={FormData?.bankName} required>
|
||||
<Input placeholder="开户行名称" maxLength={10}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccount" label="银行开户名" initialValue={FormData?.bankAccount} required>
|
||||
<Input placeholder="银行开户名" maxLength={10}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="bankCard" label="银行卡号" initialValue={FormData?.bankCard} required>
|
||||
<Input placeholder="银行卡号" maxLength={11}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={isEditMode ? '更新地址' : '保存并使用'} onClick={() => formRef.current?.submit()}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUserAddress;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '银行卡管理',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -1,134 +0,0 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Cell, Space, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
|
||||
import {CheckNormal, Checked} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
|
||||
import {listShopDealerBank, removeShopDealerBank, updateShopDealerBank} from "@/api/shop/shopDealerBank";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
|
||||
const DealerBank = () => {
|
||||
const [list, setList] = useState<ShopDealerBank[]>([])
|
||||
const [bank, setAddress] = useState<ShopDealerBank>()
|
||||
|
||||
const reload = () => {
|
||||
listShopDealerBank({})
|
||||
.then(data => {
|
||||
setList(data || [])
|
||||
// 默认地址
|
||||
setAddress(data.find(item => item.isDefault))
|
||||
})
|
||||
.catch(() => {
|
||||
Taro.showToast({
|
||||
title: '获取地址失败',
|
||||
icon: 'error'
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const onDefault = async (item: ShopDealerBank) => {
|
||||
if (bank) {
|
||||
await updateShopDealerBank({
|
||||
...bank,
|
||||
isDefault: false
|
||||
})
|
||||
}
|
||||
await updateShopDealerBank({
|
||||
id: item.id,
|
||||
isDefault: true
|
||||
})
|
||||
Taro.showToast({
|
||||
title: '设置成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload();
|
||||
}
|
||||
|
||||
const onDel = async (id?: number) => {
|
||||
await removeShopDealerBank(id)
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload();
|
||||
}
|
||||
|
||||
const selectAddress = async (item: ShopDealerBank) => {
|
||||
if (bank) {
|
||||
await updateShopDealerBank({
|
||||
...bank,
|
||||
isDefault: false
|
||||
})
|
||||
}
|
||||
await updateShopDealerBank({
|
||||
id: item.id,
|
||||
isDefault: true
|
||||
})
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload()
|
||||
});
|
||||
|
||||
if (list.length == 0) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<div className={'h-full flex flex-col justify-center items-center'} style={{
|
||||
height: 'calc(100vh - 300px)',
|
||||
}}>
|
||||
<Empty
|
||||
style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
description="您还没有地址哦"
|
||||
/>
|
||||
<Space>
|
||||
<Button onClick={() => Taro.navigateTo({url: '/doctor/bank/add'})}>新增地址</Button>
|
||||
<Button type="success" fill="dashed"
|
||||
onClick={() => Taro.navigateTo({url: '/doctor/bank/wxAddress'})}>获取微信地址</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={'p-3'}>
|
||||
{list.map((item, _) => (
|
||||
<Cell.Group>
|
||||
<Cell className={'flex flex-col gap-1'} extra={item.bankAccount} onClick={() => selectAddress(item)}>
|
||||
<View>
|
||||
<View className={'font-medium text-sm'}>{item.bankName}</View>
|
||||
</View>
|
||||
<View className={'text-xs'}>
|
||||
{item.bankCard} {item.bankAccount}
|
||||
</View>
|
||||
</Cell>
|
||||
<Cell
|
||||
align="center"
|
||||
title={
|
||||
<View className={'flex items-center gap-1'} onClick={() => onDefault(item)}>
|
||||
{item.isDefault ? <Checked className={'text-green-600'} size={16}/> : <CheckNormal size={16}/>}
|
||||
<View className={'text-gray-400'}>选择</View>
|
||||
</View>
|
||||
}
|
||||
extra={
|
||||
<>
|
||||
<View className={'text-gray-400'} onClick={() => onDel(item.id)}>
|
||||
删除
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Cell.Group>
|
||||
))}
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={'新增银行卡'} onClick={() => Taro.navigateTo({url: '/doctor/bank/add'})} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealerBank;
|
||||
@@ -1,108 +0,0 @@
|
||||
# 客户管理页面
|
||||
|
||||
## 功能概述
|
||||
|
||||
这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 数据源
|
||||
- 使用 `pageUsers` API 从 User 表读取客户数据
|
||||
- 支持按状态筛选用户(status: 0 表示正常状态)
|
||||
|
||||
### 2. 状态管理
|
||||
客户状态包括:
|
||||
- **全部** - 显示所有客户
|
||||
- **跟进中** - 正在跟进的潜在客户
|
||||
- **已签约** - 已经签约的客户
|
||||
- **已取消** - 已取消合作的客户
|
||||
|
||||
### 3. 顶部Tabs筛选
|
||||
- 支持按客户状态筛选
|
||||
- 显示每个状态的客户数量统计
|
||||
- 实时更新统计数据
|
||||
|
||||
### 4. 搜索功能
|
||||
支持多字段搜索:
|
||||
- 客户姓名(realName)
|
||||
- 昵称(nickname)
|
||||
- 用户名(username)
|
||||
- 手机号(phone)
|
||||
- 用户ID(userId)
|
||||
|
||||
### 5. 客户信息展示
|
||||
每个客户卡片显示:
|
||||
- 客户姓名和状态标签
|
||||
- 手机号码
|
||||
- 注册时间
|
||||
- 用户ID、余额、积分等统计信息
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 组件结构
|
||||
```
|
||||
CustomerManagement
|
||||
├── 搜索栏 (SearchBar)
|
||||
├── 状态筛选Tabs
|
||||
└── 客户列表
|
||||
└── 客户卡片项
|
||||
```
|
||||
|
||||
### 主要状态
|
||||
- `list`: 客户数据列表
|
||||
- `loading`: 加载状态
|
||||
- `activeTab`: 当前选中的状态Tab
|
||||
- `searchValue`: 搜索关键词
|
||||
|
||||
### 工具函数
|
||||
使用 `@/utils/customerStatus` 工具函数管理客户状态:
|
||||
- `getStatusText()`: 获取状态文本
|
||||
- `getStatusTagType()`: 获取状态标签类型
|
||||
- `getStatusOptions()`: 获取状态选项列表
|
||||
|
||||
## 使用的组件
|
||||
|
||||
### NutUI 组件
|
||||
- `Tabs` / `TabPane`: 状态筛选标签页
|
||||
- `SearchBar`: 搜索输入框
|
||||
- `Tag`: 状态标签
|
||||
- `Loading`: 加载指示器
|
||||
- `Space`: 间距布局
|
||||
|
||||
### 图标
|
||||
- `Phone`: 手机号图标
|
||||
- `User`: 用户图标
|
||||
|
||||
## 数据流
|
||||
|
||||
1. 页面初始化时调用 `fetchCustomerData()` 获取用户数据
|
||||
2. 为每个用户添加客户状态(目前使用随机状态,实际项目中应从数据库获取)
|
||||
3. 根据当前Tab和搜索条件筛选数据
|
||||
4. 渲染客户列表
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 临时实现
|
||||
- 当前使用 `getRandomStatus()` 生成随机客户状态
|
||||
- 实际项目中应该:
|
||||
1. 在数据库中添加客户状态字段
|
||||
2. 修改后端API返回真实的客户状态
|
||||
3. 删除随机状态生成函数
|
||||
|
||||
### 扩展建议
|
||||
1. 添加客户详情页面
|
||||
2. 支持客户状态的修改操作
|
||||
3. 添加客户添加/编辑功能
|
||||
4. 支持批量操作
|
||||
5. 添加导出功能
|
||||
6. 支持更多筛选条件(注册时间、地区等)
|
||||
|
||||
## 文件结构
|
||||
```
|
||||
src/doctor/customer/
|
||||
├── index.tsx # 主页面组件
|
||||
└── README.md # 说明文档
|
||||
|
||||
src/utils/
|
||||
└── customerStatus.ts # 客户状态工具函数
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '患者报备',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -1,400 +0,0 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {Loading, CellGroup, Cell, Input, Form, Calendar} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Calendar as CalendarIcon} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
addShopDealerApply, getShopDealerApply, pageShopDealerApply,
|
||||
updateShopDealerApply
|
||||
} from "@/api/shop/shopDealerApply";
|
||||
import {
|
||||
formatDateForDatabase,
|
||||
extractDateForCalendar, formatDateForDisplay
|
||||
} from "@/utils/dateUtils";
|
||||
|
||||
const AddShopDealerApply = () => {
|
||||
const {user} = useUser()
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ShopDealerApply>()
|
||||
const formRef = useRef<any>(null)
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||
|
||||
// 日期选择器状态
|
||||
const [showApplyTimePicker, setShowApplyTimePicker] = useState<boolean>(false)
|
||||
const [showContractTimePicker, setShowContractTimePicker] = useState<boolean>(false)
|
||||
const [applyTime, setApplyTime] = useState<string>('')
|
||||
const [contractTime, setContractTime] = useState<string>('')
|
||||
|
||||
// 获取审核状态文字
|
||||
const getApplyStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 20:
|
||||
return '已签约'
|
||||
case 30:
|
||||
return '已取消'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
console.log(getApplyStatusText)
|
||||
|
||||
// 处理签约时间选择
|
||||
const handleApplyTimeConfirm = (param: string) => {
|
||||
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
|
||||
const formattedDate = formatDateForDatabase(selectedDate) // 转换为数据库格式
|
||||
setApplyTime(selectedDate) // 保存原始格式用于显示
|
||||
setShowApplyTimePicker(false)
|
||||
|
||||
// 更新表单数据(使用数据库格式)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
applyTime: formattedDate
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理合同日期选择
|
||||
const handleContractTimeConfirm = (param: string) => {
|
||||
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
|
||||
const formattedDate = formatDateForDatabase(selectedDate) // 转换为数据库格式
|
||||
setContractTime(selectedDate) // 保存原始格式用于显示
|
||||
setShowContractTimePicker(false)
|
||||
|
||||
// 更新表单数据(使用数据库格式)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
contractTime: formattedDate
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
if (!params.id) {
|
||||
return false;
|
||||
}
|
||||
// 查询当前用户ID是否已有申请记录
|
||||
try {
|
||||
const dealerApply = await getShopDealerApply(Number(params.id));
|
||||
if (dealerApply) {
|
||||
setFormData(dealerApply)
|
||||
setIsEditMode(true);
|
||||
setExistingApply(dealerApply)
|
||||
|
||||
// 初始化日期数据(从数据库格式转换为Calendar组件格式)
|
||||
if (dealerApply.applyTime) {
|
||||
setApplyTime(extractDateForCalendar(dealerApply.applyTime))
|
||||
}
|
||||
if (dealerApply.contractTime) {
|
||||
setContractTime(extractDateForCalendar(dealerApply.contractTime))
|
||||
}
|
||||
|
||||
Taro.setNavigationBarTitle({title: '签约'})
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(true)
|
||||
console.error('查询申请记录失败:', error);
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
// 计算保护期过期时间(7天后)
|
||||
const calculateExpirationTime = (): string => {
|
||||
const now = new Date();
|
||||
const expirationDate = new Date(now);
|
||||
expirationDate.setDate(now.getDate() + 7); // 7天后
|
||||
|
||||
// 格式化为数据库需要的格式:YYYY-MM-DD HH:mm:ss
|
||||
const year = expirationDate.getFullYear();
|
||||
const month = String(expirationDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(expirationDate.getDate()).padStart(2, '0');
|
||||
const hours = String(expirationDate.getHours()).padStart(2, '0');
|
||||
const minutes = String(expirationDate.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(expirationDate.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!values.mobile || values.mobile.trim() === '') {
|
||||
Taro.showToast({
|
||||
title: '请填写联系方式',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(values.mobile)) {
|
||||
Taro.showToast({
|
||||
title: '请填写正确的手机号',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查客户是否已存在
|
||||
const res = await pageShopDealerApply({dealerName: values.dealerName, type: 4, applyStatus: 10});
|
||||
|
||||
if (res && res.count > 0) {
|
||||
const existingCustomer = res.list[0];
|
||||
|
||||
// 检查是否在7天保护期内
|
||||
if (!isEditMode && existingCustomer.applyTime) {
|
||||
// 将申请时间字符串转换为时间戳进行比较
|
||||
const applyTimeStamp = new Date(existingCustomer.applyTime).getTime();
|
||||
const currentTimeStamp = new Date().getTime();
|
||||
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000; // 7天的毫秒数
|
||||
|
||||
// 如果在7天保护期内,不允许重复添加
|
||||
if (currentTimeStamp - applyTimeStamp < sevenDaysInMs) {
|
||||
const remainingDays = Math.ceil((sevenDaysInMs - (currentTimeStamp - applyTimeStamp)) / (24 * 60 * 60 * 1000));
|
||||
|
||||
Taro.showToast({
|
||||
title: `该客户还在保护期,还需等待${remainingDays}天后才能重新添加`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
// 超过7天保护期,可以重新添加,显示确认对话框
|
||||
const modalResult = await new Promise<boolean>((resolve) => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '该客户已超过7天保护期,是否重新添加跟进?',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '确定',
|
||||
success: (modalRes) => {
|
||||
resolve(modalRes.confirm);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!modalResult) {
|
||||
return false; // 用户取消,不继续执行
|
||||
}
|
||||
// 用户确认后继续执行添加逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 计算过期时间
|
||||
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
|
||||
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
type: 4,
|
||||
realName: values.realName || user?.nickname,
|
||||
mobile: values.mobile,
|
||||
refereeId: 33534,
|
||||
applyStatus: isEditMode ? 20 : 10,
|
||||
auditTime: undefined,
|
||||
// 设置保护期过期时间(7天后)
|
||||
expirationTime: expirationTime,
|
||||
// 确保日期数据正确提交(使用数据库格式)
|
||||
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
|
||||
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : '')
|
||||
};
|
||||
|
||||
// 调试信息
|
||||
console.log('=== 提交数据调试 ===');
|
||||
console.log('是否编辑模式:', isEditMode);
|
||||
console.log('计算的过期时间:', expirationTime);
|
||||
console.log('提交的数据:', submitData);
|
||||
console.log('==================');
|
||||
|
||||
// 如果是编辑模式,添加现有申请的id
|
||||
if (isEditMode && existingApply?.applyId) {
|
||||
submitData.applyId = existingApply.applyId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopDealerApply(submitData);
|
||||
} else {
|
||||
await addShopDealerApply(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '提交'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
Taro.showToast({
|
||||
title: '提交失败,请重试',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理固定按钮点击事件
|
||||
const handleFixedButtonClick = () => {
|
||||
// 触发表单提交
|
||||
formRef.current?.submit();
|
||||
};
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, []); // 依赖用户ID,当用户变化时重新加载
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="dealerName" label="公司名称" initialValue={FormData?.dealerName} required>
|
||||
<Input placeholder="公司名称" maxLength={10} 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="address" label="公司地址" initialValue={FormData?.address} required>
|
||||
<Input placeholder="请输入详细地址" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="dealerCode" label="户号" initialValue={FormData?.dealerCode} required>
|
||||
<Input placeholder="请填写户号" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="comments" label="跟进情况" initialValue={FormData?.comments}>
|
||||
<Input placeholder="请填写跟进情况" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
|
||||
<Input placeholder="(元/兆瓦时)" disabled={false}/>
|
||||
</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>
|
||||
</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>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
{/* <Input placeholder="邀请人ID"/>*/}
|
||||
{/*</Form.Item>*/}
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="userId" label="报备人" initialValue={FormData?.userId} required>
|
||||
选择
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
{/* 签约时间选择器 */}
|
||||
<Calendar
|
||||
visible={showApplyTimePicker}
|
||||
defaultValue={applyTime}
|
||||
onClose={() => setShowApplyTimePicker(false)}
|
||||
onConfirm={handleApplyTimeConfirm}
|
||||
/>
|
||||
|
||||
{/* 合同日期选择器 */}
|
||||
<Calendar
|
||||
visible={showContractTimePicker}
|
||||
defaultValue={contractTime}
|
||||
onClose={() => setShowContractTimePicker(false)}
|
||||
onConfirm={handleContractTimeConfirm}
|
||||
/>
|
||||
|
||||
{/* 审核状态显示(仅在编辑模式下显示) */}
|
||||
{isEditMode && (
|
||||
<CellGroup>
|
||||
{/*<Cell*/}
|
||||
{/* title={'审核状态'}*/}
|
||||
{/* extra={*/}
|
||||
{/* <span style={{*/}
|
||||
{/* color: FormData?.applyStatus === 20 ? '#52c41a' :*/}
|
||||
{/* FormData?.applyStatus === 30 ? '#ff4d4f' : '#faad14'*/}
|
||||
{/* }}>*/}
|
||||
{/* {getApplyStatusText(FormData?.applyStatus)}*/}
|
||||
{/* </span>*/}
|
||||
{/* }*/}
|
||||
{/*/>*/}
|
||||
{FormData?.applyStatus === 20 && (
|
||||
<Cell title={'签约时间'} extra={FormData?.auditTime || '无'}/>
|
||||
)}
|
||||
{FormData?.applyStatus === 30 && (
|
||||
<Cell title={'驳回原因'} extra={FormData?.rejectReason || '无'}/>
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
{(!isEditMode || FormData?.applyStatus === 10) && (
|
||||
<FixedButton
|
||||
icon={<Edit/>}
|
||||
text={'立即提交'}
|
||||
onClick={handleFixedButtonClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddShopDealerApply;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '患者管理'
|
||||
})
|
||||
@@ -1,583 +0,0 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
||||
import {Phone, AngleDoubleLeft} from '@nutui/icons-react-taro'
|
||||
import type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
CustomerStatus,
|
||||
getStatusText,
|
||||
getStatusTagType,
|
||||
getStatusOptions,
|
||||
mapApplyStatusToCustomerStatus,
|
||||
mapCustomerStatusToApplyStatus
|
||||
} from '@/utils/customerStatus';
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
|
||||
|
||||
// 扩展User类型,添加客户状态和保护天数
|
||||
interface CustomerUser extends UserType {
|
||||
customerStatus?: CustomerStatus;
|
||||
protectDays?: number; // 剩余保护天数
|
||||
}
|
||||
|
||||
const CustomerIndex = () => {
|
||||
const [list, setList] = useState<CustomerUser[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// Tab配置
|
||||
const tabList = getStatusOptions();
|
||||
|
||||
// 复制手机号
|
||||
const copyPhone = (phone: string) => {
|
||||
Taro.setClipboardData({
|
||||
data: phone,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '手机号已复制',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 一键拨打
|
||||
const makePhoneCall = (phone: string) => {
|
||||
Taro.makePhoneCall({
|
||||
phoneNumber: phone,
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '拨打取消',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 编辑跟进情况
|
||||
const editComments = (customer: CustomerUser) => {
|
||||
Taro.showModal({
|
||||
title: '编辑跟进情况',
|
||||
// @ts-ignore
|
||||
editable: true,
|
||||
placeholderText: '请输入跟进情况',
|
||||
content: customer.comments || '',
|
||||
success: async (res) => {
|
||||
// @ts-ignore
|
||||
if (res.confirm && res.content !== undefined) {
|
||||
try {
|
||||
// 更新跟进情况
|
||||
await updateShopDealerApply({
|
||||
...customer,
|
||||
// @ts-ignore
|
||||
comments: res.content.trim()
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新列表
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true);
|
||||
} catch (error) {
|
||||
console.error('更新跟进情况失败:', error);
|
||||
Taro.showToast({
|
||||
title: '更新失败,请重试',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 计算剩余保护天数(基于过期时间)
|
||||
const calculateProtectDays = (expirationTime?: string, applyTime?: string): number => {
|
||||
try {
|
||||
// 优先使用过期时间字段
|
||||
if (expirationTime) {
|
||||
const expDate = new Date(expirationTime.replace(' ', 'T'));
|
||||
const now = new Date();
|
||||
|
||||
// 计算剩余毫秒数
|
||||
const remainingMs = expDate.getTime() - now.getTime();
|
||||
|
||||
// 转换为天数,向上取整
|
||||
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
console.log('=== 基于过期时间计算 ===');
|
||||
console.log('过期时间:', expirationTime);
|
||||
console.log('当前时间:', now.toLocaleString());
|
||||
console.log('剩余天数:', remainingDays);
|
||||
console.log('======================');
|
||||
|
||||
return Math.max(0, remainingDays);
|
||||
}
|
||||
|
||||
// 如果没有过期时间,回退到基于申请时间计算
|
||||
if (!applyTime) return 0;
|
||||
|
||||
const protectionPeriod = 7; // 保护期7天
|
||||
|
||||
// 解析申请时间
|
||||
let applyDate: Date;
|
||||
if (applyTime.includes('T')) {
|
||||
applyDate = new Date(applyTime);
|
||||
} else {
|
||||
applyDate = new Date(applyTime.replace(' ', 'T'));
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
const now = new Date();
|
||||
|
||||
// 只比较日期部分,忽略时间
|
||||
const applyDateOnly = new Date(applyDate.getFullYear(), applyDate.getMonth(), applyDate.getDate());
|
||||
const currentDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
// 计算已经过去的天数
|
||||
const timeDiff = currentDateOnly.getTime() - applyDateOnly.getTime();
|
||||
const daysPassed = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||||
|
||||
// 计算剩余保护天数
|
||||
const remainingDays = protectionPeriod - daysPassed;
|
||||
|
||||
console.log('=== 基于申请时间计算 ===');
|
||||
console.log('申请时间:', applyTime);
|
||||
console.log('已过去天数:', daysPassed);
|
||||
console.log('剩余保护天数:', remainingDays);
|
||||
console.log('======================');
|
||||
|
||||
return Math.max(0, remainingDays);
|
||||
} catch (error) {
|
||||
console.error('日期计算错误:', error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取客户数据
|
||||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 4,
|
||||
page: currentPage
|
||||
};
|
||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||
if (applyStatus !== undefined) {
|
||||
params.applyStatus = applyStatus;
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态并计算保护天数
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
|
||||
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
|
||||
}));
|
||||
|
||||
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList(mappedList);
|
||||
} else {
|
||||
setList(prevList => prevList.concat(mappedList));
|
||||
}
|
||||
|
||||
// 正确判断是否还有更多数据
|
||||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||
setHasMore(hasMoreData);
|
||||
} else {
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList([]);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
||||
setPage(currentPage);
|
||||
} catch (error) {
|
||||
console.error('获取客户数据失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab, page]);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
await fetchCustomerData(activeTab, false, nextPage);
|
||||
}
|
||||
|
||||
|
||||
// 防抖搜索功能
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDisplaySearchValue(searchValue);
|
||||
}, 300); // 300ms 防抖
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchValue]);
|
||||
|
||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||||
const getFilteredList = () => {
|
||||
let filteredList = list;
|
||||
|
||||
// 按搜索关键词筛选
|
||||
if (displaySearchValue.trim()) {
|
||||
const keyword = displaySearchValue.trim().toLowerCase();
|
||||
filteredList = filteredList.filter(customer =>
|
||||
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
|
||||
(customer.dealerName && customer.dealerName.toLowerCase().includes(keyword)) ||
|
||||
(customer.dealerCode && customer.dealerCode.toLowerCase().includes(keyword)) ||
|
||||
(customer.mobile && customer.mobile.includes(keyword)) ||
|
||||
(customer.userId && customer.userId.toString().includes(keyword))
|
||||
);
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
};
|
||||
|
||||
// 获取各状态的统计数量
|
||||
const [statusCounts, setStatusCounts] = useState({
|
||||
all: 0,
|
||||
pending: 0,
|
||||
signed: 0,
|
||||
cancelled: 0
|
||||
});
|
||||
|
||||
// 获取所有状态的统计数量
|
||||
const fetchStatusCounts = useCallback(async () => {
|
||||
try {
|
||||
// 并行获取各状态的数量
|
||||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
||||
pageShopDealerApply({type: 4}), // 全部
|
||||
pageShopDealerApply({applyStatus: 10, type: 4}), // 跟进中
|
||||
pageShopDealerApply({applyStatus: 20, type: 4}), // 已签约
|
||||
pageShopDealerApply({applyStatus: 30, type: 4}) // 已取消
|
||||
]);
|
||||
|
||||
setStatusCounts({
|
||||
all: allRes?.count || 0,
|
||||
pending: pendingRes?.count || 0,
|
||||
signed: signedRes?.count || 0,
|
||||
cancelled: cancelledRes?.count || 0
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取状态统计失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getStatusCounts = () => statusCounts;
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = (customer: ShopDealerApply) => {
|
||||
updateShopDealerApply({
|
||||
...customer,
|
||||
applyStatus: 30
|
||||
}).then(() => {
|
||||
Taro.showToast({
|
||||
title: '取消成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 重新加载当前tab的数据
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
})
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (customer: ShopDealerApply) => {
|
||||
removeShopDealerApply(customer.applyId).then(() => {
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 刷新当前tab的数据
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
}, []);
|
||||
|
||||
// 当activeTab变化时重新获取数据
|
||||
useEffect(() => {
|
||||
setList([]); // 清空列表
|
||||
setPage(1); // 重置页码
|
||||
setHasMore(true); // 重置加载状态
|
||||
fetchCustomerData(activeTab, true);
|
||||
}, [activeTab]);
|
||||
|
||||
// 监听页面显示,当从其他页面返回时刷新数据
|
||||
useDidShow(() => {
|
||||
// 刷新当前tab的数据和统计信息
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true);
|
||||
fetchStatusCounts();
|
||||
});
|
||||
|
||||
// 渲染客户项
|
||||
const renderCustomerItem = (customer: CustomerUser) => (
|
||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center mb-3">
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center justify-between mb-1">
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{customer.dealerName}
|
||||
</Text>
|
||||
{customer.customerStatus && (
|
||||
<Tag type={getStatusTagType(customer.customerStatus)}>
|
||||
{getStatusText(customer.customerStatus)}
|
||||
</Tag>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex items-center mb-1">
|
||||
<Space direction="vertical">
|
||||
<Text className="text-xs text-gray-500">联系人:{customer.realName}</Text>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-xs text-gray-500" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
makePhoneCall(customer.mobile || '');
|
||||
}}>联系电话:{customer.mobile}</Text>
|
||||
<View className="flex items-center ml-2">
|
||||
<Phone
|
||||
size={12}
|
||||
className="text-green-500 mr-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
makePhoneCall(customer.mobile || '');
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className="text-xs text-blue-500 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyPhone(customer.mobile || '');
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-500">
|
||||
添加时间:{customer.createTime}
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
|
||||
{/* 保护天数显示 */}
|
||||
{customer.applyStatus === 10 && (
|
||||
<View className="flex items-center my-1">
|
||||
<Text className="text-xs text-gray-500 mr-2">保护期:</Text>
|
||||
{customer.protectDays && customer.protectDays > 0 ? (
|
||||
<Text className={`text-xs px-2 py-1 rounded ${
|
||||
customer.protectDays <= 2
|
||||
? 'bg-red-100 text-red-600'
|
||||
: customer.protectDays <= 4
|
||||
? 'bg-orange-100 text-orange-600'
|
||||
: 'bg-green-100 text-green-600'
|
||||
}`}>
|
||||
剩余{customer.protectDays}天
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-xs px-2 py-1 rounded bg-gray-100 text-gray-500">
|
||||
已过期
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<Text className="text-xs text-gray-500">报备人:{customer?.nickName}</Text>
|
||||
<AngleDoubleLeft size={12} className={'text-blue-500'} />
|
||||
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
|
||||
</View>
|
||||
|
||||
{/* 显示 comments 字段 */}
|
||||
<Space className="flex items-center">
|
||||
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
||||
<Text
|
||||
className="text-xs text-blue-500 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
editComments(customer);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 跟进中状态显示操作按钮 */}
|
||||
{(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && (
|
||||
<Space className="flex justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => navTo(`/doctor/customer/add?id=${customer.applyId}`, true)}
|
||||
style={{marginRight: '8px', backgroundColor: '#52c41a', color: 'white'}}
|
||||
>
|
||||
签约
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleCancel(customer)}
|
||||
style={{backgroundColor: '#ff4d4f', color: 'white'}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
{(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && (
|
||||
<Space className="flex justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleDelete(customer)}
|
||||
style={{backgroundColor: '#ff4d4f', color: 'white'}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染客户列表
|
||||
const renderCustomerList = () => {
|
||||
const filteredList = getFilteredList();
|
||||
const isSearching = displaySearchValue.trim().length > 0;
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
{/* 搜索结果统计 */}
|
||||
{isSearching && (
|
||||
<View className="bg-white px-4 py-2 border-b border-gray-100">
|
||||
<Text className="text-sm text-gray-600">
|
||||
搜索 "{displaySearchValue}" 的结果,共找到 {filteredList.length} 条记录
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="p-4" style={{
|
||||
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}}>
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
// 滚动事件处理
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
// 滚动到顶部事件处理
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中...
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
filteredList.length === 0 ? (
|
||||
<Empty
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
description={loading ? "加载中..." : "暂无客户数据"}
|
||||
/>
|
||||
) : (
|
||||
<View className={'h-3 flex items-center justify-center'}>
|
||||
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && filteredList.length === 0 ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
filteredList.map(renderCustomerItem)
|
||||
)}
|
||||
</InfiniteLoading>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
{/* 搜索栏 */}
|
||||
<View className="bg-white py-2 border-b border-gray-100">
|
||||
<SearchBar
|
||||
value={searchValue}
|
||||
placeholder="搜索客户名称、手机号"
|
||||
onChange={(value) => setSearchValue(value)}
|
||||
onClear={() => {
|
||||
setSearchValue('');
|
||||
setDisplaySearchValue('');
|
||||
}}
|
||||
clearable
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 顶部Tabs */}
|
||||
<View className="bg-white">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(value as CustomerStatus)}
|
||||
>
|
||||
{tabList.map(tab => {
|
||||
const counts = getStatusCounts();
|
||||
const count = counts[tab.value as keyof typeof counts] || 0;
|
||||
return (
|
||||
<TabPane
|
||||
key={tab.value}
|
||||
title={`${tab.label}${count > 0 ? `(${count})` : ''}`}
|
||||
value={tab.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 客户列表 */}
|
||||
{renderCustomerList()}
|
||||
|
||||
<FixedButton text={'客户报备'} onClick={() => Taro.navigateTo({url: '/doctor/customer/add'})}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerIndex;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '入市查询'
|
||||
})
|
||||
@@ -1,207 +0,0 @@
|
||||
import {useState, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Loading, InfiniteLoading, Empty, Space, SearchBar} from '@nutui/nutui-react-taro'
|
||||
import type {ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
CustomerStatus,
|
||||
mapApplyStatusToCustomerStatus,
|
||||
} from '@/utils/customerStatus';
|
||||
import {pageShopDealerApply} from "@/api/shop/shopDealerApply";
|
||||
|
||||
// 扩展User类型,添加客户状态
|
||||
interface CustomerUser extends UserType {
|
||||
customerStatus?: CustomerStatus;
|
||||
}
|
||||
|
||||
const CustomerTrading = () => {
|
||||
const [list, setList] = useState<CustomerUser[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// 获取客户数据
|
||||
const fetchCustomerData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 3,
|
||||
page: currentPage
|
||||
};
|
||||
|
||||
// 添加搜索关键词
|
||||
if (searchKeyword && searchKeyword.trim()) {
|
||||
params.keywords = searchKeyword.trim();
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
|
||||
}));
|
||||
|
||||
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList(mappedList);
|
||||
} else {
|
||||
setList(prevList => prevList.concat(mappedList));
|
||||
}
|
||||
|
||||
// 正确判断是否还有更多数据
|
||||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||
setHasMore(hasMoreData);
|
||||
} else {
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList([]);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
||||
setPage(currentPage);
|
||||
} catch (error) {
|
||||
console.error('获取客户数据失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
await fetchCustomerData(false, nextPage, searchValue);
|
||||
}
|
||||
|
||||
|
||||
// 获取列表数据(现在使用服务端搜索,不需要客户端过滤)
|
||||
const getFilteredList = () => {
|
||||
return list;
|
||||
};
|
||||
|
||||
// 搜索处理函数
|
||||
const handleSearch = (keyword: string) => {
|
||||
if(keyword.length < 4){
|
||||
Taro.showToast({
|
||||
title: '请输入至少4个字符',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSearchValue(keyword);
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(true, 1, keyword);
|
||||
};
|
||||
|
||||
// 清空搜索
|
||||
const handleClearSearch = () => {
|
||||
setSearchValue('');
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(true, 1, '');
|
||||
};
|
||||
|
||||
// 渲染客户项
|
||||
const renderCustomerItem = (customer: CustomerUser) => (
|
||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center">
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center justify-between mb-1">
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{customer.dealerName}
|
||||
</Text>
|
||||
</View>
|
||||
<Space direction={'vertical'}>
|
||||
{/*<Text className="text-xs text-gray-500">统一代码:{customer.dealerCode}</Text>*/}
|
||||
<Text className="text-xs text-gray-500">
|
||||
更新时间:{customer.createTime}
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染客户列表
|
||||
const renderCustomerList = () => {
|
||||
const filteredList = getFilteredList();
|
||||
|
||||
return (
|
||||
<View className="p-4" style={{
|
||||
height: '90vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}}>
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
// 滚动事件处理
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
// 滚动到顶部事件处理
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中...
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
filteredList.length === 0 ? (
|
||||
<Empty
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
description={loading ? "加载中..." : "暂无客户数据"}
|
||||
/>
|
||||
) : (
|
||||
<View className={'h-12 flex items-center justify-center'}>
|
||||
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && filteredList.length === 0 ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
filteredList.map(renderCustomerItem)
|
||||
)}
|
||||
</InfiniteLoading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
{/* 搜索栏 */}
|
||||
<View className="bg-white shadow-sm">
|
||||
<SearchBar
|
||||
placeholder="请输入搜索关键词"
|
||||
value={searchValue}
|
||||
onSearch={(value) => handleSearch(value)}
|
||||
onClear={() => handleClearSearch()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 客户列表 */}
|
||||
{renderCustomerList()}
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerTrading;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生端'
|
||||
})
|
||||
@@ -1,135 +0,0 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Loading, CellGroup, Input, Form, Cell, Avatar} from '@nutui/nutui-react-taro'
|
||||
import {ArrowRight} from '@nutui/icons-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {addShopChatMessage} from "@/api/shop/shopChatMessage";
|
||||
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
|
||||
import navTo from "@/utils/common";
|
||||
import {getUser} from "@/api/system/user";
|
||||
import {User} from "@/api/system/user/model";
|
||||
|
||||
const AddMessage = () => {
|
||||
const {params} = useRouter();
|
||||
const [toUser, setToUser] = useState<User>()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, _] = useState<ShopChatMessage>()
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 判断是编辑还是新增模式
|
||||
const isEditMode = !!params.id
|
||||
const toUserId = params.id ? Number(params.id) : undefined
|
||||
|
||||
const reload = async () => {
|
||||
if(toUserId){
|
||||
getUser(Number(toUserId)).then(data => {
|
||||
setToUser(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values
|
||||
};
|
||||
|
||||
console.log('提交数据:', submitData)
|
||||
|
||||
// 参数校验
|
||||
if(!toUser){
|
||||
Taro.showToast({
|
||||
title: `请选择发送对象`,
|
||||
icon: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断内容是否为空
|
||||
if (!values.content) {
|
||||
Taro.showToast({
|
||||
title: `请输入内容`,
|
||||
icon: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 执行新增或更新操作
|
||||
await addShopChatMessage({
|
||||
toUserId: toUserId,
|
||||
formUserId: Taro.getStorageSync('UserId'),
|
||||
type: 'text',
|
||||
content: values.content
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: `发送成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送失败:', error);
|
||||
Taro.showToast({
|
||||
title: `发送失败`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [isEditMode]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Cell title={toUser ? (
|
||||
<View className={'flex items-center'}>
|
||||
<Avatar src={toUser.avatar}/>
|
||||
<View className={'ml-2 flex flex-col'}>
|
||||
<Text>{toUser.alias || toUser.nickname}</Text>
|
||||
<Text className={'text-gray-300'}>{toUser.mobile}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : '选择患者'} extra={(
|
||||
<ArrowRight color="#cccccc" className={toUser ? 'mt-2' : ''} size={toUser ? 20 : 18}/>
|
||||
)}
|
||||
onClick={() => navTo(`/doctor/customer/index`, true)}/>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="content" initialValue={FormData?.content} required>
|
||||
<Input placeholder="填写消息内容" maxLength={300}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={isEditMode ? '立即发送' : '立即发送'} onClick={() => formRef.current?.submit()}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMessage;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '处方管理'
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '患者管理'
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '微信客服'
|
||||
})
|
||||
@@ -1,176 +0,0 @@
|
||||
.wechat-service-page {
|
||||
min-height: 100vh;
|
||||
|
||||
.service-tabs {
|
||||
background-color: #fff;
|
||||
|
||||
.nut-tabs__titles {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nut-tabs__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 100px);
|
||||
|
||||
.qr-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.qr-title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qr-description {
|
||||
display: block;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
|
||||
.qr-code-wrapper {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
|
||||
.qr-code-image {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
display: block;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-tips {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.tip-title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: block;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 10px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '•';
|
||||
color: #07c160;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 375px) {
|
||||
.wechat-service-page {
|
||||
.qr-container {
|
||||
padding: 15px;
|
||||
|
||||
.qr-content {
|
||||
.qr-code-wrapper {
|
||||
padding: 20px;
|
||||
|
||||
.qr-code-image {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色模式适配
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.wechat-service-page {
|
||||
background-color: #1a1a1a;
|
||||
|
||||
.service-tabs {
|
||||
.nut-tabs__titles {
|
||||
background-color: #2a2a2a;
|
||||
border-bottom-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
background-color: #1a1a1a;
|
||||
|
||||
.qr-header {
|
||||
.qr-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.qr-description {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
.qr-code-wrapper {
|
||||
background-color: #2a2a2a;
|
||||
|
||||
.qr-code-image {
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-tips {
|
||||
background-color: #2a2a2a;
|
||||
|
||||
.tip-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {Tabs} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||
import {CmsWebsiteField} from "@/api/cms/cmsWebsiteField/model";
|
||||
|
||||
const WechatService = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [codes, setCodes] = useState<CmsWebsiteField[]>([])
|
||||
|
||||
// 长按保存二维码到相册
|
||||
const saveQRCodeToAlbum = (imageUrl: string) => {
|
||||
// 首先下载图片到本地
|
||||
Taro.downloadFile({
|
||||
url: imageUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 保存图片到相册
|
||||
Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('保存失败:', error)
|
||||
if (error.errMsg.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '图片下载失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '图片下载失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderQRCode = (data: typeof codes[0]) => (
|
||||
<View className="qr-container">
|
||||
<View className="qr-content">
|
||||
<View className="qr-code-wrapper">
|
||||
<Image
|
||||
src={`${data.value}`}
|
||||
className="qr-code-image"
|
||||
mode="aspectFit"
|
||||
onLongPress={() => saveQRCodeToAlbum(`${data.value}`)}
|
||||
/>
|
||||
{data.style && <Text className="wechat-id">联系电话:{data.style}</Text>}
|
||||
</View>
|
||||
|
||||
<View className="qr-tips">
|
||||
<Text className="tip-title">使用说明:</Text>
|
||||
<Text className="tip-item">1. 长按二维码保存到相册</Text>
|
||||
<Text className="tip-item">2. 打开微信扫一扫</Text>
|
||||
<Text className="tip-item">3. 选择相册中的二维码图片</Text>
|
||||
<Text className="tip-item">4. 添加好友并发送验证消息</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
listCmsWebsiteField({name: 'kefu'}).then(data => {
|
||||
if (data) {
|
||||
setCodes(data)
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View className="wechat-service-page">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(`${value}`)}
|
||||
className="service-tabs"
|
||||
>
|
||||
{codes.map((item) => (
|
||||
<Tabs.TabPane key={item.id} title={item.comments} value={item.id}>
|
||||
{renderQRCode(item)}
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default WechatService
|
||||
@@ -1,461 +0,0 @@
|
||||
import React, {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Input,
|
||||
CellGroup,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty,
|
||||
ActionSheet,
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {Wallet, ArrowRight} from '@nutui/icons-react-taro'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
|
||||
import {listShopDealerBank} from "@/api/shop/shopDealerBank";
|
||||
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [banks, setBanks] = useState<any[]>([])
|
||||
const [bank, setBank] = useState<ShopDealerBank>()
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const [withdrawAmount, setWithdrawAmount] = useState<string>('')
|
||||
const [withdrawValue, setWithdrawValue] = useState<string>('')
|
||||
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
// Tab 切换处理函数
|
||||
const handleTabChange = (value: string | number) => {
|
||||
console.log('Tab切换到:', value)
|
||||
setActiveTab(value)
|
||||
|
||||
// 如果切换到提现记录页面,刷新数据
|
||||
if (String(value) === '1') {
|
||||
fetchWithdrawRecords().then()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取可提现余额
|
||||
const fetchBalance = useCallback(async () => {
|
||||
console.log(dealerUser, 'dealerUser...')
|
||||
try {
|
||||
setAvailableAmount(String(dealerUser?.money || '0.00'))
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
}, [dealerUser])
|
||||
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await pageShopDealerWithdraw({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const processedRecords = result.list.map(record => ({
|
||||
...record,
|
||||
accountDisplay: getAccountDisplay(record)
|
||||
}))
|
||||
setWithdrawRecords(processedRecords)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提现记录失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取提现记录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
function fetchBanks() {
|
||||
listShopDealerBank({}).then(data => {
|
||||
const list = data.map(d => {
|
||||
d.name = d.bankName;
|
||||
d.type = d.bankName;
|
||||
return d;
|
||||
})
|
||||
setBanks(list.concat({
|
||||
name: '管理银行卡',
|
||||
type: 'add'
|
||||
}))
|
||||
setBank(data[0])
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化账户显示
|
||||
const getAccountDisplay = (record: ShopDealerWithdraw) => {
|
||||
if (record.payType === 10) {
|
||||
return '微信钱包'
|
||||
} else if (record.payType === 20 && record.alipayAccount) {
|
||||
return `支付宝(${record.alipayAccount.slice(-4)})`
|
||||
} else if (record.payType === 30 && record.bankCard) {
|
||||
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
|
||||
}
|
||||
return '未知账户'
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
const handleSelect = (item: ShopDealerBank) => {
|
||||
if(item.type === 'add'){
|
||||
return Taro.navigateTo({
|
||||
url: '/doctor/bank/index'
|
||||
})
|
||||
}
|
||||
setBank(item)
|
||||
setIsVisible(false)
|
||||
}
|
||||
|
||||
function fetchCmsField() {
|
||||
listCmsWebsiteField({ name: 'WithdrawValue'}).then(res => {
|
||||
if(res && res.length > 0){
|
||||
const text = res[0].value;
|
||||
setWithdrawValue(text || '')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchBalance().then()
|
||||
fetchWithdrawRecords().then()
|
||||
fetchBanks()
|
||||
fetchCmsField()
|
||||
}
|
||||
}, [fetchBalance, fetchWithdrawRecords])
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40:
|
||||
return '已到账'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 30:
|
||||
return '已驳回'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40:
|
||||
return 'success'
|
||||
case 20:
|
||||
return 'success'
|
||||
case 10:
|
||||
return 'warning'
|
||||
case 30:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息获取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!bank) {
|
||||
Taro.showToast({
|
||||
title: '请选择提现银行卡',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(withdrawAmount)
|
||||
const availableStr = String(availableAmount || '0')
|
||||
const available = parseFloat(availableStr.replace(/,/g, ''))
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
Taro.showToast({
|
||||
title: '请输入有效的提现金额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
Taro.showToast({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证银行卡信息
|
||||
if (!bank.bankCard || !bank.bankAccount || !bank.bankName) {
|
||||
Taro.showToast({
|
||||
title: '银行卡信息不完整',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: withdrawAmount,
|
||||
payType: 30, // 银行卡提现
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram',
|
||||
bankCard: bank.bankCard,
|
||||
bankAccount: bank.bankAccount,
|
||||
bankName: bank.bankName
|
||||
}
|
||||
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
setWithdrawAmount('')
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
|
||||
// 切换到提现记录页面
|
||||
setActiveTab('1')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提现申请失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '提现申请失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
// 计算预计到账金额
|
||||
const calculateExpectedAmount = (amount: string) => {
|
||||
if (!amount || isNaN(parseFloat(amount))) return '0.00'
|
||||
const withdrawAmount = parseFloat(amount)
|
||||
// 提现费率 16% + 3元
|
||||
const feeRate = 0.16
|
||||
const fixedFee = 3
|
||||
const totalFee = withdrawAmount * feeRate + fixedFee
|
||||
const expectedAmount = withdrawAmount - totalFee
|
||||
return Math.max(0, expectedAmount).toFixed(2)
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
<View>
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
{/* 装饰背景 - 小程序兼容版本 */}
|
||||
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
right: '-12px',
|
||||
top: '-12px'
|
||||
}}></View>
|
||||
|
||||
<View className="flex items-center justify-between relative z-10">
|
||||
<View className={'flex flex-col'}>
|
||||
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
|
||||
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
||||
</View>
|
||||
<View className="p-3 rounded-full" style={{
|
||||
background: 'rgba(255, 255, 255, 0.2)'
|
||||
}}>
|
||||
<Wallet color="white" size="32"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-4 pt-4 relative z-10" style={{
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}>
|
||||
<Text className="text-white text-opacity-80 text-xs">
|
||||
最低提现金额:¥100
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<CellGroup>
|
||||
<Cell style={{
|
||||
padding: '36px 12px'
|
||||
}} title={
|
||||
<View className="flex items-center justify-between">
|
||||
<Text className={'text-xl'}>¥</Text>
|
||||
<Input
|
||||
placeholder="提现金额"
|
||||
type="number"
|
||||
maxLength={7}
|
||||
value={withdrawAmount}
|
||||
onChange={(value) => setWithdrawAmount(value)}
|
||||
style={{
|
||||
padding: '0 10px',
|
||||
fontSize: '20px'
|
||||
}}
|
||||
/>
|
||||
<Button fill="none" size={'small'} onClick={() => setWithdrawAmount(dealerUser?.money || '0')}><Text className={'text-blue-500'}>全部提现</Text></Button>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
<Cell title={'提现到'} onClick={() => setIsVisible(true)} extra={
|
||||
<View className="flex items-center justify-between gap-1">
|
||||
{bank ? <Text className={'text-gray-800'}>{bank.bankName}</Text> : <Text className={'text-gray-400'}>请选择</Text>}
|
||||
<ArrowRight className={'text-gray-300'} size={15}/>
|
||||
</View>
|
||||
}/>
|
||||
<Cell title={'预计到账金额'} description={'提现费率 16% +3元'} extra={
|
||||
<View className="flex items-center justify-between gap-1">
|
||||
<Text className={'text-orange-500 px-2 text-lg'}>¥{calculateExpectedAmount(withdrawAmount)}</Text>
|
||||
</View>
|
||||
}/>
|
||||
<Cell title={<Text className={'text-gray-400'}>说明:{withdrawValue}</Text>}/>
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting || !withdrawAmount || !bank}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderWithdrawRecords = () => {
|
||||
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||||
|
||||
return (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View>
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<Space direction={'vertical'}>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.accountDisplay}
|
||||
</Text>
|
||||
</Space>
|
||||
<Tag type={getStatusColor(record.applyStatus)}>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>申请时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
<Text className="block mt-1 text-red-500">
|
||||
驳回原因:{record.rejectReason}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录"/>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane title="申请提现" value="0">
|
||||
{renderWithdrawForm()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="提现记录" value="1">
|
||||
{renderWithdrawRecords()}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<ActionSheet
|
||||
visible={isVisible}
|
||||
options={banks}
|
||||
onSelect={handleSelect}
|
||||
onCancel={() => setIsVisible(false)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerWithdraw
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '礼品卡专区',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -1,368 +0,0 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {
|
||||
Empty,
|
||||
ConfigProvider,
|
||||
InfiniteLoading,
|
||||
Loading,
|
||||
PullToRefresh,
|
||||
Tabs,
|
||||
TabPane
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
|
||||
import {pageShopCoupon} from "@/api/shop/shopCoupon";
|
||||
import CouponList from "@/components/CouponList";
|
||||
import CouponGuide from "@/components/CouponGuide";
|
||||
import CouponFilter from "@/components/CouponFilter";
|
||||
import {CouponCardProps} from "@/components/CouponCard";
|
||||
import {takeCoupon} from "@/api/shop/shopUserCoupon";
|
||||
|
||||
const CouponReceiveCenter = () => {
|
||||
const [list, setList] = useState<ShopCoupon[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [page, setPage] = useState(1)
|
||||
const [activeTab, setActiveTab] = useState('0') // 0-全部 1-满减券 2-折扣券 3-免费券
|
||||
const [showGuide, setShowGuide] = useState(false)
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
const [filters, setFilters] = useState({
|
||||
type: [] as number[],
|
||||
minAmount: undefined as number | undefined,
|
||||
sortBy: 'createTime' as 'createTime' | 'amount' | 'expireTime',
|
||||
sortOrder: 'desc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
// 获取礼品卡类型过滤条件
|
||||
const getTypeFilter = () => {
|
||||
switch (String(activeTab)) {
|
||||
case '0': // 全部
|
||||
return {}
|
||||
case '1': // 满减券
|
||||
return { type: 10 }
|
||||
case '2': // 折扣券
|
||||
return { type: 20 }
|
||||
case '3': // 免费券
|
||||
return { type: 30 }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据传入的值获取类型过滤条件
|
||||
const getTypeFilterByValue = (value: string | number) => {
|
||||
switch (String(value)) {
|
||||
case '0': // 全部
|
||||
return {}
|
||||
case '1': // 满减券
|
||||
return { type: 10 }
|
||||
case '2': // 折扣券
|
||||
return { type: 20 }
|
||||
case '3': // 免费券
|
||||
return { type: 30 }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据类型过滤条件加载礼品卡
|
||||
const loadCouponsByType = async (typeFilter: any) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = 1
|
||||
// 获取可领取的礼品卡(启用状态且未过期)
|
||||
const res = await pageShopCoupon({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: '',
|
||||
enabled: 1, // 启用状态
|
||||
isExpire: 0, // 未过期
|
||||
...typeFilter
|
||||
})
|
||||
|
||||
console.log('API返回数据:', res)
|
||||
if (res && res.list) {
|
||||
setList(res.list)
|
||||
setHasMore(res.list.length === 10)
|
||||
setPage(2)
|
||||
} else {
|
||||
setList([])
|
||||
setHasMore(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
const typeFilter = getTypeFilter()
|
||||
console.log('reload - 当前activeTab:', activeTab, '类型过滤:', typeFilter)
|
||||
|
||||
// 获取可领取的礼品卡(启用状态且未过期)
|
||||
const res = await pageShopCoupon({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: '',
|
||||
enabled: 1, // 启用状态
|
||||
isExpire: 0, // 未过期
|
||||
...typeFilter,
|
||||
// 应用筛选条件
|
||||
...(filters.type.length > 0 && { type: filters.type[0] }),
|
||||
sortBy: filters.sortBy,
|
||||
sortOrder: filters.sortOrder
|
||||
})
|
||||
|
||||
console.log('reload - API返回数据:', res)
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
|
||||
// 判断是否还有更多数据
|
||||
setHasMore(res.list.length === 10)
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2)
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await reload(true)
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
const handleTabChange = (value: string | number) => {
|
||||
console.log('Tab切换到:', value)
|
||||
setActiveTab(String(value))
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
|
||||
// 直接传递类型值,避免异步状态更新问题
|
||||
const typeFilter = getTypeFilterByValue(value)
|
||||
console.log('类型过滤条件:', typeFilter)
|
||||
|
||||
// 立即加载数据
|
||||
loadCouponsByType(typeFilter)
|
||||
}
|
||||
|
||||
// 转换礼品卡数据为CouponCard组件所需格式
|
||||
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
|
||||
let amount = 0
|
||||
let type: 10 | 20 | 30 = 10
|
||||
|
||||
if (coupon.type === 10) { // 满减券
|
||||
type = 10
|
||||
amount = parseFloat(coupon.reducePrice || '0')
|
||||
} else if (coupon.type === 20) { // 折扣券
|
||||
type = 20
|
||||
amount = coupon.discount || 0
|
||||
} else if (coupon.type === 30) { // 免费券
|
||||
type = 30
|
||||
amount = 0
|
||||
}
|
||||
|
||||
return {
|
||||
id: coupon.id?.toString(),
|
||||
amount,
|
||||
type,
|
||||
status: 0, // 可领取状态
|
||||
minAmount: parseFloat(coupon.minPrice || '0'),
|
||||
title: coupon.name || '礼品卡',
|
||||
description: coupon.description,
|
||||
startTime: coupon.startTime,
|
||||
endTime: coupon.endTime,
|
||||
showReceiveBtn: true, // 显示领取按钮
|
||||
onReceive: () => handleReceiveCoupon(coupon),
|
||||
theme: getThemeByType(coupon.type)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据礼品卡类型获取主题色
|
||||
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
|
||||
switch (type) {
|
||||
case 10: return 'red' // 满减券-红色
|
||||
case 20: return 'orange' // 折扣券-橙色
|
||||
case 30: return 'green' // 免费券-绿色
|
||||
default: return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
// 领取礼品卡
|
||||
const handleReceiveCoupon = async (coupon: ShopCoupon) => {
|
||||
try {
|
||||
// 检查是否已登录
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
if (!userId) {
|
||||
Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用领取接口
|
||||
await takeCoupon({
|
||||
couponId: coupon.id!,
|
||||
userId: userId
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
reload(true)
|
||||
} catch (error: any) {
|
||||
console.error('领取礼品卡失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '领取失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选条件变更
|
||||
const handleFiltersChange = (newFilters: any) => {
|
||||
setFilters(newFilters)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
// 查看我的礼品卡
|
||||
const handleViewMyCoupons = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/user/coupon/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false) // 不刷新,追加数据
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider className="h-screen flex flex-col">
|
||||
{/* Tab切换 */}
|
||||
<View className="bg-white hidden">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<TabPane title="全部" value="0">
|
||||
</TabPane>
|
||||
<TabPane title="满减券" value="1">
|
||||
</TabPane>
|
||||
<TabPane title="折扣券" value="2">
|
||||
</TabPane>
|
||||
<TabPane title="免费券" value="3">
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡列表 - 占满剩余空间 */}
|
||||
<View className="flex-1 overflow-hidden">
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 'calc(100vh - 60px)',
|
||||
overflowY: 'auto',
|
||||
paddingTop: '24px',
|
||||
paddingBottom: '32px'
|
||||
}}
|
||||
id="coupon-scroll"
|
||||
>
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center h-full">
|
||||
<Empty
|
||||
description="暂无可领取的礼品卡"
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
actions={[
|
||||
{
|
||||
text: '查看我的礼品卡',
|
||||
onClick: handleViewMyCoupons
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="coupon-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<CouponList
|
||||
coupons={list.map(transformCouponData)}
|
||||
showEmpty={false}
|
||||
/>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
|
||||
{/* 使用指南弹窗 */}
|
||||
<CouponGuide
|
||||
visible={showGuide}
|
||||
onClose={() => setShowGuide(false)}
|
||||
/>
|
||||
|
||||
{/* 筛选弹窗 */}
|
||||
<CouponFilter
|
||||
visible={showFilter}
|
||||
filters={filters}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClose={() => setShowFilter(false)}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default CouponReceiveCenter;
|
||||
@@ -41,7 +41,6 @@ export const useConfig = () => {
|
||||
configWebsiteField().then(data => {
|
||||
setConfig(data);
|
||||
Taro.setStorageSync('config', data);
|
||||
|
||||
setLoading(false);
|
||||
}).catch(err => {
|
||||
setError(err instanceof Error ? err : new Error('获取配置失败'));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { UserOrderStats } from '@/api/user';
|
||||
import { getUserOrderStats, UserOrderStats } from '@/api/user';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
|
||||
/**
|
||||
* 订单统计Hook
|
||||
@@ -31,20 +30,17 @@ export const useOrderStats = () => {
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return false;
|
||||
}
|
||||
// TODO 读取订单数量
|
||||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
|
||||
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
|
||||
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
|
||||
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
|
||||
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
|
||||
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
|
||||
|
||||
// 聚合接口:一次请求返回各状态数量(后台按用户做了缓存)
|
||||
const stats = await getUserOrderStats();
|
||||
|
||||
setOrderStats({
|
||||
pending: pending?.count || 0,
|
||||
paid: paid?.count || 0,
|
||||
shipped: shipped?.count || 0,
|
||||
completed: completed?.count || 0,
|
||||
refund: refund?.count || 0,
|
||||
total: total?.count || 0
|
||||
pending: stats?.pending || 0,
|
||||
paid: stats?.paid || 0,
|
||||
shipped: stats?.shipped || 0,
|
||||
completed: stats?.completed || 0,
|
||||
refund: stats?.refund || 0,
|
||||
total: stats?.total || 0
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
|
||||
* 提供主题切换和状态管理功能
|
||||
*/
|
||||
export const useTheme = (): UseThemeReturn => {
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
|
||||
|
||||
// 获取当前主题
|
||||
const getCurrentTheme = (): GradientTheme => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
|
||||
if (savedTheme === 'auto') {
|
||||
// 自动主题:根据用户ID生成
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
return gradientUtils.getThemeByUserId(userId)
|
||||
} else {
|
||||
// 手动选择的主题
|
||||
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
|
||||
const getSavedThemeName = useCallback((): string => {
|
||||
try {
|
||||
return Taro.getStorageSync('user_theme') || 'nature'
|
||||
} catch {
|
||||
return 'nature'
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getStoredUserId = useCallback((): number => {
|
||||
try {
|
||||
const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
|
||||
const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
|
||||
return Number.isFinite(asNumber) ? asNumber : 1
|
||||
} catch {
|
||||
return 1
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resolveTheme = useCallback(
|
||||
(themeName: string): GradientTheme => {
|
||||
if (themeName === 'auto') {
|
||||
return gradientUtils.getThemeByUserId(getStoredUserId())
|
||||
}
|
||||
return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
|
||||
},
|
||||
[getStoredUserId]
|
||||
)
|
||||
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(() => getSavedThemeName() === 'auto')
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(() => resolveTheme(getSavedThemeName()))
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
const savedTheme = getSavedThemeName()
|
||||
setIsAutoTheme(savedTheme === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(savedTheme))
|
||||
}, [])
|
||||
|
||||
// 设置主题
|
||||
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
|
||||
try {
|
||||
Taro.setStorageSync('user_theme', themeName)
|
||||
setIsAutoTheme(themeName === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(themeName))
|
||||
} catch (error) {
|
||||
console.error('保存主题失败:', error)
|
||||
}
|
||||
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
|
||||
|
||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||
const refreshTheme = () => {
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(getSavedThemeName()))
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -50,7 +50,7 @@ export const useUser = () => {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
|
||||
return Taro.navigateTo({
|
||||
url: '/doctor/apply/add'
|
||||
url: '/dealer/apply/add'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
|
||||
import {pageShopGift} from "@/api/shop/shopGift";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import { getUserCardStats } from '@/api/user'
|
||||
|
||||
interface UserData {
|
||||
balance: number
|
||||
balance: string
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
@@ -24,7 +22,7 @@ interface UseUserDataReturn {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refresh: () => Promise<void>
|
||||
updateBalance: (newBalance: number) => void
|
||||
updateBalance: (newBalance: string) => void
|
||||
updatePoints: (newPoints: number) => void
|
||||
}
|
||||
|
||||
@@ -43,18 +41,14 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发请求所有数据
|
||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||
getUserInfo(),
|
||||
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
|
||||
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
|
||||
])
|
||||
// 聚合接口:一次请求返回余额/积分/优惠券/礼品卡统计(后端可按用户做缓存)
|
||||
const stats = await getUserCardStats()
|
||||
|
||||
const newData: UserData = {
|
||||
balance: userDataRes?.balance || 0.00,
|
||||
points: userDataRes?.points || 0,
|
||||
coupons: couponsRes?.count || 0,
|
||||
giftCards: giftCardsRes?.count || 0,
|
||||
balance: stats?.balance || '0.00',
|
||||
points: stats?.points || 0,
|
||||
coupons: stats?.coupons || 0,
|
||||
giftCards: stats?.giftCards || 0,
|
||||
orders: {
|
||||
pending: 0,
|
||||
paid: 0,
|
||||
@@ -78,7 +72,7 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
}, [fetchUserData])
|
||||
|
||||
// 更新余额(本地更新,避免频繁请求)
|
||||
const updateBalance = useCallback((newBalance: number) => {
|
||||
const updateBalance = useCallback((newBalance: string) => {
|
||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ function Cart() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '购物车 - WebSoft Inc.',
|
||||
title: '购物车 - 网宿软件',
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分类'
|
||||
})
|
||||
@@ -1,425 +0,0 @@
|
||||
.category-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 1rpx);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 左侧分类导航 */
|
||||
.category-left {
|
||||
width: 200rpx;
|
||||
background-color: #fff;
|
||||
border-right: 2rpx solid #f0f0f0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.category-scroll {
|
||||
height: calc(100vh - 1rpx);
|
||||
}
|
||||
|
||||
.category-item {
|
||||
padding: 30rpx 20rpx;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
color: #ff6b35;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8rpx;
|
||||
height: 50rpx;
|
||||
background: linear-gradient(180deg, #ff6b35 0%, #ff8f6b 100%);
|
||||
border-radius: 0 8rpx 8rpx 0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2rpx;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.active .category-name {
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.active .category-count {
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧商品列表 */
|
||||
.category-right {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
|
||||
.goods-scroll {
|
||||
height: calc(100vh - 1rpx);
|
||||
}
|
||||
|
||||
.goods-section {
|
||||
.section-title {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: linear-gradient(135deg, #f8f8f8 0%, #f0f0f0 100%);
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
z-index: 10;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #fafafa;
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.goods-name {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 12rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.goods-price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.goods-price {
|
||||
font-size: 36rpx;
|
||||
color: #ff6b35;
|
||||
font-weight: 700;
|
||||
margin-right: 16rpx;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 24rpx;
|
||||
position: relative;
|
||||
top: -2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-original-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-stock {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
background-color: #f8f8f8;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 4rpx;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
padding: 120rpx 30rpx;
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '📋';
|
||||
display: block;
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.category-skeleton {
|
||||
display: flex;
|
||||
height: calc(100vh - 1rpx);
|
||||
|
||||
.category-left {
|
||||
width: 200rpx;
|
||||
background-color: #fff;
|
||||
border-right: 2rpx solid #f0f0f0;
|
||||
padding: 20rpx 0;
|
||||
|
||||
.skeleton-category-item {
|
||||
padding: 30rpx 20rpx;
|
||||
|
||||
.skeleton-text {
|
||||
height: 32rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4rpx;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-right {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
|
||||
.skeleton-goods-item {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
|
||||
.skeleton-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 16rpx;
|
||||
margin-right: 24rpx;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-info {
|
||||
flex: 1;
|
||||
|
||||
.skeleton-text {
|
||||
height: 32rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4rpx;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 750rpx) {
|
||||
.category-left {
|
||||
width: 160rpx;
|
||||
|
||||
.category-item {
|
||||
padding: 24rpx 16rpx;
|
||||
|
||||
.category-name {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-right {
|
||||
.goods-section {
|
||||
.goods-list {
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
padding: 20rpx 0;
|
||||
|
||||
.goods-image {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
.goods-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.goods-price-row {
|
||||
.goods-price {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-container {
|
||||
height: calc(100vh - 1rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
padding: 60rpx 40rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8f6b 100%);
|
||||
color: #fff;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 50rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useShareAppMessage } from "@tarojs/taro"
|
||||
import { useEffect, useState, useRef, useCallback } from "react"
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Image } from '@nutui/nutui-react-taro'
|
||||
import {listCmsNavigation} from "@/api/cms/cmsNavigation"
|
||||
import { CmsNavigation } from "@/api/cms/cmsNavigation/model"
|
||||
import { pageShopGoods } from "@/api/shop/shopGoods"
|
||||
import { ShopGoods } from "@/api/shop/shopGoods/model"
|
||||
import './category.scss'
|
||||
|
||||
function Category() {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [categories, setCategories] = useState<CmsNavigation[]>([])
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number>(0)
|
||||
const [goods, setGoods] = useState<{ [key: number]: ShopGoods[] }>({})
|
||||
const [allGoods, setAllGoods] = useState<ShopGoods[]>([])
|
||||
const rightScrollRef = useRef<any>(null)
|
||||
const [scrollIntoView, setScrollIntoView] = useState('')
|
||||
const [isScrollingByClick, setIsScrollingByClick] = useState(false)
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const home = await listCmsNavigation({home: 1})
|
||||
// 获取商品分类
|
||||
const categoryList = await listCmsNavigation({ parentId: home[0].navigationId || 0, position: 1 })
|
||||
|
||||
if (!categoryList || categoryList.length === 0) {
|
||||
Taro.showToast({
|
||||
title: '暂无商品分类',
|
||||
icon: 'none'
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setCategories(categoryList)
|
||||
const firstCategory = categoryList[0]
|
||||
setSelectedCategoryId(firstCategory.navigationId!)
|
||||
|
||||
// 并行获取所有分类的商品数据
|
||||
const goodsPromises = categoryList.map((category: CmsNavigation) =>
|
||||
pageShopGoods({ categoryId: category.navigationId }).catch(err => {
|
||||
console.error(`分类 ${category.title} 商品加载失败:`, err)
|
||||
return { list: [] }
|
||||
})
|
||||
)
|
||||
|
||||
const goodsResults = await Promise.all(goodsPromises)
|
||||
|
||||
// 组织商品数据
|
||||
const goodsByCategory: { [key: number]: ShopGoods[] } = {}
|
||||
categoryList.forEach((category: CmsNavigation, index: number) => {
|
||||
goodsByCategory[category.navigationId!] = goodsResults[index]?.list || []
|
||||
})
|
||||
|
||||
setGoods(goodsByCategory)
|
||||
|
||||
// 获取所有商品用于搜索等功能
|
||||
try {
|
||||
const allGoodsRes = await pageShopGoods({})
|
||||
setAllGoods(allGoodsRes?.list || [])
|
||||
} catch (err) {
|
||||
console.error('获取所有商品失败:', err)
|
||||
}
|
||||
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '商品分类'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('分类数据加载失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initData().then()
|
||||
console.log(allGoods,'allGoods')
|
||||
}, [])
|
||||
|
||||
// 点击左侧分类
|
||||
const handleCategoryClick = (categoryId: number) => {
|
||||
setIsScrollingByClick(true)
|
||||
setSelectedCategoryId(categoryId)
|
||||
setScrollIntoView(`category-${categoryId}`)
|
||||
|
||||
// 延迟重置滚动标志
|
||||
setTimeout(() => {
|
||||
setIsScrollingByClick(false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 右侧滚动时处理分类切换
|
||||
const handleRightScroll = useCallback((e: any) => {
|
||||
console.log(e,'右侧滚动时处理分类切换')
|
||||
if (isScrollingByClick) return
|
||||
|
||||
// 这里可以添加逻辑来检测当前滚动到哪个分类
|
||||
// 由于小程序限制,暂时简化处理
|
||||
}, [isScrollingByClick])
|
||||
|
||||
// 跳转商品详情
|
||||
const goToGoodsDetail = (goodsId: number) => {
|
||||
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${goodsId}` })
|
||||
}
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '商品分类',
|
||||
path: '/pages/category/category',
|
||||
success: function () {
|
||||
console.log('分享成功')
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 骨架屏组件
|
||||
const CategorySkeleton = () => (
|
||||
<View className="category-skeleton" style={{
|
||||
marginTop: '1rpx',
|
||||
}}>
|
||||
<View className="category-left">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<View key={i} className="skeleton-category-item">
|
||||
<View className="skeleton-text" />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className="category-right">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<View key={i} className="skeleton-goods-item">
|
||||
<View className="skeleton-image" />
|
||||
<View className="skeleton-info">
|
||||
<View className="skeleton-text" style={{ width: '80%', marginBottom: '8px' }} />
|
||||
<View className="skeleton-text" style={{ width: '60%' }} />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <CategorySkeleton />
|
||||
}
|
||||
|
||||
// 空状态处理
|
||||
if (!categories || categories.length === 0) {
|
||||
return (
|
||||
<View className="empty-container">
|
||||
<View className="empty-content">
|
||||
<Text className="empty-icon">📋</Text>
|
||||
<Text className="empty-title">暂无商品分类</Text>
|
||||
<Text className="empty-desc">请稍后再试或联系客服</Text>
|
||||
<View className="empty-action" onClick={initData}>
|
||||
<Text>重新加载</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="category-container" style={{
|
||||
marginTop: '1rpx',
|
||||
}}>
|
||||
{/* 左侧分类导航 */}
|
||||
<View className="category-left">
|
||||
<ScrollView
|
||||
className="category-scroll"
|
||||
scrollY
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
> {categories.map((category) => (
|
||||
<View
|
||||
key={category.navigationId}
|
||||
className={`category-item ${
|
||||
selectedCategoryId === category.navigationId ? 'active' : ''
|
||||
}`}
|
||||
onClick={() => handleCategoryClick(category.navigationId!)}
|
||||
>
|
||||
<Text className="category-name">{category.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 右侧商品列表 */}
|
||||
<View className="category-right">
|
||||
<ScrollView
|
||||
ref={rightScrollRef}
|
||||
className="goods-scroll"
|
||||
scrollY
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
scrollIntoView={scrollIntoView}
|
||||
onScroll={handleRightScroll}
|
||||
>
|
||||
{categories.map((category) => {
|
||||
const categoryGoods = goods[category.navigationId!] || []
|
||||
return (
|
||||
<View
|
||||
key={category.navigationId}
|
||||
id={`category-${category.navigationId}`}
|
||||
className="goods-section"
|
||||
>
|
||||
<View className="section-title">
|
||||
<Text>{category.title}</Text>
|
||||
</View>
|
||||
|
||||
{categoryGoods.length > 0 ? (
|
||||
<View className="goods-list">
|
||||
{categoryGoods.map((item) => (
|
||||
<View
|
||||
key={item.goodsId}
|
||||
className="goods-item"
|
||||
onClick={() => goToGoodsDetail(item.goodsId!)}
|
||||
>
|
||||
<Image
|
||||
className="goods-image"
|
||||
src={item.image || ''}
|
||||
mode="aspectFill"
|
||||
lazyLoad
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<View className="goods-info">
|
||||
<Text className="goods-name">{item.name}</Text>
|
||||
{item.comments && (
|
||||
<Text className="goods-desc">{item.comments}</Text>
|
||||
)}
|
||||
<View className="goods-price-row">
|
||||
<Text className="goods-price">¥{item.price}</Text>
|
||||
{item.salePrice && Number(item.salePrice) !== Number(item.price) && (
|
||||
<Text className="goods-original-price">¥{item.salePrice}</Text>
|
||||
)}
|
||||
</View>
|
||||
{item.stock !== undefined && (
|
||||
<Text className="goods-stock">
|
||||
库存: {item.stock > 0 ? item.stock : '缺货'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View className="empty-section">
|
||||
<Text>该分类暂无商品</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Category
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '在线开方',
|
||||
navigationBarTitleText: '文章列表',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
96
src/pages/category/index.tsx
Normal file
96
src/pages/category/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react"
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {getCmsNavigation, listCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import ArticleList from './components/ArticleList'
|
||||
import ArticleTabs from "./components/ArticleTabs";
|
||||
import './index.scss'
|
||||
|
||||
function Category() {
|
||||
const {params} = useRouter();
|
||||
const [categoryId, setCategoryId] = useState<number>(0)
|
||||
const [category, setCategory] = useState<CmsNavigation[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [nav, setNav] = useState<CmsNavigation>()
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
|
||||
const reload = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 1.加载远程数据
|
||||
const id = Number(params.id || 4328)
|
||||
const nav = await getCmsNavigation(id)
|
||||
const categoryList = await listCmsNavigation({parentId: id})
|
||||
const shopGoods = await pageCmsArticle({categoryId: id})
|
||||
|
||||
// 2.赋值
|
||||
setCategoryId(id)
|
||||
setNav(nav)
|
||||
setList(shopGoods?.list || [])
|
||||
setCategory(categoryList)
|
||||
Taro.setNavigationBarTitle({
|
||||
title: `${nav?.categoryName}`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('文章分类加载失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, []);
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 骨架屏组件
|
||||
const ArticleSkeleton = () => (
|
||||
<View className="px-3">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<View key={i} className="flex items-center py-4 border-b border-gray-100">
|
||||
{/* 左侧文字骨架屏 */}
|
||||
<View className="flex-1 pr-4">
|
||||
{/* 标题骨架屏 */}
|
||||
<View className="bg-gray-200 h-5 rounded animate-pulse mb-2" style={{width: '75%'}}/>
|
||||
{/* 副标题骨架屏 */}
|
||||
<View className="bg-gray-200 h-4 rounded animate-pulse" style={{width: '50%'}}/>
|
||||
</View>
|
||||
{/* 右侧图片骨架屏 */}
|
||||
<View
|
||||
className="bg-gray-200 rounded animate-pulse flex-shrink-0"
|
||||
style={{width: '100px', height: '100px'}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <ArticleSkeleton />
|
||||
}
|
||||
|
||||
if(category.length > 0){
|
||||
return <ArticleTabs data={category}/>
|
||||
}
|
||||
|
||||
return <ArticleList data={list}/>
|
||||
}
|
||||
|
||||
export default Category
|
||||
@@ -6,13 +6,38 @@ import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
|
||||
type AdImage = {
|
||||
url?: string
|
||||
path?: string
|
||||
title?: string
|
||||
// Compatible keys (some backends use different fields)
|
||||
src?: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
function normalizeAdImages(ad?: CmsAd): AdImage[] {
|
||||
const list = ad?.imageList
|
||||
if (Array.isArray(list) && list.length) return list as AdImage[]
|
||||
|
||||
// Some APIs only return `images` as a JSON string.
|
||||
const raw = ad?.images
|
||||
if (!raw) return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? (parsed as AdImage[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function toNumberPx(input: unknown, fallback: number) {
|
||||
const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10)
|
||||
return Number.isFinite(n) ? n : fallback
|
||||
}
|
||||
|
||||
const MyPage = () => {
|
||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||
const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||||
|
||||
@@ -21,24 +46,21 @@ const MyPage = () => {
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 轮播图
|
||||
const flash = await getCmsAdByCode('flash')
|
||||
// 今日热卖
|
||||
const hotToday = await getCmsAdByCode('hot_today')
|
||||
// 时里动态
|
||||
const news = await pageCmsArticle({limit:1,recommend:1})
|
||||
// 赋值
|
||||
if(flash){
|
||||
setCarouselData(flash)
|
||||
}
|
||||
if(hotToday){
|
||||
setHotToday(hotToday)
|
||||
}
|
||||
if(news && news.list.length > 0){
|
||||
setItem(news.list[0])
|
||||
try {
|
||||
const [flashRes] = await Promise.allSettled([
|
||||
getCmsAdByCode('mp-ad'),
|
||||
getCmsAdByCode('hot_today'),
|
||||
pageCmsArticle({ limit: 1, recommend: 1 }),
|
||||
])
|
||||
|
||||
if (flashRes.status === 'fulfilled') {
|
||||
console.log('flashflashflash', flashRes.value)
|
||||
setCarouselData(flashRes.value)
|
||||
} else {
|
||||
console.error('Failed to fetch flash:', flashRes.reason)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Banner数据加载失败:', error)
|
||||
} finally {
|
||||
@@ -47,11 +69,13 @@ const MyPage = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
loadData().then()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认300px
|
||||
const carouselHeight = carouselData?.height || 300;
|
||||
const carouselHeight = toNumberPx(carouselData?.height, 300)
|
||||
const carouselImages = normalizeAdImages(carouselData)
|
||||
console.log(carouselImages,'carouselImages')
|
||||
|
||||
// 骨架屏组件
|
||||
const BannerSkeleton = () => (
|
||||
@@ -100,12 +124,6 @@ const MyPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex p-2 justify-between bg-white" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图区域 */}
|
||||
<View
|
||||
style={{width: '50%', height: '100%'}}
|
||||
className="banner-swiper-container"
|
||||
>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
@@ -120,14 +138,17 @@ const MyPage = () => {
|
||||
direction="horizontal"
|
||||
className="custom-swiper"
|
||||
>
|
||||
{carouselData && carouselData?.imageList?.map((img, index) => (
|
||||
{carouselImages.map((img, index) => {
|
||||
const src = img.url || img.src || img.imageUrl
|
||||
if (!src) return null
|
||||
return (
|
||||
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
||||
<Image
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={img.url}
|
||||
src={src}
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => navTo(`${img.path}`)}
|
||||
onClick={() => (img.path ? navTo(`${img.path}`) : undefined)}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
@@ -136,58 +157,9 @@ const MyPage = () => {
|
||||
}}
|
||||
/>
|
||||
</Swiper.Item>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* 右侧上下图片区域 - 从API获取数据 */}
|
||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||||
{/* 上层图片 - 使用今日热卖素材 */}
|
||||
<View className={'ml-2 bg-white'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>今日热卖</View>
|
||||
<View className={'px-3 flex justify-between'} style={{
|
||||
height: '94px'
|
||||
}}>
|
||||
{
|
||||
hotToday?.imageList?.map(item => (
|
||||
<View className={'item flex flex-col mr-1'} key={item.url}>
|
||||
<Image
|
||||
width={60}
|
||||
height={60}
|
||||
src={item.url}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo(item.path)}
|
||||
/>
|
||||
<View className={'text-xs my-2 text-orange-600 whitespace-nowrap text-center'}>{item.title || '到手价¥9.9'}</View>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 下层图片 - 使用社区拼团素材 rounded-lg shadow-sm */}
|
||||
<View className={'ml-2 bg-white mt-2'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>{item?.overview || item?.categoryName || '推荐文章'}</View>
|
||||
<View className={'rounded-lg px-3 pb-3'}>
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={94}
|
||||
src={item?.image}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('cms/detail/index?id=' + item?.articleId)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user