refactor(doctor): 重构医生模块为经销商模块并优化相关功能

- 将 doctor 目录重命名为 dealer 目录
- 更新页面标题从'会员注册'为'注册会员'
- 删除银行卡管理、患者报备和订单消息功能
- 重命名组件 AddDoctor 为 AddUserAddress
- 添加用户角色管理和默认角色写入逻辑
- 优化注册成功后跳转至用户中心页面
- 更新应用配置中的页面路径和子包结构
- 添加经销商资金管理、团队管理和二维码推广功能
- 更新租户信息配置,增加租户名称和版权信息
- 优化文章列表组件的类型定义和渲染方式
- 修复广告轮播图数据加载和图片兼容性问题
This commit is contained in:
2026-02-02 19:59:50 +08:00
parent 46f42e08a8
commit 5612f40818
161 changed files with 5129 additions and 5735 deletions

209
README_QR_LOGIN.md Normal file
View 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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
{
"miniprogramRoot": "dist/",
"projectname": "template-5",
"description": "WebSoft Inc.",
"appid": "wx4cd177c96383371d",
"description": "网宿软件",
"appid": "wx541db955e7a62709",
"setting": {
"urlCheck": true,
"es6": false,

View File

@@ -1,7 +1,7 @@
{
"miniprogramRoot": "./",
"projectname": "template-5",
"description": "WebSoft Inc.",
"projectname": "mp-react",
"description": "网宿软件",
"appid": "touristappid",
"setting": {
"urlCheck": true,

View File

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

View File

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

View File

@@ -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,11 +312,9 @@ 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
}
}

View File

@@ -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查询文章
*/

View File

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

View File

@@ -113,7 +113,5 @@ export interface CmsNavigationParam extends PageParam {
parentId?: number;
hide?: number;
model?: string;
home?: number;
position?: number;
keywords?: string;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 应用参数

View File

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

View File

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

View File

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

View File

@@ -31,5 +31,11 @@ export interface ShopDealerCapital {
*/
export interface ShopDealerCapitalParam extends PageParam {
id?: number;
// 仅查询当前分销商的收益/资金明细
userId?: number;
// 可选:按订单过滤
orderId?: number;
// 可选:资金流动类型过滤
flowType?: number;
keywords?: string;
}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import type { PageParam } from '@/api';
/**
* 礼品卡
* 水票
*/
export interface ShopGift {
// 礼品卡ID

View File

@@ -118,7 +118,7 @@ export interface WxPayResult {
*/
export async function createOrder(data: OrderCreateRequest) {
const res = await request.post<ApiResult<WxPayResult>>(
'/shop/shop-order',
'/shop/shop-order',
data
);
if (res.code === 0) {

View File

@@ -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;
// 归属门店IDshop_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[];
// 归属门店IDshop_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[];
// 归属门店IDshop_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;
// 归属门店IDshop_store.id
storeId?: number;
// 配送员用户ID
riderId?: number;
// 发货仓库ID
warehouseId?: number;
keywords?: string;
deliveryStatus?: number;
statusFilter?: number;

View File

@@ -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',
{
params
}
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',
{
params
}
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;

View 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;
// 默认仓库IDshop_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;
}

View File

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

View File

@@ -0,0 +1,67 @@
import type { PageParam } from '@/api';
/**
* 配送员
*/
export interface ShopStoreRider {
// 主键ID
id?: string;
// 配送点IDshop_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;
}

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

View File

@@ -0,0 +1,36 @@
import type { PageParam } from '@/api';
/**
* 店员
*/
export interface ShopStoreUser {
// 主键ID
id?: number;
// 配送点IDshop_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;
}

View File

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

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index';
import type { PageParam } from '@/api';
/**
* 收货地址

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

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

View File

@@ -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
{
params
}
);
if (res.code === 0) {
return res.data;

View File

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

View File

@@ -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))
}
/**
* 获取用户完整仪表板数据(一次性获取所有数据)
*/

View File

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

View File

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

View File

@@ -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('分享成功');

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
title: '礼品卡类型说明',
icon: <Gift size="24" className="text-purple-500" />,
content: [
'🎁 实物礼品:需到指定地址领取商品',
'🎁 礼品:需到指定地址领取商品',
'💻 虚拟礼品卡:自动发放到账户余额',
'🛎️ 服务礼品卡:联系客服预约服务',
'⏰ 注意查看有效期,过期无法使用'

View File

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

View File

@@ -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)
}}
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>
</Sticky>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{ top: '84px'}}
value={tabIndex}
onChange={(page) => {
swiperRef.current?.to(page)
setTabIndex(page)
}}
>
{
tabs?.map((item, index) => {
return <TabPane key={index} title={item.title}></TabPane>
})
}
</Tabs>
<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) => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
{list?.map(item => {
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)}
>
<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>
<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

View File

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

View File

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '会员注册',
navigationBarTitleText: '注册会员',
navigationBarTextStyle: 'black'
})

View File

@@ -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})
console.log(roles, 'roles...')
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)
})
if (roles.length > 0) {
await updateUserRole({
...roles[0],
roleId: 1848
})
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 user 角色
try {
// 1) 先尝试通过 roleCode=user 查询角色ID避免硬编码
// 2) 取不到就回退到旧的默认ID1848
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: 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;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '收益明细'
})

View File

@@ -0,0 +1,2 @@
/* Intentionally empty: styling is done via utility classes. */

View 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

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '分销中心'
})

View File

@@ -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"/>*/}

View File

@@ -15,7 +15,7 @@ const DealerInfo: React.FC = () => {
// 跳转到申请页面
const navigateToApply = () => {
Taro.navigateTo({
url: '/pages/doctor/apply/add'
url: '/pages/dealer/apply/add'
})
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '分销订单'
})

View File

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

View File

@@ -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
Taro.showToast({
title: '保存成功',
icon: 'success'
})
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
try {
await Taro.saveImageToPhotosAlbum({filePath})
} catch (e: any) {
const msg = e?.errMsg || e?.message || ''
// Fallback: some devices/clients may fail to save directly from a temp path.
if (
msg.includes('no such file or directory') &&
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
) {
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
if (saved?.savedFilePath) {
filePath = saved.savedFilePath
}
await Taro.saveImageToPhotosAlbum({filePath})
} else {
throw e
}
}
Taro.showToast({
title: '保存成功',
icon: 'success'
})
} catch (error: any) {
if (error.errMsg?.includes('auth deny')) {
Taro.showModal({
const errMsg = error?.errMsg || error?.message
if (errMsg?.includes('cancel')) {
Taro.showToast({title: '已取消', icon: 'none'})
return
}
if (isAlbumAuthError(errMsg)) {
const modal = await Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (res) => {
if (res.confirm) {
Taro.openSetting()
}
}
confirmText: '去设置'
})
if (modal.confirm) {
await Taro.openSetting()
}
} else {
Taro.showToast({
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
await Taro.showModal({
title: '保存失败',
icon: 'error'
content: errMsg || '保存失败,请稍后重试',
showCancel: false
})
}
} finally {
Taro.hideLoading()
setSaving(false)
}
}
@@ -126,7 +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>

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '邀请推广'
})

View File

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

View 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

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '添加银行卡',
navigationBarTextStyle: 'black'
})

View File

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

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '银行卡管理',
navigationBarTextStyle: 'black'
})

View File

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

View File

@@ -1,108 +0,0 @@
# 客户管理页面
## 功能概述
这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。
## 主要功能
### 1. 数据源
- 使用 `pageUsers` API 从 User 表读取客户数据
- 支持按状态筛选用户status: 0 表示正常状态)
### 2. 状态管理
客户状态包括:
- **全部** - 显示所有客户
- **跟进中** - 正在跟进的潜在客户
- **已签约** - 已经签约的客户
- **已取消** - 已取消合作的客户
### 3. 顶部Tabs筛选
- 支持按客户状态筛选
- 显示每个状态的客户数量统计
- 实时更新统计数据
### 4. 搜索功能
支持多字段搜索:
- 客户姓名realName
- 昵称nickname
- 用户名username
- 手机号phone
- 用户IDuserId
### 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 # 客户状态工具函数
```

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '患者报备',
navigationBarTextStyle: 'black'
})

View File

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

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '患者管理'
})

View File

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

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '入市查询'
})

View File

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

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '医生端'
})

View File

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

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '处方管理'
})

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '患者管理'
})

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '微信客服'
})

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '礼品卡专区',
navigationBarTextStyle: 'black'
})

View File

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

View File

@@ -19,7 +19,7 @@ export const useConfig = () => {
const data = await configWebsiteField();
setConfig(data);
Taro.setStorageSync('config', data);
// 设置主题
if (data.theme && !Taro.getStorageSync('user_theme')) {
Taro.setStorageSync('user_theme', data.theme);
@@ -41,11 +41,10 @@ export const useConfig = () => {
configWebsiteField().then(data => {
setConfig(data);
Taro.setStorageSync('config', data);
setLoading(false);
}).catch(err => {
setError(err instanceof Error ? err : new Error('获取配置失败'));
setLoading(false);
});
}};
};
};

View File

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

View File

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

View File

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

View File

@@ -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)
}, [])

View File

@@ -41,7 +41,7 @@ function Cart() {
useShareAppMessage(() => {
return {
title: '购物车 - WebSoft Inc.',
title: '购物车 - 网宿软件',
success: function () {
console.log('分享成功');
},

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '分类'
})

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '在线开方',
navigationBarTitleText: '文章列表',
navigationBarTextStyle: 'black'
})

View 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

Some files were not shown because too many files have changed in this diff Show More