diff --git a/README_QR_LOGIN.md b/README_QR_LOGIN.md new file mode 100644 index 0000000..e362af2 --- /dev/null +++ b/README_QR_LOGIN.md @@ -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'; + + +``` + +#### 弹窗方式: +```tsx +import QRScanModal from '@/components/QRScanModal'; + + 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 ( + + { + // 处理登录成功 + console.log('用户登录成功:', result); + }} + onError={(error) => { + // 处理登录失败 + console.error('登录失败:', error); + }} + /> + + ); +}; +``` + +### 2. 自定义扫码逻辑 + +```tsx +import { useQRLogin } from '@/hooks/useQRLogin'; + +const MyComponent = () => { + const { + startScan, + isLoading, + isSuccess, + result, + error + } = useQRLogin(); + + return ( + + ); +}; +``` + +## 🔒 安全注意事项 + +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 diff --git a/config/app.ts b/config/app.ts index 31d1864..f53e6bd 100644 --- a/config/app.ts +++ b/config/app.ts @@ -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 ./ diff --git a/config/env.ts b/config/env.ts index c496514..f230b12 100644 --- a/config/env.ts +++ b/config/env.ts @@ -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', } diff --git a/project.config.json b/project.config.json index a1d64ea..35c7b62 100644 --- a/project.config.json +++ b/project.config.json @@ -1,8 +1,8 @@ { "miniprogramRoot": "dist/", "projectname": "template-5", - "description": "WebSoft Inc.", - "appid": "wx4cd177c96383371d", + "description": "网宿软件", + "appid": "wx541db955e7a62709", "setting": { "urlCheck": true, "es6": false, diff --git a/project.tt.json b/project.tt.json index a07e825..11ab567 100644 --- a/project.tt.json +++ b/project.tt.json @@ -1,7 +1,7 @@ { "miniprogramRoot": "./", - "projectname": "template-5", - "description": "WebSoft Inc.", + "projectname": "mp-react", + "description": "网宿软件", "appid": "touristappid", "setting": { "urlCheck": true, diff --git a/src/admin/components/UserCard.tsx b/src/admin/components/UserCard.tsx index a83f124..fb3a433 100644 --- a/src/admin/components/UserCard.tsx +++ b/src/admin/components/UserCard.tsx @@ -237,7 +237,7 @@ function UserCard() {
navTo('/user/gift/index', true)}> - 礼品卡 + 水票 {giftCount}
{/*
*/} diff --git a/src/admin/components/UserCell.tsx b/src/admin/components/UserCell.tsx index 57bb70c..0928980 100644 --- a/src/admin/components/UserCell.tsx +++ b/src/admin/components/UserCell.tsx @@ -36,7 +36,7 @@ const UserCell = () => { backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)', }} title={ - navTo('/doctor/index', true)}> + navTo('/dealer/index', true)}> 开通会员 享优惠 diff --git a/src/api/afterSale.ts b/src/api/afterSale.ts index e285647..faf03f5 100644 --- a/src/api/afterSale.ts +++ b/src/api/afterSale.ts @@ -117,7 +117,7 @@ export const STATUS_COLOR_MAP = { // 申请售后 export const applyAfterSale = async (params: AfterSaleApplyParams): Promise => { try { - const response = await request({ + const response = await request({ url: '/api/after-sale/apply', method: 'POST', data: params @@ -136,7 +136,7 @@ export const getAfterSaleDetail = async (params: { afterSaleId?: string }): Promise => { try { - const response = await request({ + const response = await request({ 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 => { try { - const response = await request({ + const response = await request({ url: '/api/after-sale/list', method: 'GET', data: params @@ -170,7 +170,7 @@ export const getAfterSaleList = async (params: AfterSaleListParams): Promise => { 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 -} +} \ No newline at end of file diff --git a/src/api/cms/cmsArticle/index.ts b/src/api/cms/cmsArticle/index.ts index 9d95918..608c0e5 100644 --- a/src/api/cms/cmsArticle/index.ts +++ b/src/api/cms/cmsArticle/index.ts @@ -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>( - '/cms/cms-article/getByCode/' + code - ); - if (res.code === 0 && res.data) { - return res.data; - } - return Promise.reject(new Error(res.message)); -} - /** * 根据code查询文章 */ diff --git a/src/api/cms/cmsNavigation/index.ts b/src/api/cms/cmsNavigation/index.ts index cd892e2..7eb316f 100644 --- a/src/api/cms/cmsNavigation/index.ts +++ b/src/api/cms/cmsNavigation/index.ts @@ -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>( + const res = await request.get>( '/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)); } + diff --git a/src/api/cms/cmsNavigation/model/index.ts b/src/api/cms/cmsNavigation/model/index.ts index 9452bb2..cdd4f0c 100644 --- a/src/api/cms/cmsNavigation/model/index.ts +++ b/src/api/cms/cmsNavigation/model/index.ts @@ -113,7 +113,5 @@ export interface CmsNavigationParam extends PageParam { parentId?: number; hide?: number; model?: string; - home?: number; - position?: number; keywords?: string; } diff --git a/src/api/cms/cmsWebsiteField/model/index.ts b/src/api/cms/cmsWebsiteField/model/index.ts index 00a9874..ec5967c 100644 --- a/src/api/cms/cmsWebsiteField/model/index.ts +++ b/src/api/cms/cmsWebsiteField/model/index.ts @@ -1,4 +1,4 @@ -import type { PageParam } from '@/api'; +import type { PageParam } from '@/api/index'; /** * 应用参数 diff --git a/src/api/invite/index.ts b/src/api/invite/index.ts index 9dc508e..ba38f75 100644 --- a/src/api/invite/index.ts +++ b/src/api/invite/index.ts @@ -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}`; diff --git a/src/api/shop/shopDealerApply/model/index.ts b/src/api/shop/shopDealerApply/model/index.ts index 3187633..8ea27cc 100644 --- a/src/api/shop/shopDealerApply/model/index.ts +++ b/src/api/shop/shopDealerApply/model/index.ts @@ -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驳回) } diff --git a/src/api/shop/shopDealerBank/model/index.ts b/src/api/shop/shopDealerBank/model/index.ts deleted file mode 100644 index 0fbd93b..0000000 --- a/src/api/shop/shopDealerBank/model/index.ts +++ /dev/null @@ -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; -} diff --git a/src/api/shop/shopDealerCapital/model/index.ts b/src/api/shop/shopDealerCapital/model/index.ts index e6a6bc2..00d29a7 100644 --- a/src/api/shop/shopDealerCapital/model/index.ts +++ b/src/api/shop/shopDealerCapital/model/index.ts @@ -31,5 +31,11 @@ export interface ShopDealerCapital { */ export interface ShopDealerCapitalParam extends PageParam { id?: number; + // 仅查询当前分销商的收益/资金明细 + userId?: number; + // 可选:按订单过滤 + orderId?: number; + // 可选:资金流动类型过滤 + flowType?: number; keywords?: string; } diff --git a/src/api/shop/shopDealerOrder/model/index.ts b/src/api/shop/shopDealerOrder/model/index.ts index 68b4928..76d74a9 100644 --- a/src/api/shop/shopDealerOrder/model/index.ts +++ b/src/api/shop/shopDealerOrder/model/index.ts @@ -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; } diff --git a/src/api/shop/shopDealerWithdraw/index.ts b/src/api/shop/shopDealerWithdraw/index.ts index 6968031..398b9be 100644 --- a/src/api/shop/shopDealerWithdraw/index.ts +++ b/src/api/shop/shopDealerWithdraw/index.ts @@ -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>( +export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise { + const res = await request.post>( '/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 { + const res = await request.post>( + '/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>( + '/shop/shop-dealer-withdraw/receive-success/' + id, + {} + ); if (res.code === 0) { return res.message; } diff --git a/src/api/shop/shopGift/model/index.ts b/src/api/shop/shopGift/model/index.ts index 93713a6..f3c7450 100644 --- a/src/api/shop/shopGift/model/index.ts +++ b/src/api/shop/shopGift/model/index.ts @@ -1,7 +1,7 @@ import type { PageParam } from '@/api'; /** - * 礼品卡 + * 水票 */ export interface ShopGift { // 礼品卡ID diff --git a/src/api/shop/shopOrder/index.ts b/src/api/shop/shopOrder/index.ts index c2637c4..ca1e805 100644 --- a/src/api/shop/shopOrder/index.ts +++ b/src/api/shop/shopOrder/index.ts @@ -118,7 +118,7 @@ export interface WxPayResult { */ export async function createOrder(data: OrderCreateRequest) { const res = await request.post>( - '/shop/shop-order', + '/shop/shop-order', data ); if (res.code === 0) { diff --git a/src/api/shop/shopOrder/model/index.ts b/src/api/shop/shopOrder/model/index.ts index 92709f8..b8afaa1 100644 --- a/src/api/shop/shopOrder/model/index.ts +++ b/src/api/shop/shopOrder/model/index.ts @@ -1,5 +1,5 @@ import type { PageParam } from '@/api/index'; -import {OrderGoods} from "@/api/system/orderGoods/model"; +import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model'; /** * 订单 @@ -27,6 +27,14 @@ export interface ShopOrder { merchantName?: string; // 商户编号 merchantCode?: string; + // 归属门店ID(shop_store.id) + storeId?: number; + // 归属门店名称 + storeName?: string; + // 配送员用户ID(优先级派单) + riderId?: number; + // 发货仓库ID + warehouseId?: number; // 使用的优惠券id couponId?: number; // 使用的会员卡id @@ -61,6 +69,8 @@ export interface ShopOrder { sendStartTime?: string; // 配送结束时间 sendEndTime?: string; + // 配送员送达拍照(选填) + sendEndImg?: string; // 发货店铺id expressMerchantId?: number; // 发货店铺 @@ -146,7 +156,7 @@ export interface ShopOrder { // 是否已收到赠品 hasTakeGift?: string; // 订单商品项 - orderGoods?: OrderGoods[]; + orderGoods?: ShopOrderGoods[]; } /** @@ -165,6 +175,14 @@ export interface OrderGoodsItem { export interface OrderCreateRequest { // 商品信息列表 goodsItems: OrderGoodsItem[]; + // 归属门店ID(shop_store.id) + storeId?: number; + // 归属门店名称(可选) + storeName?: string; + // 配送员用户ID(优先级派单) + riderId?: number; + // 发货仓库ID + warehouseId?: number; // 收货地址ID addressId?: number; // 支付方式 @@ -197,6 +215,14 @@ export interface OrderGoodsItem { export interface OrderCreateRequest { // 商品信息列表 goodsItems: OrderGoodsItem[]; + // 归属门店ID(shop_store.id) + storeId?: number; + // 归属门店名称(可选) + storeName?: string; + // 配送员用户ID(优先级派单) + riderId?: number; + // 发货仓库ID + warehouseId?: number; // 收货地址ID addressId?: number; // 支付方式 @@ -223,6 +249,12 @@ export interface ShopOrderParam extends PageParam { payType?: number; isInvoice?: boolean; userId?: number; + // 归属门店ID(shop_store.id) + storeId?: number; + // 配送员用户ID + riderId?: number; + // 发货仓库ID + warehouseId?: number; keywords?: string; deliveryStatus?: number; statusFilter?: number; diff --git a/src/api/shop/shopUser/index.ts b/src/api/shop/shopStore/index.ts similarity index 51% rename from src/api/shop/shopUser/index.ts rename to src/api/shop/shopStore/index.ts index 45184f8..e67303f 100644 --- a/src/api/shop/shopUser/index.ts +++ b/src/api/shop/shopStore/index.ts @@ -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>>( - '/shop/shop-user/page', - { - params - } +export async function pageShopStore(params: ShopStoreParam) { + const res = await request.get>>( + '/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>( - '/shop/shop-user', - { - params - } +export async function listShopStore(params?: ShopStoreParam) { + const res = await request.get>( + '/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>( - '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>( - '/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>( - '/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>( - '/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>( - '/shop/shop-user/' + userId +export async function getShopStore(id: number) { + const res = await request.get>( + '/shop/shop-store/' + id ); if (res.code === 0 && res.data) { return res.data; diff --git a/src/api/shop/shopStore/model/index.ts b/src/api/shop/shopStore/model/index.ts new file mode 100644 index 0000000..3e5bcd1 --- /dev/null +++ b/src/api/shop/shopStore/model/index.ts @@ -0,0 +1,63 @@ +import type { PageParam } from '@/api'; + +/** + * 门店 + */ +export interface ShopStore { + // 自增ID + id?: number; + // 店铺名称 + name?: string; + // 门店地址 + address?: string; + // 手机号码 + phone?: string; + // 邮箱 + email?: string; + // 门店经理 + managerName?: string; + // 门店banner + shopBanner?: string; + // 所在省份 + province?: string; + // 所在城市 + city?: string; + // 所在辖区 + region?: string; + // 经度和纬度 + lngAndLat?: string; + // 位置 + location?:string; + // 区域 + district?: string; + // 轮廓 + points?: string; + // 用户ID + userId?: number; + // 默认仓库ID(shop_warehouse.id) + warehouseId?: number; + // 默认仓库名称(可选) + warehouseName?: string; + // 状态 + status?: number; + // 备注 + comments?: string; + // 排序号 + sortNumber?: number; + // 是否删除 + isDelete?: number; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 门店搜索条件 + */ +export interface ShopStoreParam extends PageParam { + id?: number; + keywords?: string; +} diff --git a/src/api/shop/shopDealerBank/index.ts b/src/api/shop/shopStoreRider/index.ts similarity index 52% rename from src/api/shop/shopDealerBank/index.ts rename to src/api/shop/shopStoreRider/index.ts index eca6fea..c411aef 100644 --- a/src/api/shop/shopDealerBank/index.ts +++ b/src/api/shop/shopStoreRider/index.ts @@ -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>>( - '/shop/shop-dealer-bank/page', +export async function pageShopStoreRider(params: ShopStoreRiderParam) { + const res = await request.get>>( + '/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>( - '/shop/shop-dealer-bank', +export async function listShopStoreRider(params?: ShopStoreRiderParam) { + const res = await request.get>( + '/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>( - '/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>( - '/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>( - '/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>( - '/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>( - '/shop/shop-dealer-bank/' + id +export async function getShopStoreRider(id: number) { + const res = await request.get>( + '/shop/shop-store-rider/' + id ); if (res.code === 0 && res.data) { return res.data; diff --git a/src/api/shop/shopStoreRider/model/index.ts b/src/api/shop/shopStoreRider/model/index.ts new file mode 100644 index 0000000..26e51f4 --- /dev/null +++ b/src/api/shop/shopStoreRider/model/index.ts @@ -0,0 +1,67 @@ +import type { PageParam } from '@/api'; + +/** + * 配送员 + */ +export interface ShopStoreRider { + // 主键ID + id?: string; + // 配送点ID(shop_dealer.id) + dealerId?: number; + // 骑手编号(可选) + riderNo?: string; + // 姓名 + realName?: string; + // 手机号 + mobile?: string; + // 头像 + avatar?: string; + // 身份证号(可选) + idCardNo?: string; + // 状态:1启用;0禁用 + status?: number; + // 接单状态:0休息/下线;1在线;2忙碌 + workStatus?: number; + // 是否开启自动派单:1是;0否 + autoDispatchEnabled?: number; + // 派单优先级(同小区多骑手时可用,值越大越优先) + dispatchPriority?: number; + // 最大同时配送单数(0表示不限制) + maxOnhandOrders?: number; + // 是否计算工资(提成):1计算;0不计算(如三方配送点可设0) + commissionCalcEnabled?: number; + // 水每桶提成金额(元/桶) + waterBucketUnitFee?: string; + // 其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表) + otherGoodsCommissionType?: number; + // 其他商品提成值:固定金额(元)或比例(%) + otherGoodsCommissionValue?: string; + // 用户ID + userId?: number; + // 备注 + comments?: string; + // 排序号 + sortNumber?: number; + // 是否删除 + isDelete?: number; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 配送员搜索条件 + */ +export interface ShopStoreRiderParam extends PageParam { + id?: number; + keywords?: string; + // 配送点/门店ID(后端可能用 dealerId 或 storeId) + dealerId?: number; + storeId?: number; + status?: number; + workStatus?: number; + autoDispatchEnabled?: number; +} diff --git a/src/api/shop/shopStoreUser/index.ts b/src/api/shop/shopStoreUser/index.ts new file mode 100644 index 0000000..0e500ab --- /dev/null +++ b/src/api/shop/shopStoreUser/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-store-user/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopStoreUser/model/index.ts b/src/api/shop/shopStoreUser/model/index.ts new file mode 100644 index 0000000..46151f5 --- /dev/null +++ b/src/api/shop/shopStoreUser/model/index.ts @@ -0,0 +1,36 @@ +import type { PageParam } from '@/api'; + +/** + * 店员 + */ +export interface ShopStoreUser { + // 主键ID + id?: number; + // 配送点ID(shop_dealer.id) + storeId?: number; + // 用户ID + userId?: number; + // 备注 + comments?: string; + // 排序号 + sortNumber?: number; + // 是否删除 + isDelete?: number; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 店员搜索条件 + */ +export interface ShopStoreUserParam extends PageParam { + id?: number; + keywords?: string; + storeId?: number; + userId?: number; + isDelete?: number; +} diff --git a/src/api/shop/shopUser/model/index.ts b/src/api/shop/shopUser/model/index.ts deleted file mode 100644 index 30a0e16..0000000 --- a/src/api/shop/shopUser/model/index.ts +++ /dev/null @@ -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; -} diff --git a/src/api/shop/shopUserAddress/model/index.ts b/src/api/shop/shopUserAddress/model/index.ts index fff978c..9bf7f50 100644 --- a/src/api/shop/shopUserAddress/model/index.ts +++ b/src/api/shop/shopUserAddress/model/index.ts @@ -1,4 +1,4 @@ -import type { PageParam } from '@/api/index'; +import type { PageParam } from '@/api'; /** * 收货地址 diff --git a/src/api/shop/shopWarehouse/index.ts b/src/api/shop/shopWarehouse/index.ts new file mode 100644 index 0000000..59a5f6e --- /dev/null +++ b/src/api/shop/shopWarehouse/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-warehouse/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopWarehouse/model/index.ts b/src/api/shop/shopWarehouse/model/index.ts new file mode 100644 index 0000000..4b96a81 --- /dev/null +++ b/src/api/shop/shopWarehouse/model/index.ts @@ -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; +} diff --git a/src/api/system/dict/index.ts b/src/api/system/dict/index.ts index a017f6a..0076c39 100644 --- a/src/api/system/dict/index.ts +++ b/src/api/system/dict/index.ts @@ -9,7 +9,9 @@ import {SERVER_API_URL} from "@/utils/server"; export async function listDictionaries(params?: DictParam) { const res = await request.get>( SERVER_API_URL + '/system/dict', - params + { + params + } ); if (res.code === 0) { return res.data; diff --git a/src/api/system/userRole/index.ts b/src/api/system/userRole/index.ts index 3dfa2e1..ecaefcc 100644 --- a/src/api/system/userRole/index.ts +++ b/src/api/system/userRole/index.ts @@ -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>( + SERVER_API_URL + '/system/user-role', + data + ); + if (res.code === 0) { + return res.message; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/user/index.ts b/src/api/user/index.ts index 7189f58..8f8b3c6 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -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>('/user/card/stats') + if (res.code === 0 && res.data) { + return res.data + } + return Promise.reject(new Error(res.message)) +} + /** * 获取用户完整仪表板数据(一次性获取所有数据) */ diff --git a/src/app.config.ts b/src/app.config.ts index ae304e4..67a1f2b 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -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": "用于保存小程序码到相册,方便分享给好友" } } } diff --git a/src/assets/tabbar/category-active.png b/src/assets/tabbar/category-active.png index eebe3d5..a7431dc 100644 Binary files a/src/assets/tabbar/category-active.png and b/src/assets/tabbar/category-active.png differ diff --git a/src/assets/tabbar/category.png b/src/assets/tabbar/category.png index 1f955de..cc56063 100644 Binary files a/src/assets/tabbar/category.png and b/src/assets/tabbar/category.png differ diff --git a/src/assets/tabbar/tv-active.png b/src/assets/tabbar/tv-active.png deleted file mode 100644 index 4852b4f..0000000 Binary files a/src/assets/tabbar/tv-active.png and /dev/null differ diff --git a/src/assets/tabbar/tv.png b/src/assets/tabbar/tv.png deleted file mode 100644 index 2dd6bd0..0000000 Binary files a/src/assets/tabbar/tv.png and /dev/null differ diff --git a/src/cms/category/components/ArticleList.tsx b/src/cms/category/components/ArticleList.tsx index ffe5d29..cde08b4 100644 --- a/src/cms/category/components/ArticleList.tsx +++ b/src/cms/category/components/ArticleList.tsx @@ -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 ( <> - - {props.data.map((item: any, index: number) => { +
+ {props.data.map((item: CmsArticle, index: number) => { return ( - {item.title} - {item.comments && ( - - {item.comments} - - )} - - } + title={item.title} extra={ } @@ -28,7 +19,7 @@ const ArticleList = (props: any) => { /> ) })} - +
) } diff --git a/src/cms/category/index.tsx b/src/cms/category/index.tsx index 0ac2267..75a784b 100644 --- a/src/cms/category/index.tsx +++ b/src/cms/category/index.tsx @@ -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('分享成功'); diff --git a/src/cms/detail/index.tsx b/src/cms/detail/index.tsx index f92c890..8d0c717 100644 --- a/src/cms/detail/index.tsx +++ b/src/cms/detail/index.tsx @@ -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() {
{item?.title}
{item?.createTime}
+ {/*如果有视频就显示视频 视频沾满宽度*/} + {item?.video && + + } diff --git a/src/components/GiftCard.tsx b/src/components/GiftCard.tsx index b6f89e6..55a9824 100644 --- a/src/components/GiftCard.tsx +++ b/src/components/GiftCard.tsx @@ -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 = ({ // 获取礼品卡类型文本 const getTypeText = () => { switch (type) { - case 10: return '实物礼品卡' + case 10: return '礼品劵' case 20: return '虚拟礼品卡' case 30: return '服务礼品卡' - default: return '礼品卡' + default: return '水票' } } diff --git a/src/components/GiftCardGuide.tsx b/src/components/GiftCardGuide.tsx index 6b1db57..b1e7eb1 100644 --- a/src/components/GiftCardGuide.tsx +++ b/src/components/GiftCardGuide.tsx @@ -51,7 +51,7 @@ const GiftCardGuide: React.FC = ({ title: '礼品卡类型说明', icon: , content: [ - '🎁 实物礼品卡:需到指定地址领取商品', + '🎁 礼品劵:需到指定地址领取商品', '💻 虚拟礼品卡:自动发放到账户余额', '🛎️ 服务礼品卡:联系客服预约服务', '⏰ 注意查看有效期,过期无法使用' diff --git a/src/components/GiftCardShare.tsx b/src/components/GiftCardShare.tsx index ea4e217..0d29302 100644 --- a/src/components/GiftCardShare.tsx +++ b/src/components/GiftCardShare.tsx @@ -28,10 +28,10 @@ const GiftCardShare: React.FC = ({ // 获取礼品卡类型文本 const getTypeText = () => { switch (giftCard.type) { - case 10: return '实物礼品卡' + case 10: return '礼品劵' case 20: return '虚拟礼品卡' case 30: return '服务礼品卡' - default: return '礼品卡' + default: return '水票' } } diff --git a/src/components/GoodsList.tsx b/src/components/GoodsList.tsx index 1ada7ca..3777b6a 100644 --- a/src/components/GoodsList.tsx +++ b/src/components/GoodsList.tsx @@ -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('0') - const [list, setList] = useState([]) - const [goods, setGoods] = useState() - const [stickyStatus, setStickyStatus] = useState(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([]) + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) + const swiperRef = useRef | null>(null) + const [tabIndex, setTabIndex] = useState(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 ( <> - - {/* Tabs粘性布局组件 */} - - { - setTab1value(value) - }} - style={{ - backgroundColor: 'transparent', - paddingTop: stickyStatus ? '0' : '8px', - paddingBottom: stickyStatus ? '8px' : '0', - }} - activeType="smile" - > - - - - - - - - + { + swiperRef.current?.to(page) + setTabIndex(page) + }} + > + { + tabs?.map((item, index) => { + return + }) + } + +
+ { - - {/* 今日主推 */} - {tab1value == '0' && list?.map((item, index) => { + }} + onScrollToUpper={() => { + + }} + loadingText={ + <> + 加载中 + + } + loadMoreText={ + <> + 没有更多了 + + } + > + {list?.map(item => { return ( - - Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> - - - {item.name} - - {item.comments} - 已售 {item.sales} - - - - - {item.price} - - - - handleShare(item)} - > - - - - Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买 - - - - - - + + +
+ copyText(`${item.orderNo}`)}>{item.orderNo} + 待付款 +
+
{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}
+
+
+
+ +
{item.realName}
+
+
{item.totalNum}件商品
+
+
+
实付金额:¥{item.payPrice}
+ + + + +
+
) })} - - {/* 即将到期 */} - {tab1value == '1' && ( - - )} - - {/* 活动预告 */} - {tab1value == '2' && ( - - )} -
- +
+
) } -export default BestSellers + +export default GoodsList diff --git a/src/components/SimpleQRCodeModal.tsx b/src/components/SimpleQRCodeModal.tsx index 167c8cc..1acde23 100644 --- a/src/components/SimpleQRCodeModal.tsx +++ b/src/components/SimpleQRCodeModal.tsx @@ -81,7 +81,7 @@ const SimpleQRCodeModal: React.FC = ({ {qrContent ? ( 二维码 { - const {user, loginUser} = useUser() +const AddUserAddress = () => { + const {user, loginUser, fetchUserInfo} = useUser() const [loading, setLoading] = useState(true) const [FormData, setFormData] = useState() const formRef = useRef(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) 取不到就回退到旧的默认ID(1848) + let userRoleId: number | undefined; + try { + // 注意:当前 request.get 的封装不支持 axios 风格的 { params: ... }, + // 某些自动生成的 API 可能无法按参数过滤;这里直接取全量再本地查找更稳。 + const roleList = await listRoles(); + userRoleId = roleList?.find(r => r.roleCode === 'user')?.roleId; + } catch (_) { + // ignore + } + if (!userRoleId) userRoleId = 1848; + + const baseRolePayload = { + userId: user.userId, + tenantId: Number(TenantId), + roleId: userRoleId + }; + + // 后端若已创建 user-role 记录则更新;否则尝试“无id更新”触发创建(多数实现会 upsert) + if (roles.length > 0) { + await updateUserRole({ + ...roles[0], + roleId: 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 = () => { > - - - + {/**/} + {/* */} + {/**/} { ); }; -export default AddDoctor; +export default AddUserAddress; diff --git a/src/dealer/capital/index.config.ts b/src/dealer/capital/index.config.ts new file mode 100644 index 0000000..8e2e153 --- /dev/null +++ b/src/dealer/capital/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '收益明细' +}) + diff --git a/src/dealer/capital/index.scss b/src/dealer/capital/index.scss new file mode 100644 index 0000000..5e9d65f --- /dev/null +++ b/src/dealer/capital/index.scss @@ -0,0 +1,2 @@ +/* Intentionally empty: styling is done via utility classes. */ + diff --git a/src/dealer/capital/index.tsx b/src/dealer/capital/index.tsx new file mode 100644 index 0000000..b599fb9 --- /dev/null +++ b/src/dealer/capital/index.tsx @@ -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([]) + + 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 ( + + + 加载中... + + ) + } + + return ( + + + + + {loading && records.length === 0 ? ( + + + 加载中... + + ) : records.length > 0 ? ( + <> + {records.map((item) => ( + + + + {item.describe || '收益明细'} + + + {getFlowTypeText(item.flowType)} + + + + + + 佣金收入 + + + {formatMoney(item.flowType, item.money)} + + + + + + {/*用户:{item.userId ?? '-'}*/} + + + {item.createTime || '-'} + + + + ))} + + {loadingMore && ( + + + 加载更多... + + )} + {!hasMore && records.length > 0 && ( + + 没有更多数据了 + + )} + + ) : ( + + )} + + + + + ) +} + +export default DealerCapital diff --git a/src/dealer/index.config.ts b/src/dealer/index.config.ts new file mode 100644 index 0000000..d456dbd --- /dev/null +++ b/src/dealer/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '分销中心' +}) diff --git a/src/doctor/index.scss b/src/dealer/index.scss similarity index 100% rename from src/doctor/index.scss rename to src/dealer/index.scss diff --git a/src/doctor/index.tsx b/src/dealer/index.tsx similarity index 60% rename from src/doctor/index.tsx rename to src/dealer/index.tsx index 834cb7e..bdf8b3e 100644 --- a/src/doctor/index.tsx +++ b/src/dealer/index.tsx @@ -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 ( - // - // - // {error} - // - // - // - // ) - // } + if (error) { + return ( + + + {error} + + + + ) + } return ( @@ -106,12 +103,12 @@ const DealerIndex: React.FC = () => { - {dealerUser?.realName || '医生名称'} + {dealerUser?.realName || '分销商'} - 医生编号: {dealerUser.userId} + ID: {dealerUser.userId} @@ -128,9 +125,80 @@ const DealerIndex: React.FC = () => { )} + {/* 佣金统计卡片 */} + {dealerUser && ( + + + 佣金统计 + + + + + {formatMoney(dealerUser.money)} + + 可提现 + + + + {formatMoney(dealerUser.freezeMoney)} + + 冻结中 + + + + {formatMoney(dealerUser.totalMoney)} + + 累计收益 + + + + )} + + {/* 团队统计 */} + {dealerUser && ( + + + 我的邀请 + navigateToPage('/dealer/team/index')} + > + 查看详情 + + + + + + + {dealerUser.firstNum || 0} + + 一级成员 + + + + {dealerUser.secondNum || 0} + + 二级成员 + + + + {dealerUser.thirdNum || 0} + + 三级成员 + + + + )} + {/* 功能导航 */} - 管理工具 + 分销工具 { border: 'none' } as React.CSSProperties} > - navigateToPage('/doctor/customer/index')}> + navigateToPage('/dealer/orders/index')}> - + - navigateToPage('/doctor/orders/add')}> + navigateToPage('/dealer/withdraw/index')}> - + - navigateToPage('/doctor/team/index')}> + navigateToPage('/dealer/team/index')}> - + - navigateToPage('/doctor/orders/index')}> + navigateToPage('/dealer/qrcode/index')}> - + - - - - - - - - - - navigateToPage('/doctor/team/index')}> - - - - - - - - navigateToPage('/doctor/qrcode/index')}> - - - - - - - - navigateToPage('/doctor/apply/add')}> - - - - - - - {/* 第二行功能 */} @@ -217,7 +252,7 @@ const DealerIndex: React.FC = () => { {/* border: 'none'*/} {/* } as React.CSSProperties}*/} {/*>*/} - {/* navigateToPage('/doctor/invite-stats/index')}>*/} + {/* navigateToPage('/dealer/invite-stats/index')}>*/} {/* */} {/* */} {/* */} diff --git a/src/doctor/info.tsx b/src/dealer/info.tsx similarity index 99% rename from src/doctor/info.tsx rename to src/dealer/info.tsx index 8e03a69..0f18841 100644 --- a/src/doctor/info.tsx +++ b/src/dealer/info.tsx @@ -15,7 +15,7 @@ const DealerInfo: React.FC = () => { // 跳转到申请页面 const navigateToApply = () => { Taro.navigateTo({ - url: '/pages/doctor/apply/add' + url: '/pages/dealer/apply/add' }) } diff --git a/src/doctor/invite-stats/index.config.ts b/src/dealer/invite-stats/index.config.ts similarity index 100% rename from src/doctor/invite-stats/index.config.ts rename to src/dealer/invite-stats/index.config.ts diff --git a/src/doctor/invite-stats/index.tsx b/src/dealer/invite-stats/index.tsx similarity index 100% rename from src/doctor/invite-stats/index.tsx rename to src/dealer/invite-stats/index.tsx diff --git a/src/dealer/orders/index.config.ts b/src/dealer/orders/index.config.ts new file mode 100644 index 0000000..3bb5694 --- /dev/null +++ b/src/dealer/orders/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '分销订单' +}) diff --git a/src/doctor/orders/index.tsx b/src/dealer/orders/index.tsx similarity index 79% rename from src/doctor/orders/index.tsx rename to src/dealer/orders/index.tsx index 196e715..7fbe6d8 100644 --- a/src/doctor/orders/index.tsx +++ b/src/dealer/orders/index.tsx @@ -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) => ( - + - 订单号:{order.orderNo} + 订单号:{order.orderNo || '-'} {getStatusText(order.isSettled, order.isInvalid)} - - - 订单金额:¥{order.orderPrice || '0.00'} - - - 我的佣金:¥{order.userCommission} - - + {/**/} + {/* */} + {/* 订单金额:¥{order.orderPrice || '0.00'}*/} + {/* */} + {/**/} - 客户:{order.customerName} + {order.createTime} - {order.createTime} + 订单金额:¥{order.orderPrice || '0.00'} ) + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + return ( { )} ) : ( - + )} diff --git a/src/doctor/qrcode/index.config.ts b/src/dealer/qrcode/index.config.ts similarity index 100% rename from src/doctor/qrcode/index.config.ts rename to src/dealer/qrcode/index.config.ts diff --git a/src/doctor/qrcode/index.tsx b/src/dealer/qrcode/index.tsx similarity index 76% rename from src/doctor/qrcode/index.tsx rename to src/dealer/qrcode/index.tsx index 268580a..5cf152d 100644 --- a/src/doctor/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -11,6 +11,7 @@ import {businessGradients} from '@/styles/gradients' const DealerQrcode: React.FC = () => { const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState('') const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) // const [inviteStats, setInviteStats] = useState(null) // const [statsLoading, setStatsLoading] = useState(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 => { + 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 => { + // 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 = {} + 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={} onClick={saveMiniProgramCode} - disabled={!miniProgramCodeUrl || loading} + disabled={!miniProgramCodeUrl || loading || saving} > 保存小程序码到相册 diff --git a/src/dealer/team/index.config.ts b/src/dealer/team/index.config.ts new file mode 100644 index 0000000..ddd6b66 --- /dev/null +++ b/src/dealer/team/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '邀请推广' +}) diff --git a/src/doctor/team/index.tsx b/src/dealer/team/index.tsx similarity index 99% rename from src/doctor/team/index.tsx rename to src/dealer/team/index.tsx index 36f6b14..af7d8e7 100644 --- a/src/doctor/team/index.tsx +++ b/src/dealer/team/index.tsx @@ -407,7 +407,7 @@ const DealerTeam: React.FC = () => { navTo(`/doctor/apply/add`, true)}]} + }} actions={[{text: '立即申请', onClick: () => navTo(`/dealer/apply/add`, true)}]} /> ) @@ -431,7 +431,7 @@ const DealerTeam: React.FC = () => { )} - navTo(`/doctor/qrcode/index`, true)}/> + navTo(`/dealer/qrcode/index`, true)}/> ) } diff --git a/src/doctor/withdraw/debug.tsx b/src/dealer/withdraw/debug.tsx similarity index 100% rename from src/doctor/withdraw/debug.tsx rename to src/dealer/withdraw/debug.tsx diff --git a/src/doctor/withdraw/index.config.ts b/src/dealer/withdraw/index.config.ts similarity index 100% rename from src/doctor/withdraw/index.config.ts rename to src/dealer/withdraw/index.config.ts diff --git a/src/dealer/withdraw/index.tsx b/src/dealer/withdraw/index.tsx new file mode 100644 index 0000000..c6ce532 --- /dev/null +++ b/src/dealer/withdraw/index.tsx @@ -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 => { + 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('0') + const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [claimingId, setClaimingId] = useState(null) + const [availableAmount, setAvailableAmount] = useState('0.00') + const [withdrawRecords, setWithdrawRecords] = useState([]) + const formRef = useRef(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 = () => ( + + {/* 余额卡片 */} + + {/* 装饰背景 - 小程序兼容版本 */} + + + + + {formatMoney(dealerUser?.money)} + 可提现余额 + + + + + + + + 最低提现金额:¥100 | 手续费:免费 + + + + +
+ + + + + + {/* 快捷金额 */} + + 快捷金额 + + {quickAmounts.map(amount => ( + + ))} + + + + + + + 提现方式:微信钱包(提交后进入“待审核”,审核通过后请到“提现记录”点击“领取到微信零钱”完成收款确认) + + + + + + + +
+
+ ) + + const renderWithdrawRecords = () => { + console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId}) + + return ( + + + {loading ? ( + + + 加载中... + + ) : withdrawRecords.length > 0 ? ( + withdrawRecords.map(record => ( + + + + + 提现金额:¥{record.money} + + {/**/} + {/* 提现账户:{record.accountDisplay}*/} + {/**/} + + + {getStatusText(record.applyStatus)} + + + + + {record.applyStatus === 20 && record.payType === 10 && ( + + + + )} + + + + 创建时间:{record.createTime} + {record.auditTime && ( + + 审核时间:{record.auditTime} + + )} + {record.rejectReason && ( + + 驳回原因:{record.rejectReason} + + )} + + + + + )) + ) : ( + + )} + + + ) + } + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + + return ( + + + + {renderWithdrawForm()} + + + + {renderWithdrawRecords()} + + + + ) +} + +export default DealerWithdraw diff --git a/src/doctor/bank/add.config.ts b/src/doctor/bank/add.config.ts deleted file mode 100644 index e07ffad..0000000 --- a/src/doctor/bank/add.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '添加银行卡', - navigationBarTextStyle: 'black' -}) diff --git a/src/doctor/bank/add.tsx b/src/doctor/bank/add.tsx deleted file mode 100644 index ef11e80..0000000 --- a/src/doctor/bank/add.tsx +++ /dev/null @@ -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(true) - const [FormData, setFormData] = useState() - const formRef = useRef(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 加载中 - } - - return ( - <> -
submitSucceed(values)} - onFinishFailed={(errors) => submitFailed(errors)} - > - - - - - - - - - - - -
- - {/* 底部浮动按钮 */} - formRef.current?.submit()}/> - - ); -}; - -export default AddUserAddress; diff --git a/src/doctor/bank/index.config.ts b/src/doctor/bank/index.config.ts deleted file mode 100644 index 8f92036..0000000 --- a/src/doctor/bank/index.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '银行卡管理', - navigationBarTextStyle: 'black' -}) diff --git a/src/doctor/bank/index.tsx b/src/doctor/bank/index.tsx deleted file mode 100644 index 9587c72..0000000 --- a/src/doctor/bank/index.tsx +++ /dev/null @@ -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([]) - const [bank, setAddress] = useState() - - 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 ( - -
- - - - - -
-
- ) - } - - return ( - - {list.map((item, _) => ( - - selectAddress(item)}> - - {item.bankName} - - - {item.bankCard} {item.bankAccount} - - - onDefault(item)}> - {item.isDefault ? : } - 选择 - - } - extra={ - <> - onDel(item.id)}> - 删除 - - - } - /> - - ))} - {/* 底部浮动按钮 */} - Taro.navigateTo({url: '/doctor/bank/add'})} /> -
- ); -}; - -export default DealerBank; diff --git a/src/doctor/customer/README.md b/src/doctor/customer/README.md deleted file mode 100644 index 14cb5ef..0000000 --- a/src/doctor/customer/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# 客户管理页面 - -## 功能概述 - -这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。 - -## 主要功能 - -### 1. 数据源 -- 使用 `pageUsers` API 从 User 表读取客户数据 -- 支持按状态筛选用户(status: 0 表示正常状态) - -### 2. 状态管理 -客户状态包括: -- **全部** - 显示所有客户 -- **跟进中** - 正在跟进的潜在客户 -- **已签约** - 已经签约的客户 -- **已取消** - 已取消合作的客户 - -### 3. 顶部Tabs筛选 -- 支持按客户状态筛选 -- 显示每个状态的客户数量统计 -- 实时更新统计数据 - -### 4. 搜索功能 -支持多字段搜索: -- 客户姓名(realName) -- 昵称(nickname) -- 用户名(username) -- 手机号(phone) -- 用户ID(userId) - -### 5. 客户信息展示 -每个客户卡片显示: -- 客户姓名和状态标签 -- 手机号码 -- 注册时间 -- 用户ID、余额、积分等统计信息 - -## 技术实现 - -### 组件结构 -``` -CustomerManagement -├── 搜索栏 (SearchBar) -├── 状态筛选Tabs -└── 客户列表 - └── 客户卡片项 -``` - -### 主要状态 -- `list`: 客户数据列表 -- `loading`: 加载状态 -- `activeTab`: 当前选中的状态Tab -- `searchValue`: 搜索关键词 - -### 工具函数 -使用 `@/utils/customerStatus` 工具函数管理客户状态: -- `getStatusText()`: 获取状态文本 -- `getStatusTagType()`: 获取状态标签类型 -- `getStatusOptions()`: 获取状态选项列表 - -## 使用的组件 - -### NutUI 组件 -- `Tabs` / `TabPane`: 状态筛选标签页 -- `SearchBar`: 搜索输入框 -- `Tag`: 状态标签 -- `Loading`: 加载指示器 -- `Space`: 间距布局 - -### 图标 -- `Phone`: 手机号图标 -- `User`: 用户图标 - -## 数据流 - -1. 页面初始化时调用 `fetchCustomerData()` 获取用户数据 -2. 为每个用户添加客户状态(目前使用随机状态,实际项目中应从数据库获取) -3. 根据当前Tab和搜索条件筛选数据 -4. 渲染客户列表 - -## 注意事项 - -### 临时实现 -- 当前使用 `getRandomStatus()` 生成随机客户状态 -- 实际项目中应该: - 1. 在数据库中添加客户状态字段 - 2. 修改后端API返回真实的客户状态 - 3. 删除随机状态生成函数 - -### 扩展建议 -1. 添加客户详情页面 -2. 支持客户状态的修改操作 -3. 添加客户添加/编辑功能 -4. 支持批量操作 -5. 添加导出功能 -6. 支持更多筛选条件(注册时间、地区等) - -## 文件结构 -``` -src/doctor/customer/ -├── index.tsx # 主页面组件 -└── README.md # 说明文档 - -src/utils/ -└── customerStatus.ts # 客户状态工具函数 -``` diff --git a/src/doctor/customer/add.config.ts b/src/doctor/customer/add.config.ts deleted file mode 100644 index 596fab1..0000000 --- a/src/doctor/customer/add.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '患者报备', - navigationBarTextStyle: 'black' -}) diff --git a/src/doctor/customer/add.tsx b/src/doctor/customer/add.tsx deleted file mode 100644 index d91c3b0..0000000 --- a/src/doctor/customer/add.tsx +++ /dev/null @@ -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(true) - const [FormData, setFormData] = useState() - const formRef = useRef(null) - const [isEditMode, setIsEditMode] = useState(false) - const [existingApply, setExistingApply] = useState(null) - - // 日期选择器状态 - const [showApplyTimePicker, setShowApplyTimePicker] = useState(false) - const [showContractTimePicker, setShowContractTimePicker] = useState(false) - const [applyTime, setApplyTime] = useState('') - const [contractTime, setContractTime] = useState('') - - // 获取审核状态文字 - 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((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 加载中 - } - - return ( - <> -
submitSucceed(values)} - onFinishFailed={(errors) => submitFailed(errors)} - > - - - - - - - - - - - - - - - - - - - - - {isEditMode && ( - <> - - - - - setShowApplyTimePicker(true)} - > - - - - {applyTime ? formatDateForDisplay(applyTime) : '请选择签约时间'} - - - - - - setShowContractTimePicker(true)} - > - - - - {contractTime ? formatDateForDisplay(contractTime) : '请选择合同生效起止时间'} - - - - - {/**/} - {/* */} - {/**/} - - )} - -选择 - - -
- - {/* 签约时间选择器 */} - setShowApplyTimePicker(false)} - onConfirm={handleApplyTimeConfirm} - /> - - {/* 合同日期选择器 */} - setShowContractTimePicker(false)} - onConfirm={handleContractTimeConfirm} - /> - - {/* 审核状态显示(仅在编辑模式下显示) */} - {isEditMode && ( - - {/**/} - {/* {getApplyStatusText(FormData?.applyStatus)}*/} - {/* */} - {/* }*/} - {/*/>*/} - {FormData?.applyStatus === 20 && ( - - )} - {FormData?.applyStatus === 30 && ( - - )} - - )} - - - {/* 底部浮动按钮 */} - {(!isEditMode || FormData?.applyStatus === 10) && ( - } - text={'立即提交'} - onClick={handleFixedButtonClick} - /> - )} - - - ); -}; - -export default AddShopDealerApply; diff --git a/src/doctor/customer/index.config.ts b/src/doctor/customer/index.config.ts deleted file mode 100644 index 539f8b2..0000000 --- a/src/doctor/customer/index.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '患者管理' -}) diff --git a/src/doctor/customer/index.tsx b/src/doctor/customer/index.tsx deleted file mode 100644 index 33cec2e..0000000 --- a/src/doctor/customer/index.tsx +++ /dev/null @@ -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([]) - const [loading, setLoading] = useState(false) - const [activeTab, setActiveTab] = useState('all') - const [searchValue, setSearchValue] = useState('') - const [displaySearchValue, setDisplaySearchValue] = useState('') - 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) => ( - - - - - - {customer.dealerName} - - {customer.customerStatus && ( - - {getStatusText(customer.customerStatus)} - - )} - - - - 联系人:{customer.realName} - - { - e.stopPropagation(); - makePhoneCall(customer.mobile || ''); - }}>联系电话:{customer.mobile} - - { - e.stopPropagation(); - makePhoneCall(customer.mobile || ''); - }} - /> - { - e.stopPropagation(); - copyPhone(customer.mobile || ''); - }} - > - 复制 - - - - - 添加时间:{customer.createTime} - - - - - {/* 保护天数显示 */} - {customer.applyStatus === 10 && ( - - 保护期: - {customer.protectDays && customer.protectDays > 0 ? ( - - 剩余{customer.protectDays}天 - - ) : ( - - 已过期 - - )} - - )} - - - 报备人:{customer?.nickName} - - {customer?.refereeName} - - - {/* 显示 comments 字段 */} - - 跟进情况:{customer.comments || '暂无'} - { - e.stopPropagation(); - editComments(customer); - }} - > - 编辑 - - - - - - {/* 跟进中状态显示操作按钮 */} - {(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && ( - - - - - )} - {(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && ( - - - - )} - - ); - - // 渲染客户列表 - const renderCustomerList = () => { - const filteredList = getFilteredList(); - const isSearching = displaySearchValue.trim().length > 0; - - return ( - - {/* 搜索结果统计 */} - {isSearching && ( - - - 搜索 "{displaySearchValue}" 的结果,共找到 {filteredList.length} 条记录 - - - )} - - - { - // 滚动事件处理 - }} - onScrollToUpper={() => { - // 滚动到顶部事件处理 - }} - loadingText={ - <> - 加载中... - - } - loadMoreText={ - filteredList.length === 0 ? ( - - ) : ( - - 没有更多了 - - ) - } - > - {loading && filteredList.length === 0 ? ( - - - 加载中... - - ) : ( - filteredList.map(renderCustomerItem) - )} - - - - ); - }; - - return ( - - {/* 搜索栏 */} - - setSearchValue(value)} - onClear={() => { - setSearchValue(''); - setDisplaySearchValue(''); - }} - clearable - /> - - - {/* 顶部Tabs */} - - setActiveTab(value as CustomerStatus)} - > - {tabList.map(tab => { - const counts = getStatusCounts(); - const count = counts[tab.value as keyof typeof counts] || 0; - return ( - 0 ? `(${count})` : ''}`} - value={tab.value} - /> - ); - })} - - - - {/* 客户列表 */} - {renderCustomerList()} - - Taro.navigateTo({url: '/doctor/customer/add'})}/> - - ); -}; - -export default CustomerIndex; diff --git a/src/doctor/customer/trading.config.ts b/src/doctor/customer/trading.config.ts deleted file mode 100644 index 8773b6f..0000000 --- a/src/doctor/customer/trading.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '入市查询' -}) diff --git a/src/doctor/customer/trading.tsx b/src/doctor/customer/trading.tsx deleted file mode 100644 index 43d8ca4..0000000 --- a/src/doctor/customer/trading.tsx +++ /dev/null @@ -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([]) - const [loading, setLoading] = useState(false) - const [searchValue, setSearchValue] = useState('') - 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) => ( - - - - - - {customer.dealerName} - - - - {/*统一代码:{customer.dealerCode}*/} - - 更新时间:{customer.createTime} - - - - - - ); - - // 渲染客户列表 - const renderCustomerList = () => { - const filteredList = getFilteredList(); - - return ( - - { - // 滚动事件处理 - }} - onScrollToUpper={() => { - // 滚动到顶部事件处理 - }} - loadingText={ - <> - 加载中... - - } - loadMoreText={ - filteredList.length === 0 ? ( - - ) : ( - - 没有更多了 - - ) - } - > - {loading && filteredList.length === 0 ? ( - - - 加载中... - - ) : ( - filteredList.map(renderCustomerItem) - )} - - - ); - }; - - return ( - - {/* 搜索栏 */} - - handleSearch(value)} - onClear={() => handleClearSearch()} - /> - - - {/* 客户列表 */} - {renderCustomerList()} - - - ); -}; - -export default CustomerTrading; diff --git a/src/doctor/index.config.ts b/src/doctor/index.config.ts deleted file mode 100644 index f05d7ba..0000000 --- a/src/doctor/index.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '医生端' -}) diff --git a/src/doctor/orders/add.tsx b/src/doctor/orders/add.tsx deleted file mode 100644 index 36126a3..0000000 --- a/src/doctor/orders/add.tsx +++ /dev/null @@ -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() - const [loading, setLoading] = useState(true) - const [FormData, _] = useState() - const formRef = useRef(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 加载中 - } - - return ( - <> - - - - {toUser.alias || toUser.nickname} - {toUser.mobile} - -
- ) : '选择患者'} extra={( - - )} - onClick={() => navTo(`/doctor/customer/index`, true)}/> -
submitSucceed(values)} - onFinishFailed={(errors) => submitFailed(errors)} - > - - - - - -
- - {/* 底部浮动按钮 */} - formRef.current?.submit()}/> - - ); -}; - -export default AddMessage; diff --git a/src/doctor/orders/index.config.ts b/src/doctor/orders/index.config.ts deleted file mode 100644 index ba927a9..0000000 --- a/src/doctor/orders/index.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '处方管理' -}) diff --git a/src/doctor/team/index.config.ts b/src/doctor/team/index.config.ts deleted file mode 100644 index 539f8b2..0000000 --- a/src/doctor/team/index.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '患者管理' -}) diff --git a/src/doctor/wechat/index.config.ts b/src/doctor/wechat/index.config.ts deleted file mode 100644 index a3d18db..0000000 --- a/src/doctor/wechat/index.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '微信客服' -}) diff --git a/src/doctor/wechat/index.scss b/src/doctor/wechat/index.scss deleted file mode 100644 index ea3fb82..0000000 --- a/src/doctor/wechat/index.scss +++ /dev/null @@ -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; - } - } - } - } - } -} diff --git a/src/doctor/wechat/index.tsx b/src/doctor/wechat/index.tsx deleted file mode 100644 index 1132e6e..0000000 --- a/src/doctor/wechat/index.tsx +++ /dev/null @@ -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([]) - - // 长按保存二维码到相册 - 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]) => ( - - - - saveQRCodeToAlbum(`${data.value}`)} - /> - {data.style && 联系电话:{data.style}} - - - - 使用说明: - 1. 长按二维码保存到相册 - 2. 打开微信扫一扫 - 3. 选择相册中的二维码图片 - 4. 添加好友并发送验证消息 - - - - ) - - useEffect(() => { - listCmsWebsiteField({name: 'kefu'}).then(data => { - if (data) { - setCodes(data) - } - }) - }, []); - - return ( - - setActiveTab(`${value}`)} - className="service-tabs" - > - {codes.map((item) => ( - - {renderQRCode(item)} - - ))} - - - ) -} - -export default WechatService diff --git a/src/doctor/withdraw/index.tsx b/src/doctor/withdraw/index.tsx deleted file mode 100644 index 18f257b..0000000 --- a/src/doctor/withdraw/index.tsx +++ /dev/null @@ -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('0') - const [loading, setLoading] = useState(false) - const [refreshing, setRefreshing] = useState(false) - const [submitting, setSubmitting] = useState(false) - const [banks, setBanks] = useState([]) - const [bank, setBank] = useState() - const [isVisible, setIsVisible] = useState(false) - const [availableAmount, setAvailableAmount] = useState('0.00') - const [withdrawRecords, setWithdrawRecords] = useState([]) - const [withdrawAmount, setWithdrawAmount] = useState('') - const [withdrawValue, setWithdrawValue] = useState('') - - 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 = () => ( - - {/* 余额卡片 */} - - {/* 装饰背景 - 小程序兼容版本 */} - - - - - {formatMoney(dealerUser?.money)} - 可提现余额 - - - - - - - - 最低提现金额:¥100 - - - - - - - - setWithdrawAmount(value)} - style={{ - padding: '0 10px', - fontSize: '20px' - }} - /> - - - } - /> - setIsVisible(true)} extra={ - - {bank ? {bank.bankName} : 请选择} - - - }/> - - ¥{calculateExpectedAmount(withdrawAmount)} -
- }/> - 说明:{withdrawValue}}/> -
- - - - -
- ) - - const renderWithdrawRecords = () => { - console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId}) - - return ( - - - {loading ? ( - - - 加载中... - - ) : withdrawRecords.length > 0 ? ( - withdrawRecords.map(record => ( - - - - - 提现金额:¥{record.money} - - - 提现账户:{record.accountDisplay} - - - - {getStatusText(record.applyStatus)} - - - - - 申请时间:{record.createTime} - {record.auditTime && ( - - 审核时间:{new Date(record.auditTime).toLocaleString()} - - )} - {record.rejectReason && ( - - 驳回原因:{record.rejectReason} - - )} - - - )) - ) : ( - - )} - - - ) - } - - return ( - - - - {renderWithdrawForm()} - - - - {renderWithdrawRecords()} - - - setIsVisible(false)} - /> - - ) -} - -export default DealerWithdraw diff --git a/src/gift/index.config.ts b/src/gift/index.config.ts deleted file mode 100644 index 4599c0a..0000000 --- a/src/gift/index.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '礼品卡专区', - navigationBarTextStyle: 'black' -}) diff --git a/src/gift/index.tsx b/src/gift/index.tsx deleted file mode 100644 index 2ad7585..0000000 --- a/src/gift/index.tsx +++ /dev/null @@ -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([]) - 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 ( - - {/* Tab切换 */} - - - - - - - - - - - - - - {/* 礼品卡列表 - 占满剩余空间 */} - - - - {list.length === 0 && !loading ? ( - - - - ) : ( - - - 加载中... - - } - loadMoreText={ - - {list.length === 0 ? "暂无数据" : "没有更多了"} - - } - > - - - )} - - -
- - {/* 使用指南弹窗 */} - setShowGuide(false)} - /> - - {/* 筛选弹窗 */} - setShowFilter(false)} - /> - - ); - -}; - -export default CouponReceiveCenter; diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index e246713..d572bb4 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -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); }); }}; -}; +}; \ No newline at end of file diff --git a/src/hooks/useOrderStats.ts b/src/hooks/useOrderStats.ts index a26c29e..75baa07 100644 --- a/src/hooks/useOrderStats.ts +++ b/src/hooks/useOrderStats.ts @@ -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) { diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index f6684da..0621f86 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -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(gradientThemes[0]) - const [isAutoTheme, setIsAutoTheme] = useState(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(() => getSavedThemeName() === 'auto') + const [currentTheme, setCurrentTheme] = useState(() => 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 { diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 1a662e0..479a0f3 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -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' }); } }); diff --git a/src/hooks/useUserData.ts b/src/hooks/useUserData.ts index fcc0423..4c5e462 100644 --- a/src/hooks/useUserData.ts +++ b/src/hooks/useUserData.ts @@ -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 - 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) }, []) diff --git a/src/pages/cart/cart.tsx b/src/pages/cart/cart.tsx index 89f07f9..db99bd8 100644 --- a/src/pages/cart/cart.tsx +++ b/src/pages/cart/cart.tsx @@ -41,7 +41,7 @@ function Cart() { useShareAppMessage(() => { return { - title: '购物车 - WebSoft Inc.', + title: '购物车 - 网宿软件', success: function () { console.log('分享成功'); }, diff --git a/src/pages/category/category.config.ts b/src/pages/category/category.config.ts deleted file mode 100644 index f54b62b..0000000 --- a/src/pages/category/category.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '分类' -}) diff --git a/src/pages/category/category.scss b/src/pages/category/category.scss deleted file mode 100644 index 30a4e43..0000000 --- a/src/pages/category/category.scss +++ /dev/null @@ -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); - } - } - } -} diff --git a/src/pages/category/category.tsx b/src/pages/category/category.tsx deleted file mode 100644 index 02f67ce..0000000 --- a/src/pages/category/category.tsx +++ /dev/null @@ -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(true) - const [categories, setCategories] = useState([]) - const [selectedCategoryId, setSelectedCategoryId] = useState(0) - const [goods, setGoods] = useState<{ [key: number]: ShopGoods[] }>({}) - const [allGoods, setAllGoods] = useState([]) - const rightScrollRef = useRef(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 = () => ( - - - {[1, 2, 3, 4, 5].map(i => ( - - - - ))} - - - {[1, 2, 3, 4].map(i => ( - - - - - - - - ))} - - - ) - - if (loading) { - return - } - - // 空状态处理 - if (!categories || categories.length === 0) { - return ( - - - 📋 - 暂无商品分类 - 请稍后再试或联系客服 - - 重新加载 - - - - ) - } - - return ( - - {/* 左侧分类导航 */} - - {categories.map((category) => ( - handleCategoryClick(category.navigationId!)} - > - {category.title} - - ))} - - - - {/* 右侧商品列表 */} - - - {categories.map((category) => { - const categoryGoods = goods[category.navigationId!] || [] - return ( - - - {category.title} - - - {categoryGoods.length > 0 ? ( - - {categoryGoods.map((item) => ( - goToGoodsDetail(item.goodsId!)} - > - - - {item.name} - {item.comments && ( - {item.comments} - )} - - ¥{item.price} - {item.salePrice && Number(item.salePrice) !== Number(item.price) && ( - ¥{item.salePrice} - )} - - {item.stock !== undefined && ( - - 库存: {item.stock > 0 ? item.stock : '缺货'} - - )} - - - ))} - - ) : ( - - 该分类暂无商品 - - )} - - ) - })} - - - - ) -} - -export default Category diff --git a/src/doctor/orders/add.config.ts b/src/pages/category/index.config.ts similarity index 62% rename from src/doctor/orders/add.config.ts rename to src/pages/category/index.config.ts index 00434e4..689ba07 100644 --- a/src/doctor/orders/add.config.ts +++ b/src/pages/category/index.config.ts @@ -1,4 +1,4 @@ export default definePageConfig({ - navigationBarTitleText: '在线开方', + navigationBarTitleText: '文章列表', navigationBarTextStyle: 'black' }) diff --git a/src/pages/index/BestSellers.scss b/src/pages/category/index.scss similarity index 100% rename from src/pages/index/BestSellers.scss rename to src/pages/category/index.scss diff --git a/src/pages/category/index.tsx b/src/pages/category/index.tsx new file mode 100644 index 0000000..27002d5 --- /dev/null +++ b/src/pages/category/index.tsx @@ -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(0) + const [category, setCategory] = useState([]) + const [loading, setLoading] = useState(true) + const [nav, setNav] = useState() + const [list, setList] = useState([]) + + 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 = () => ( + + {[1, 2, 3, 4, 5].map(i => ( + + {/* 左侧文字骨架屏 */} + + {/* 标题骨架屏 */} + + {/* 副标题骨架屏 */} + + + {/* 右侧图片骨架屏 */} + + + ))} + + ) + + if (loading) { + return + } + + if(category.length > 0){ + return + } + + return +} + +export default Category diff --git a/src/pages/index/Banner.tsx b/src/pages/index/Banner.tsx index b8506b3..f3b04d3 100644 --- a/src/pages/index/Banner.tsx +++ b/src/pages/index/Banner.tsx @@ -6,13 +6,38 @@ import {Image} from '@nutui/nutui-react-taro' import {getCmsAdByCode} from "@/api/cms/cmsAd"; import navTo from "@/utils/common"; import {pageCmsArticle} from "@/api/cms/cmsArticle"; -import {CmsArticle} from "@/api/cms/cmsArticle/model"; +type AdImage = { + url?: string + path?: string + title?: string + // Compatible keys (some backends use different fields) + src?: string + imageUrl?: string +} + +function normalizeAdImages(ad?: CmsAd): AdImage[] { + const list = ad?.imageList + if (Array.isArray(list) && list.length) return list as AdImage[] + + // Some APIs only return `images` as a JSON string. + const raw = ad?.images + if (!raw) return [] + try { + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? (parsed as AdImage[]) : [] + } catch { + return [] + } +} + +function toNumberPx(input: unknown, fallback: number) { + const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10) + return Number.isFinite(n) ? n : fallback +} const MyPage = () => { const [carouselData, setCarouselData] = useState() - const [hotToday, setHotToday] = useState() - const [item, setItem] = useState() const [loading, setLoading] = useState(true) // const [disableSwiper, setDisableSwiper] = useState(false) @@ -21,24 +46,21 @@ const MyPage = () => { // 加载数据 const loadData = async () => { + setLoading(true) try { - setLoading(true) - // 轮播图 - const flash = await getCmsAdByCode('flash') - // 今日热卖 - const hotToday = await getCmsAdByCode('hot_today') - // 时里动态 - const news = await pageCmsArticle({limit:1,recommend:1}) - // 赋值 - if(flash){ - setCarouselData(flash) - } - if(hotToday){ - setHotToday(hotToday) - } - if(news && news.list.length > 0){ - setItem(news.list[0]) + const [flashRes] = await Promise.allSettled([ + getCmsAdByCode('mp-ad'), + getCmsAdByCode('hot_today'), + pageCmsArticle({ limit: 1, recommend: 1 }), + ]) + + if (flashRes.status === 'fulfilled') { + console.log('flashflashflash', flashRes.value) + setCarouselData(flashRes.value) + } else { + console.error('Failed to fetch flash:', flashRes.reason) } + } catch (error) { console.error('Banner数据加载失败:', error) } finally { @@ -47,11 +69,13 @@ const MyPage = () => { } useEffect(() => { - loadData() + loadData().then() }, []) // 轮播图高度,默认300px - const carouselHeight = carouselData?.height || 300; + const carouselHeight = toNumberPx(carouselData?.height, 300) + const carouselImages = normalizeAdImages(carouselData) + console.log(carouselImages,'carouselImages') // 骨架屏组件 const BannerSkeleton = () => ( @@ -100,94 +124,42 @@ const MyPage = () => { } return ( - - {/* 左侧轮播图区域 */} - - - {carouselData && carouselData?.imageList?.map((img, index) => ( - - navTo(`${img.path}`)} - lazyLoad={false} - style={{ - height: `${carouselHeight}px`, - borderRadius: '4px', - touchAction: 'manipulation' // 关键修改:优化触摸操作 - }} - /> - - ))} - - - - {/* 右侧上下图片区域 - 从API获取数据 */} - - {/* 上层图片 - 使用今日热卖素材 */} - - 今日热卖 - - { - hotToday?.imageList?.map(item => ( - - navTo(item.path)} - /> - {item.title || '到手价¥9.9'} - - )) - } - - - - {/* 下层图片 - 使用社区拼团素材 rounded-lg shadow-sm */} - - {item?.overview || item?.categoryName || '推荐文章'} - + + {carouselImages.map((img, index) => { + const src = img.url || img.src || img.imageUrl + if (!src) return null + return ( + (img.path ? navTo(`${img.path}`) : undefined)} lazyLoad={false} style={{ - borderRadius: '4px' + height: `${carouselHeight}px`, + borderRadius: '4px', + touchAction: 'manipulation' // 关键修改:优化触摸操作 }} - onClick={() => navTo('cms/detail/index?id=' + item?.articleId)} /> - - - - + + ) + })} + ) } diff --git a/src/pages/index/BestSellers.tsx b/src/pages/index/BestSellers.tsx index 1ada7ca..1b85144 100644 --- a/src/pages/index/BestSellers.tsx +++ b/src/pages/index/BestSellers.tsx @@ -124,6 +124,8 @@ const BestSellers = (props: {onStickyChange?: (isSticky: boolean) => void}) => { {item.price} + 会员价 + ¥{item.salePrice} diff --git a/src/pages/index/GoodsList.tsx b/src/pages/index/GoodsList.tsx index 9292a89..f1eaf02 100644 --- a/src/pages/index/GoodsList.tsx +++ b/src/pages/index/GoodsList.tsx @@ -1,15 +1,14 @@ import {useEffect, useState} from "react"; -import {Image, Tabs, Empty, Sticky} from '@nutui/nutui-react-taro' -import {View, Text} from '@tarojs/components'; -import Taro from "@tarojs/taro"; +import {Image} from '@nutui/nutui-react-taro' +import {Share} from '@nutui/icons-react-taro' +import Taro from '@tarojs/taro' import {ShopGoods} from "@/api/shop/shopGoods/model"; import {pageShopGoods} from "@/api/shop/shopGoods"; -import './GoodsList.scss'; +import './GoodsList.scss' -const GoodsList = (props: {onStickyChange?: (isSticky: boolean) => void}) => { - const [tab1value, setTab1value] = useState('0') + +const BestSellers = () => { const [list, setList] = useState([]) - const [stickyStatus, setStickyStatus] = useState(false) const reload = () => { pageShopGoods({}).then(res => { @@ -17,127 +16,52 @@ const GoodsList = (props: {onStickyChange?: (isSticky: boolean) => void}) => { }) } - // 处理粘性布局状态变化 - 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 - } - useEffect(() => { reload() }, []) return ( <> - - {/* Tabs粘性布局组件 */} - - { - setTab1value(value) - }} - style={{ - backgroundColor: 'transparent', - paddingTop: stickyStatus ? '0' : '8px', - paddingBottom: stickyStatus ? '8px' : '0', - }} - activeType="smile" - > - - - - - - - - - - - {/* 今日主推 - 瀑布流布局 */} - {tab1value == '0' && ( - - {list?.map((item, index) => { - return ( - - - Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})} - /> - - {item.name} - - {item.comments} - - - - - {item.price} - - - 已售 {item.sales} - - - - - - ) - })} - - )} - - {/* 即将到期 */} - {tab1value == '1' && ( - - )} - - {/* 活动预告 */} - {tab1value == '2' && ( - - )} - - +
+
+ {list?.map((item, index) => { + return ( +
+ Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> +
+
+
{item.name}
+
+ {item.comments} + 已售 {item.sales} +
+
+
+ + {item.price} +
+
+
+ Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> +
+
Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买 +
+
+
+
+
+
+ ) + })} +
+
) } -export default GoodsList +export default BestSellers diff --git a/src/pages/index/Header.scss b/src/pages/index/Header.scss index 56a75fb..e79ce78 100644 --- a/src/pages/index/Header.scss +++ b/src/pages/index/Header.scss @@ -1,5 +1,5 @@ .header-bg{ - background: linear-gradient(to bottom, #FFD700, #FFA500); + background: linear-gradient(to bottom, #03605c, #18ae4f); height: 335px; width: 100%; top: 0; @@ -7,7 +7,7 @@ z-index: 0; } .header-bg2{ - background: linear-gradient(to bottom, #FFD700, #FFA500); + background: linear-gradient(to bottom, #03605c, #18ae4f); height: 200px; width: 100%; top: 0; @@ -20,4 +20,4 @@ .header-bg { height: 100%; } -} +} \ No newline at end of file diff --git a/src/pages/index/Header.tsx b/src/pages/index/Header.tsx index ec151f8..d8a000d 100644 --- a/src/pages/index/Header.tsx +++ b/src/pages/index/Header.tsx @@ -1,10 +1,10 @@ import {useEffect, useState} from "react"; import Taro from '@tarojs/taro'; -import {Button, Space, Sticky} from '@nutui/nutui-react-taro' +import {Button, Sticky, Popup, Cell, CellGroup} from '@nutui/nutui-react-taro' import {TriangleDown} from '@nutui/icons-react-taro' import {Avatar, NavBar} from '@nutui/nutui-react-taro' import {getUserInfo, getWxOpenId} from "@/api/layout"; -import {TenantId} from "@/config/app"; +import {TenantId, TenantName} from "@/config/app"; import {getOrganization} from "@/api/system/organization"; import {myUserVerify} from "@/api/system/userVerify"; import { useShopInfo } from '@/hooks/useShopInfo'; @@ -12,17 +12,141 @@ import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite"; import {View,Text} from '@tarojs/components' import MySearch from "./MySearch"; import './Header.scss'; +import {User} from "@/api/system/user/model"; +import {getShopStore, listShopStore} from "@/api/shop/shopStore"; +import type {ShopStore} from "@/api/shop/shopStore/model"; +import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection"; const Header = (_: any) => { // 使用新的useShopInfo Hook const { - getWebsiteName, getWebsiteLogo } = useShopInfo(); const [IsLogin, setIsLogin] = useState(true) const [statusBarHeight, setStatusBarHeight] = useState() const [stickyStatus, setStickyStatus] = useState(false) + const [userInfo] = useState() + + // 门店选择:用于首页展示“最近门店”,并在下单时写入订单 storeId + const [storePopupVisible, setStorePopupVisible] = useState(false) + const [stores, setStores] = useState([]) + const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage()) + const [userLocation, setUserLocation] = useState<{lng: number; lat: number} | null>(null) + + const getTenantName = () => { + return userInfo?.tenantName || TenantName + } + + const parseStoreCoords = (s: ShopStore): {lng: number; lat: number} | null => { + const raw = (s.lngAndLat || s.location || '').trim() + if (!raw) return null + + const parts = raw.split(/[,\s]+/).filter(Boolean) + if (parts.length < 2) return null + + const a = parseFloat(parts[0]) + const b = parseFloat(parts[1]) + if (Number.isNaN(a) || Number.isNaN(b)) return null + + // 常见格式是 "lng,lat";这里做一个简单兜底(经度范围更宽) + const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90 + const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180 + if (looksLikeLngLat) return {lng: a, lat: b} + if (looksLikeLatLng) return {lng: b, lat: a} + return null + } + + const distanceMeters = (a: {lng: number; lat: number}, b: {lng: number; lat: number}) => { + const toRad = (x: number) => (x * Math.PI) / 180 + const R = 6371000 // meters + const dLat = toRad(b.lat - a.lat) + const dLng = toRad(b.lng - a.lng) + const lat1 = toRad(a.lat) + const lat2 = toRad(b.lat) + const sin1 = Math.sin(dLat / 2) + const sin2 = Math.sin(dLng / 2) + const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2 + return 2 * R * Math.asin(Math.min(1, Math.sqrt(h))) + } + + const formatDistance = (meters?: number) => { + if (meters === undefined || Number.isNaN(meters)) return '' + if (meters < 1000) return `${Math.round(meters)}m` + return `${(meters / 1000).toFixed(1)}km` + } + + const getStoreDistance = (s: ShopStore) => { + if (!userLocation) return undefined + const coords = parseStoreCoords(s) + if (!coords) return undefined + return distanceMeters(userLocation, coords) + } + + const initStoreSelection = async () => { + // 先读取本地已选门店,避免页面首屏抖动 + const stored = getSelectedStoreFromStorage() + if (stored?.id) { + setSelectedStore(stored) + } + + // 拉取门店列表(失败时允许用户手动重试/继续使用本地门店) + let list: ShopStore[] = [] + try { + list = await listShopStore() + } catch (e) { + console.error('获取门店列表失败:', e) + list = [] + } + const usable = (list || []).filter(s => s?.isDelete !== 1) + setStores(usable) + + // 尝试获取定位,用于计算最近门店 + let loc: {lng: number; lat: number} | null = null + try { + const r = await Taro.getLocation({type: 'gcj02'}) + loc = {lng: r.longitude, lat: r.latitude} + } catch (e) { + // 不强制定位授权;无定位时仍允许用户手动选择 + console.warn('获取定位失败,将不显示最近门店距离:', e) + } + setUserLocation(loc) + + const ensureStoreDetail = async (s: ShopStore) => { + if (!s?.id) return s + // 如果后端已经返回默认仓库等字段,就不额外请求 + if (s.warehouseId) return s + try { + const full = await getShopStore(s.id) + return full || s + } catch (_e) { + return s + } + } + + // 若用户没有选过门店,则自动选择最近门店(或第一个) + const alreadySelected = stored?.id + if (alreadySelected || usable.length === 0) return + + let autoPick: ShopStore | undefined + if (loc) { + autoPick = [...usable] + .map(s => { + const coords = parseStoreCoords(s) + const d = coords ? distanceMeters(loc, coords) : undefined + return {s, d} + }) + .sort((x, y) => (x.d ?? Number.POSITIVE_INFINITY) - (y.d ?? Number.POSITIVE_INFINITY))[0]?.s + } else { + autoPick = usable[0] + } + + if (autoPick?.id) { + const full = await ensureStoreDetail(autoPick) + setSelectedStore(full) + saveSelectedStoreToStorage(full) + } + } const reload = async () => { Taro.getSystemInfo({ @@ -182,6 +306,7 @@ const Header = (_: any) => { useEffect(() => { reload().then() + initStoreSelection().then() }, []) return ( @@ -211,31 +336,95 @@ const Header = (_: any) => { onBackClick={() => { }} left={ + setStorePopupVisible(true)} + > + + + {selectedStore?.name || '请选择'} + + + + } + right={ !IsLogin ? ( - - - - ) : ( - - - {getWebsiteName()} - - - )}> - {/**/} + + ) : null + } + > + {getTenantName()} + + setStorePopupVisible(false)} + > + + + 选择门店 + setStorePopupVisible(false)} + > + 关闭 + + + + + {userLocation ? '已获取定位,按距离排序' : '未获取定位,可手动选择门店'} + + + + {[...stores] + .sort((a, b) => (getStoreDistance(a) ?? Number.POSITIVE_INFINITY) - (getStoreDistance(b) ?? Number.POSITIVE_INFINITY)) + .map((s) => { + const d = getStoreDistance(s) + const isActive = !!selectedStore?.id && selectedStore.id === s.id + return ( + + {s.name || `门店${s.id}`} + {d !== undefined && {formatDistance(d)}} + + } + description={s.address || ''} + onClick={async () => { + let storeToSave = s + if (s?.id) { + try { + const full = await getShopStore(s.id) + if (full) storeToSave = full + } catch (_e) { + // keep base item + } + } + setSelectedStore(storeToSave) + saveSelectedStoreToStorage(storeToSave) + setStorePopupVisible(false) + Taro.showToast({title: '门店已切换', icon: 'success'}) + }} + /> + ) + })} + +
+ ) diff --git a/src/pages/index/HeaderWithHook.tsx b/src/pages/index/HeaderWithHook.tsx deleted file mode 100644 index da16113..0000000 --- a/src/pages/index/HeaderWithHook.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import {useEffect, useState} from "react"; -import Taro from '@tarojs/taro'; -import {Button, Space} from '@nutui/nutui-react-taro' -import {TriangleDown} from '@nutui/icons-react-taro' -import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro' -import {getUserInfo, getWxOpenId} from "@/api/layout"; -import {TenantId} from "@/config/app"; -import {getOrganization} from "@/api/system/organization"; -import {myUserVerify} from "@/api/system/userVerify"; -import {User} from "@/api/system/user/model"; -import { useShopInfo } from '@/hooks/useShopInfo'; -import { useUser } from '@/hooks/useUser'; -import {handleInviteRelation} from "@/utils/invite"; -import MySearch from "./MySearch"; -import './Header.scss'; - -const Header = (props: any) => { - // 使用新的hooks - const { - shopInfo, - loading: shopLoading, - getWebsiteName, - getWebsiteLogo - } = useShopInfo(); - - const { - user, - isLoggedIn, - loading: userLoading - } = useUser(); - - const [showBasic, setShowBasic] = useState(false) - const [statusBarHeight, setStatusBarHeight] = useState() - - const reload = async () => { - Taro.getSystemInfo({ - success: (res) => { - setStatusBarHeight(res.statusBarHeight) - }, - }) - - // 注意:商店信息现在通过useShopInfo自动管理,不需要手动获取 - // 用户信息现在通过useUser自动管理,不需要手动获取 - - // 如果需要获取openId,可以在用户登录后处理 - if (user && !user.openid) { - Taro.login({ - success: (res) => { - getWxOpenId({code: res.code}).then(() => { - console.log('OpenId获取成功'); - }) - } - }) - } - - // 检查用户认证状态 - if (user?.userId) { - // 获取组织信息 - getOrganization({userId: user.userId}).then((data) => { - console.log('组织信息>>>', data) - }).catch(() => { - console.log('获取组织信息失败') - }); - - // 检查用户认证 - myUserVerify({userId: user.userId}).then((data) => { - console.log('认证信息>>>', data) - }).catch(() => { - console.log('获取认证信息失败') - }); - } - } - - // 获取手机号授权 - const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => { - const {code, encryptedData, iv} = detail - Taro.login({ - success: function () { - if (code) { - Taro.request({ - url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone', - method: 'POST', - data: { - code, - encryptedData, - iv, - notVerifyPhone: true, - refereeId: 0, - sceneType: 'save_referee', - tenantId: TenantId - }, - success: async function (res) { - if (res.data.code == 1) { - Taro.showToast({ - title: res.data.message, - icon: 'error', - duration: 2000 - }) - return false; - } - // 登录成功 - Taro.setStorageSync('access_token', res.data.data.access_token) - Taro.setStorageSync('UserId', res.data.data.user.userId) - - // 处理邀请关系 - if (res.data.data.user?.userId) { - try { - const inviteSuccess = await handleInviteRelation(res.data.data.user.userId) - if (inviteSuccess) { - Taro.showToast({ - title: '邀请关系建立成功', - icon: 'success', - duration: 2000 - }) - } - } catch (error) { - console.error('处理邀请关系失败:', error) - } - } - - // 重新加载小程序 - Taro.reLaunch({ - url: '/pages/index/index' - }) - } - }) - } else { - console.log('登录失败!') - } - } - }) - } - - useEffect(() => { - reload().then() - }, []) - - // 显示加载状态 - if (shopLoading || userLoading) { - return ( -
-
- 加载中... -
-
- ); - } - - return ( - <> -
- -
- { - }} - left={ - !isLoggedIn ? ( -
- - -
- ) : ( -
- - {getWebsiteName()} - -
- )}> -
- { - setShowBasic(false) - }} - > -
-

商店信息

-
网站名称: {getWebsiteName()}
-
Logo: logo
- -

用户信息

-
登录状态: {isLoggedIn ? '已登录' : '未登录'}
- {user && ( - <> -
用户ID: {user.userId}
-
手机号: {user.phone}
-
昵称: {user.nickname}
- - )} - - -
-
- - ) -} - -export default Header; diff --git a/src/pages/index/IndexWithHook.tsx b/src/pages/index/IndexWithHook.tsx deleted file mode 100644 index c537314..0000000 --- a/src/pages/index/IndexWithHook.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import Header from './Header'; -import BestSellers from './BestSellers'; -import Taro from '@tarojs/taro'; -import {useShareAppMessage, useShareTimeline} from "@tarojs/taro" -import {useEffect, useState} from "react"; -import {Sticky} from '@nutui/nutui-react-taro' -import { useShopInfo } from '@/hooks/useShopInfo'; -import { useUser } from '@/hooks/useUser'; -import Menu from "./Menu"; -import Banner from "./Banner"; -import './index.scss' - -const Home = () => { - const [stickyStatus, setStickyStatus] = useState(false); - - // 使用新的hooks - const { - shopInfo, - loading: shopLoading, - error: shopError, - getWebsiteName, - getWebsiteLogo, - refreshShopInfo - } = useShopInfo(); - - const { - user, - isLoggedIn, - loading: userLoading - } = useUser(); - - const onSticky = (args: any) => { - setStickyStatus(args[0].isFixed); - }; - - const showAuthModal = () => { - Taro.showModal({ - title: '授权提示', - content: '需要获取您的用户信息', - confirmText: '去授权', - cancelText: '取消', - success: (res) => { - if (res.confirm) { - // 用户点击确认,打开授权设置页面 - Taro.openSetting({ - success: (settingRes) => { - if (settingRes.authSetting['scope.userInfo']) { - console.log('用户已授权'); - } else { - console.log('用户拒绝授权'); - } - } - }); - } - } - }); - }; - - // 分享给好友 - useShareAppMessage(() => { - return { - title: `${getWebsiteName()} - 精选商城`, - path: '/pages/index/index', - imageUrl: getWebsiteLogo(), - success: function (res: any) { - console.log('分享成功', res); - Taro.showToast({ - title: '分享成功', - icon: 'success', - duration: 2000 - }); - }, - fail: function (res: any) { - console.log('分享失败', res); - Taro.showToast({ - title: '分享失败', - icon: 'none', - duration: 2000 - }); - } - }; - }); - - // 分享到朋友圈 - useShareTimeline(() => { - return { - title: `${getWebsiteName()} - 精选商城`, - imageUrl: getWebsiteLogo(), - success: function (res: any) { - console.log('分享到朋友圈成功', res); - }, - fail: function (res: any) { - console.log('分享到朋友圈失败', res); - } - }; - }); - - useEffect(() => { - // 设置页面标题 - if (shopInfo?.appName) { - Taro.setNavigationBarTitle({ - title: shopInfo.appName - }); - } - }, [shopInfo]); - - useEffect(() => { - // 检查用户授权状态 - Taro.getSetting({ - success: (res) => { - if (res.authSetting['scope.userInfo']) { - console.log('用户已经授权过,可以直接获取用户信息'); - } else { - console.log('用户未授权,需要弹出授权窗口'); - showAuthModal(); - } - } - }); - - // 获取用户基本信息(头像、昵称等) - Taro.getUserInfo({ - success: (res) => { - const avatar = res.userInfo.avatarUrl; - console.log('用户头像:', avatar); - }, - fail: (err) => { - console.log('获取用户信息失败:', err); - } - }); - }, []); - - // 处理错误状态 - if (shopError) { - return ( -
-
加载商店信息失败: {shopError}
- -
- ); - } - - // 显示加载状态 - if (shopLoading) { - return ( -
-
加载中...
-
- ); - } - - return ( - <> - onSticky(args)}> -
- -
- - - - - {/* 调试信息面板 - 仅在开发环境显示 */} - {process.env.NODE_ENV === 'development' && ( -
-
商店: {getWebsiteName()}
-
用户: {isLoggedIn ? (user?.nickname || '已登录') : '未登录'}
-
加载: {userLoading ? '用户加载中' : '已完成'}
-
- )} -
- - ) -} - -export default Home; diff --git a/src/pages/index/Menu.tsx b/src/pages/index/Menu.tsx index 710d2ee..c634965 100644 --- a/src/pages/index/Menu.tsx +++ b/src/pages/index/Menu.tsx @@ -34,7 +34,7 @@ const Page = () => { return ( shopLoading ? (加载中) :
-
+
{ navItems.map((item, index) => (
onNav(item)}> diff --git a/src/pages/index/MySearch.tsx b/src/pages/index/MySearch.tsx index 3c6d9a5..2015ee6 100644 --- a/src/pages/index/MySearch.tsx +++ b/src/pages/index/MySearch.tsx @@ -30,7 +30,7 @@ function MySearch(props: any) { return ( -
+
(false) - // Tabs粘性状态 - const [_, setTabsStickyStatus] = useState(false) + const [activeTab, setActiveTab] = useState('推荐') + const [goodsList, setGoodsList] = useState([]) useShareAppMessage(() => { // 获取当前用户ID,用于生成邀请链接 @@ -85,9 +84,7 @@ function Home() { // } // 处理Tabs粘性状态变化 - const handleTabsStickyChange = (isSticky: boolean) => { - setTabsStickyStatus(isSticky) - } + // const handleTabsStickyChange = (isSticky: boolean) => {} const reload = () => { @@ -99,6 +96,10 @@ function Home() { }) + pageShopGoods({}).then(res => { + setGoodsList(res?.list || []) + }) + // 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载 if (hasPendingInvite()) { console.log('检测到待处理的邀请关系') @@ -152,16 +153,140 @@ function Home() { }); }, []); + const tabs = useMemo(() => ['推荐', '桶装水', '优惠组合', '购机套餐', '饮水设备'], []) + + const shortcuts = useMemo< + Array<{ key: string; title: string; icon: ReactNode; onClick: () => void }> + >( + () => [ + { + key: 'ticket', + title: '我的水票', + icon: , + onClick: () => Taro.navigateTo({ url: '/user/gift/index' }), + }, + { + key: 'order', + title: '立即订水', + icon: , + onClick: () => Taro.navigateTo({ url: '/shop/goodsDetail/index?id=10072' }), + }, + { + key: 'invite', + title: '邀请有礼', + icon: , + onClick: () => Taro.navigateTo({ url: '/dealer/qrcode/index' }), + }, + { + key: 'coupon', + title: '领券中心', + icon: , + onClick: () => Taro.navigateTo({ url: '/coupon/index' }), + }, + ], + [] + ) + + const visibleGoods = useMemo(() => { + // 先按效果图展示两列卡片,数据不够时也保持布局稳定 + const list = goodsList || [] + if (list.length <= 6) return list + return list.slice(0, 6) + }, [goodsList]) + return ( <> {/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
-
- - - -
+ + {/* 顶部活动主视觉:使用 Banner 组件 */} + + + {/* 电子水票 */} + + + 电子水票 + + 您还有 0 张水票 + + + + + + {shortcuts.map((item) => ( + + {item.icon} + {item.title} + + ))} + + + + + {/* 分类Tabs */} + + + {tabs.map((tab) => { + const active = tab === activeTab + return ( + setActiveTab(tab)} + > + {tab} + + ) + })} + + + + {/* 商品列表 */} + + {visibleGoods.map((item) => ( + + + + Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` }) + } + /> + + + + {item.name} + + 已购:{item.sales || 0}人 + + + {item.price} + + + + + + Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` }) + } + > + 立即购买 + + + + + ))} + + ) } diff --git a/src/pages/menu/menu.config.ts b/src/pages/menu/menu.config.ts deleted file mode 100644 index 2cb081e..0000000 --- a/src/pages/menu/menu.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '菜单' -}) diff --git a/src/pages/menu/menu.tsx b/src/pages/menu/menu.tsx deleted file mode 100644 index 325e9cf..0000000 --- a/src/pages/menu/menu.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import {useEffect, useState} from "react"; -import Taro from '@tarojs/taro'; -import {pageCmsArticle} from "@/api/cms/cmsArticle"; -import {CmsArticle} from "@/api/cms/cmsArticle/model"; -import {NavBar, Cell} from '@nutui/nutui-react-taro'; -import {Text} from '@tarojs/components'; -import {ArrowRight, ImageRectangle, Coupon, Follow} from '@nutui/icons-react-taro' -import navTo from "@/utils/common"; - -/** - * 文章终极列表 - * @constructor - */ -const Menu = () => { - const [statusBarHeight, setStatusBarHeight] = useState() - const [loading, setLoading] = useState(false) - const [list, setList] = useState() - - const reload = async () => { - setLoading(true) - const article = await pageCmsArticle({categoryId: 4289, status: 0}) - if (article) { - setList(article?.list) - setLoading(false) - } - } - - useEffect(() => { - Taro.getSystemInfo({ - success: (res) => { - setStatusBarHeight(res.statusBarHeight) - }, - }) - reload().then(() => { - console.log('初始化完成') - }) - }, []) - - return ( - <> - {loading && (
暂无数据
)} - { - }} - > - 发现 - - {list && ( - <> - - - 好物推荐 -
- } extra={}/> - - - 权益中心 -
- } - extra={} - onClick={() => { - navTo('/shop/shopArticle/index', true) - }} - /> - - - 我的收藏 -
- } extra={}/> - - )} - - ) -} -export default Menu diff --git a/src/pages/user/components/IsDealer.tsx b/src/pages/user/components/IsDealer.tsx index 116e9c0..8e86034 100644 --- a/src/pages/user/components/IsDealer.tsx +++ b/src/pages/user/components/IsDealer.tsx @@ -5,26 +5,13 @@ import {ArrowRight, Reward, Setting} from '@nutui/icons-react-taro' import {useUser} from '@/hooks/useUser' import {useDealerUser} from "@/hooks/useDealerUser"; import {useThemeStyles} from "@/hooks/useTheme"; -import { useConfig } from "@/hooks/useConfig"; -import {useEffect, useState} from "react"; -import {getCmsAdByCode} from "@/api/cms/cmsAd"; -import {CmsAd} from "@/api/cms/cmsAd/model"; // 使用新的自定义Hook +import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook const IsDealer = () => { const themeStyles = useThemeStyles(); const { config } = useConfig(); // 使用新的Hook const {isSuperAdmin} = useUser(); const {dealerUser} = useDealerUser() - const [register, setRegister] = useState() - - const reload = async () => { - const item = await getCmsAdByCode('register') - setRegister(item) - } - - useEffect(() => { - reload().then() - }, []); /** * 管理中心 @@ -64,12 +51,12 @@ const IsDealer = () => { {config?.vipText || '入驻申请'} + className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '开发者中心'} {/*门店核销*/} } extra={} - onClick={() => navTo('/doctor/index', true)} + onClick={() => navTo('/dealer/index', true)} /> @@ -88,12 +75,12 @@ const IsDealer = () => { title={ - {register?.name || '注册会员'} - + {config?.vipText || '门店入驻'} + {config?.vipComments || ''} } extra={} - onClick={() => navTo(`${register?.path}`, true)} + onClick={() => navTo('/dealer/apply/add', true)} /> diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx index 0235e65..b7cc16b 100644 --- a/src/pages/user/components/UserCard.tsx +++ b/src/pages/user/components/UserCard.tsx @@ -1,4 +1,4 @@ -import {Avatar, Tag, Space} from '@nutui/nutui-react-taro' +import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro' import {View, Text} from '@tarojs/components' import {getUserInfo, getWxOpenId} from '@/api/layout'; import Taro from '@tarojs/taro'; @@ -11,15 +11,21 @@ import {useUserData} from "@/hooks/useUserData"; import {getStoredInviteParams} from "@/utils/invite"; import UnifiedQRButton from "@/components/UnifiedQRButton"; import {useThemeStyles} from "@/hooks/useTheme"; +import {getRootDomain} from "@/utils/domain"; const UserCard = forwardRef((_, ref) => { const {data, refresh} = useUserData() - const {getDisplayName, getRoleName} = useUser(); + const {getDisplayName} = useUser(); const [IsLogin, setIsLogin] = useState(false) const [userInfo, setUserInfo] = useState() const themeStyles = useThemeStyles(); + // 角色名称:优先取用户 roles 数组的第一个角色名称 + const getRoleName = () => { + return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户' + } + // 下拉刷新 const handleRefresh = async () => { await refresh() @@ -65,6 +71,8 @@ const UserCard = forwardRef((_, ref) => { setUserInfo(data) setIsLogin(true); Taro.setStorageSync('UserId', data.userId) + // 登录态已就绪后刷新卡片统计(余额/积分/券/水票) + refresh().then() // 获取openId if (!data.openid) { @@ -162,6 +170,8 @@ const UserCard = forwardRef((_, ref) => { Taro.setStorageSync('UserId', res.data.data.user.userId) setUserInfo(res.data.data.user) setIsLogin(true) + // 登录态已就绪后刷新卡片统计(余额/积分/券/水票) + refresh().then() } }) } else { @@ -179,22 +189,37 @@ const UserCard = forwardRef((_, ref) => { className={'user-card w-full flex flex-col justify-around rounded-xl'} > - - + {IsLogin && ( + navTo(`/user/profile/profile`)}> + + - - {getDisplayName()} - {IsLogin ? ( - - - - {getRoleName()} - - + /> + + {getDisplayName() || '点击登录'} + {getRootDomain() && ( + {getRoleName()} + )} + - ) : ''} - + + )} + {!IsLogin && ( + + )} ((_, ref) => { // 核销成功,可以显示更多信息或跳转到详情页 Taro.showModal({ title: '核销成功', - content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}` + content: `已成功核销的品类:${result.data.goodsName || '水票'},面值¥${result.data.faceValue}` }); } }} @@ -225,22 +250,22 @@ const UserCard = forwardRef((_, ref) => { navTo('/user/wallet/wallet', true)}> - 余额 - {data?.balance || '0.00'} + 余额 + {data?.balance || '0.00'} - 积分 - {data?.points || 0} + 积分 + {data?.points || 0} navTo('/user/coupon/index', true)}> - 优惠券 - {data?.coupons || 0} + 优惠券 + {data?.coupons || 0} navTo('/user/gift/index', true)}> - 礼品卡 - {data?.giftCards || 0} + 水票 + {data?.giftCards || 0} diff --git a/src/pages/user/components/UserGrid.tsx b/src/pages/user/components/UserGrid.tsx index dad57ca..54b6577 100644 --- a/src/pages/user/components/UserGrid.tsx +++ b/src/pages/user/components/UserGrid.tsx @@ -11,13 +11,14 @@ import { People, // AfterSaleService, Logout, - ShoppingAdd, + Shop, + Jdl, Service } from '@nutui/icons-react-taro' import {useUser} from "@/hooks/useUser"; const UserCell = () => { - const {logoutUser} = useUser(); + const {logoutUser, hasRole} = useUser(); const onLogout = () => { Taro.showModal({ @@ -49,10 +50,49 @@ const UserCell = () => { border: 'none' } as React.CSSProperties} > - navTo('/user/poster/poster', true)}> + + {hasRole('store') && ( + navTo('/store/index', true)}> + + + + + + + )} + + {hasRole('rider') && ( + navTo('/rider/index', true)}> + + + + + + + )} + + {(hasRole('staff') || hasRole('admin')) && ( + navTo('/user/store/orders/index', true)}> + + + + + + + )} + + navTo('/user/address/index', true)}> - - + + + + + + + navTo('/user/help/index')}> + + + @@ -71,14 +111,6 @@ const UserCell = () => { - navTo('/user/address/index', true)}> - - - - - - - navTo('/user/userVerify/index', true)}> @@ -87,7 +119,7 @@ const UserCell = () => { - navTo('/doctor/team/index', true)}> + navTo('/dealer/team/index', true)}> @@ -95,7 +127,7 @@ const UserCell = () => { - {/* navTo('/doctor/qrcode/index', true)}>*/} + {/* navTo('/dealer/qrcode/index', true)}>*/} {/* */} {/* */} {/* */} @@ -111,13 +143,6 @@ const UserCell = () => { {/* */} {/**/} - navTo('/user/help/index')}> - - - - - - navTo('/user/about/index')}> @@ -138,8 +163,54 @@ const UserCell = () => { + {/**/} + {/* 账号管理*/} + {/* */} + {/* */} + {/* navTo('/user/profile/profile', true)}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* navTo('/user/theme/index', true)}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* navTo('/user/about/index')}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/**/} ) } export default UserCell - diff --git a/src/pages/user/user.tsx b/src/pages/user/user.tsx index 9d49ce2..70b97aa 100644 --- a/src/pages/user/user.tsx +++ b/src/pages/user/user.tsx @@ -3,23 +3,19 @@ import {PullToRefresh} from '@nutui/nutui-react-taro' import UserCard from "./components/UserCard"; import UserOrder from "./components/UserOrder"; import UserFooter from "./components/UserFooter"; -import {useUserData} from "@/hooks/useUserData"; import {View} from '@tarojs/components'; import './user.scss' import IsDealer from "./components/IsDealer"; -import UserCell from "@/pages/user/components/UserCell"; import {useThemeStyles} from "@/hooks/useTheme"; +import UserGrid from "@/pages/user/components/UserGrid"; function User() { - const {refresh} = useUserData() const userCardRef = useRef() const themeStyles = useThemeStyles(); // 下拉刷新处理 const handleRefresh = async () => { - await refresh() - // 如果 UserCard 组件有自己的刷新方法,也可以调用 if (userCardRef.current?.handleRefresh) { await userCardRef.current.handleRefresh() } @@ -55,7 +51,7 @@ function User() { - + ) diff --git a/src/passport/register.tsx b/src/passport/register.tsx deleted file mode 100644 index 553e0e0..0000000 --- a/src/passport/register.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import {useEffect, useState} from "react"; -import Taro from '@tarojs/taro' -import {Input, Radio, Button} from '@nutui/nutui-react-taro' - -const Register = () => { - const [isAgree, setIsAgree] = useState(false) - const reload = () => { - Taro.hideTabBar() - } - - useEffect(() => { - reload() - }, []) - - return ( - <> -
-
免费试用14天,快速上手独立站
-
建站、选品、营销、支付、物流,全部搞定
-
- WebSoft为您提供独立站的解决方案,提供专业、高效、安全的运营服务。 -
-
- -
-
- -
-
- -
-
- -
-
- setIsAgree(!isAgree)}> - setIsAgree(!isAgree)}>勾选表示您已阅读并同意 - Taro.navigateTo({url: '/passport/agreement'})} className={'text-blue-600'}>《服务协议及隐私政策》 -
-
- - - ) -} -export default Register diff --git a/src/passport/sms-login.tsx b/src/passport/sms-login.tsx index 62da43d..c30933c 100644 --- a/src/passport/sms-login.tsx +++ b/src/passport/sms-login.tsx @@ -3,6 +3,7 @@ import Taro from '@tarojs/taro' import {Input, Button} from '@nutui/nutui-react-taro' import {loginBySms, sendSmsCaptcha} from "@/api/passport/login"; import {LoginParam} from "@/api/passport/login/model"; +import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite"; const SmsLogin = () => { const [loading, setLoading] = useState(false) @@ -131,6 +132,15 @@ const SmsLogin = () => { code: formData.code }) + // 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定 + if (hasPendingInvite()) { + try { + await checkAndHandleInviteRelation() + } catch (e) { + console.error('短信登录后处理邀请关系失败:', e) + } + } + Taro.showToast({ title: '登录成功', icon: 'success' diff --git a/src/passport/unified-qr/index.tsx b/src/passport/unified-qr/index.tsx index 9e87c7f..c229ad6 100644 --- a/src/passport/unified-qr/index.tsx +++ b/src/passport/unified-qr/index.tsx @@ -193,7 +193,7 @@ const UnifiedQRPage: React.FC = () => { {result.type === ScanType.VERIFICATION && result.data && ( - 礼品卡:{result.data.goodsName || '未知商品'} + 水票:{result.data.goodsName || '未知商品'} 面值:¥{result.data.faceValue} diff --git a/src/rider/index.config.ts b/src/rider/index.config.ts new file mode 100644 index 0000000..1293fcb --- /dev/null +++ b/src/rider/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '配送中心' +}) diff --git a/src/rider/index.scss b/src/rider/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/rider/index.tsx b/src/rider/index.tsx new file mode 100644 index 0000000..0a023fe --- /dev/null +++ b/src/rider/index.tsx @@ -0,0 +1,295 @@ +import React from 'react' +import {View, Text} from '@tarojs/components' +import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro' +import { + User, + Shopping, + Dongdong, + ArrowRight, + Purse, + People +} from '@nutui/icons-react-taro' +import {useDealerUser} from '@/hooks/useDealerUser' +import { useThemeStyles } from '@/hooks/useTheme' +import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients' +import Taro from '@tarojs/taro' + +const DealerIndex: React.FC = () => { + const { + dealerUser, + error, + refresh, + } = useDealerUser() + + // 使用主题样式 + const themeStyles = useThemeStyles() + + // 导航到各个功能页面 + const navigateToPage = (url: string) => { + Taro.navigateTo({url}) + } + + // 格式化金额 + const formatMoney = (money?: string) => { + if (!money) return '0.00' + return parseFloat(money).toFixed(2) + } + + // 格式化时间 + const formatTime = (time?: string) => { + if (!time) return '-' + return new Date(time).toLocaleDateString() + } + + // 获取用户主题 + const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId) + + // 获取渐变背景 + const getGradientBackground = (themeColor?: string) => { + if (themeColor) { + const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30) + return gradientUtils.createGradient(themeColor, darkerColor) + } + return userTheme.background + } + + console.log(getGradientBackground(),'getGradientBackground()') + + if (error) { + return ( + + + {error} + + + + ) + } + + return ( + + + {/*头部信息*/} + {dealerUser && ( + + {/* 装饰性背景元素 - 小程序兼容版本 */} + + + + + } + className="mr-4" + style={{ + border: '2px solid rgba(255, 255, 255, 0.3)' + }} + /> + + + {dealerUser?.realName || '分销商'} + + + ID: {dealerUser.userId} + + + + 加入时间 + + {formatTime(dealerUser.createTime)} + + + + + )} + + {/* 佣金统计卡片 */} + {dealerUser && ( + + + 工资统计 + + + + + {formatMoney(dealerUser.money)} + + 工资收入 + + + + {formatMoney(dealerUser.freezeMoney)} + + 桶数 + + + + {formatMoney(dealerUser.totalMoney)} + + 累计收入 + + + + )} + + {/* 团队统计 */} + {dealerUser && ( + + + 我的邀请 + navigateToPage('/dealer/team/index')} + > + 查看详情 + + + + + + + {dealerUser.firstNum || 0} + + 一级成员 + + + + {dealerUser.secondNum || 0} + + 二级成员 + + + + {dealerUser.thirdNum || 0} + + 三级成员 + + + + )} + + {/* 功能导航 */} + + 配送工具 + + + navigateToPage('/rider/orders/index')}> + + + + + + + + navigateToPage('/rider/withdraw/index')}> + + + + + + + + navigateToPage('/rider/team/index')}> + + + + + + + + navigateToPage('/rider/qrcode/index')}> + + + + + + + + + {/* 第二行功能 */} + {/**/} + {/* navigateToPage('/dealer/invite-stats/index')}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* /!* 预留其他功能位置 *!/*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} + + + + + {/* 底部安全区域 */} + + + ) +} + +export default DealerIndex diff --git a/src/rider/orders/index.config.ts b/src/rider/orders/index.config.ts new file mode 100644 index 0000000..9606465 --- /dev/null +++ b/src/rider/orders/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '配送订单', + navigationBarTextStyle: 'black' +} diff --git a/src/rider/orders/index.tsx b/src/rider/orders/index.tsx new file mode 100644 index 0000000..80b53a5 --- /dev/null +++ b/src/rider/orders/index.tsx @@ -0,0 +1,390 @@ +import {useCallback, useEffect, useMemo, useRef, useState} from 'react' +import Taro from '@tarojs/taro' +import { Tabs, TabPane, Cell, Space, Button, Dialog, Image, Empty, InfiniteLoading} from '@nutui/nutui-react-taro' +import {View, Text} from '@tarojs/components' +import dayjs from 'dayjs' +import {pageShopOrder, updateShopOrder} from '@/api/shop/shopOrder' +import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model' +import {uploadFile} from '@/api/system/file' + +export default function RiderOrders() { + + const riderId = useMemo(() => { + const v = Number(Taro.getStorageSync('UserId')) + return Number.isFinite(v) && v > 0 ? v : undefined + }, []) + + const pageRef = useRef(1) + const [tabIndex, setTabIndex] = useState(0) + const [list, setList] = useState([]) + const [hasMore, setHasMore] = useState(true) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const [deliverDialogVisible, setDeliverDialogVisible] = useState(false) + const [deliverSubmitting, setDeliverSubmitting] = useState(false) + const [deliverOrder, setDeliverOrder] = useState(null) + const [deliverImg, setDeliverImg] = useState(undefined) + + // 前端展示用:后台可配置实际自动确认收货时长 + const AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK = 24 + + const riderTabs = useMemo( + () => [ + {index: 0, title: '全部', statusFilter: -1}, + {index: 1, title: '配送中', statusFilter: 3}, // 后端:deliveryStatus=20 + {index: 2, title: '待客户确认', statusFilter: 3}, // 同上,前端再按 sendEndTime 细分 + {index: 3, title: '已完成', statusFilter: 5}, // 后端:orderStatus=1 + ], + [] + ) + + const isAbnormalOrder = (order: ShopOrder) => { + const s = order.orderStatus + return s === 2 || s === 3 || s === 4 || s === 5 || s === 6 || s === 7 + } + + const getOrderStatusText = (order: ShopOrder) => { + if (order.orderStatus === 2) return '已取消' + if (order.orderStatus === 3) return '取消中' + if (order.orderStatus === 4) return '退款申请中' + if (order.orderStatus === 5) return '退款被拒绝' + if (order.orderStatus === 6) return '退款成功' + if (order.orderStatus === 7) return '客户申请退款' + if (!order.payStatus) return '未付款' + if (order.orderStatus === 1) return '已完成' + + // 配送员页:用 sendEndTime 表示“已送达收货点” + if (order.deliveryStatus === 20) { + if (order.sendEndTime) return '待客户确认收货' + return '配送中' + } + if (order.deliveryStatus === 10) return '待发货' + if (order.deliveryStatus === 30) return '部分发货' + return '处理中' + } + + const getOrderStatusColor = (order: ShopOrder) => { + if (isAbnormalOrder(order)) return 'text-orange-500' + if (order.orderStatus === 1) return 'text-green-600' + if (order.sendEndTime) return 'text-purple-600' + return 'text-blue-600' + } + + const canConfirmDelivered = (order: ShopOrder) => { + if (!order.payStatus) return false + if (order.orderStatus === 1) return false + if (isAbnormalOrder(order)) return false + // 只允许在“配送中”阶段确认送达 + if (order.deliveryStatus !== 20) return false + return !order.sendEndTime + } + + const filterByTab = useCallback( + (orders: ShopOrder[]) => { + if (tabIndex === 1) { + // 配送中:未确认送达 + return orders.filter(o => o.deliveryStatus === 20 && !o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1) + } + if (tabIndex === 2) { + // 待客户确认:已确认送达 + return orders.filter(o => o.deliveryStatus === 20 && !!o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1) + } + if (tabIndex === 3) { + return orders.filter(o => o.orderStatus === 1) + } + return orders + }, + [tabIndex] + ) + + const reload = useCallback( + async (resetPage = false) => { + if (!riderId) return + setLoading(true) + setError(null) + + const currentPage = resetPage ? 1 : pageRef.current + const currentTab = riderTabs.find(t => t.index === tabIndex) || riderTabs[0] + + const params: ShopOrderParam = { + page: currentPage, + riderId, + statusFilter: currentTab.statusFilter, + } + + try { + const res = await pageShopOrder(params) + const incoming = (res?.list || []) as ShopOrder[] + setList(prev => (resetPage ? incoming : prev.concat(incoming))) + setHasMore(incoming.length >= 10) + pageRef.current = currentPage + } catch (e) { + console.error('加载配送订单失败:', e) + setError('加载失败,请重试') + setHasMore(false) + } finally { + setLoading(false) + } + }, + [riderId, riderTabs, tabIndex] + ) + + const reloadMore = useCallback(async () => { + if (loading || !hasMore) return + pageRef.current += 1 + await reload(false) + }, [hasMore, loading, reload]) + + const openDeliverDialog = (order: ShopOrder) => { + setDeliverOrder(order) + setDeliverImg(order.sendEndImg) + setDeliverDialogVisible(true) + } + + const handleChooseDeliverImg = async () => { + try { + const file = await uploadFile() + setDeliverImg(file?.url) + } catch (e) { + console.error('上传送达照片失败:', e) + Taro.showToast({title: '上传失败,请重试', icon: 'none'}) + } + } + + const handleConfirmDelivered = async () => { + if (!deliverOrder?.orderId) return + if (deliverSubmitting) return + setDeliverSubmitting(true) + try { + await updateShopOrder({ + orderId: deliverOrder.orderId, + // 用于前端/后端识别“配送员已送达收货点” + sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), + sendEndImg: deliverImg, + }) + Taro.showToast({title: '已确认送达', icon: 'success'}) + setDeliverDialogVisible(false) + setDeliverOrder(null) + setDeliverImg(undefined) + pageRef.current = 1 + await reload(true) + } catch (e) { + console.error('确认送达失败:', e) + Taro.showToast({title: '确认送达失败', icon: 'none'}) + } finally { + setDeliverSubmitting(false) + } + } + + useEffect(() => { + }, []) + + useEffect(() => { + pageRef.current = 1 + void reload(true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tabIndex, riderId]) + + if (!riderId) { + return ( + + 请先登录 + + ) + } + + const displayList = filterByTab(list) + + return ( + + + + setTabIndex(Number(paneKey))} + > + {riderTabs.map(t => ( + + ))} + + + + {error ? ( + + {error} + + + ) : ( + 加载中} + loadMoreText={ + displayList.length === 0 ? ( + + ) : ( + 没有更多了 + ) + } + > + {displayList.map((o, idx) => { + const phoneToCall = o.phone || o.mobile + const flow1Done = !!o.riderId + const flow2Done = o.deliveryStatus === 20 || o.deliveryStatus === 30 + const flow3Done = !!o.sendEndTime + const flow4Done = o.orderStatus === 1 + // 直接使用订单分页接口返回的 orderGoods + const goodsList = o.orderGoods || [] + const goodsNameList = goodsList + .map(g => g?.goodsName || (g as any)?.goodsTitle || (g as any)?.title || (g as any)?.name) + .filter(Boolean) as string[] + const goodsSummary = goodsNameList.length + ? `${goodsNameList.slice(0, 3).join('、')}${goodsList.length > 3 ? ` 等${goodsList.length}件` : ''}` + : (o.title || '-') + + const autoConfirmAt = o.sendEndTime + ? dayjs(o.sendEndTime).add(AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK, 'hour') + : null + const autoConfirmLeftMin = autoConfirmAt ? autoConfirmAt.diff(dayjs(), 'minute') : null + + return ( + o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})} + > + + + {o.orderNo || `订单#${o.orderId}`} + {getOrderStatusText(o)} + + + + 下单时间:{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'} + + + + + 收货点: + {o.selfTakeMerchantName || o.address || '-'} + + + 客户: + {o.realName || '-'} {o.phone ? `(${o.phone})` : ''} + + + 金额: + ¥{o.payPrice || o.totalPrice || '-'} + 数量: + {o.totalNum ?? '-'} + + + 商品: + {goodsSummary} + + {o.sendEndTime && ( + + 送达时间: + {dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')} + + )} + + + {/* 配送流程 */} + + 流程: + 1 派单 + {'>'} + 2 配送中 + {'>'} + 3 送达收货点 + {'>'} + 4 客户确认收货 + + {o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && ( + + 若客户未确认,预计 {autoConfirmAt.format('YYYY-MM-DD HH:mm')} 自动确认收货(以后台配置为准) + {typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''} + + )} + + + + + {!!phoneToCall && ( + + )} + {canConfirmDelivered(o) && ( + + )} + + + + + ) + })} + + )} + + + + { + if (deliverSubmitting) return + setDeliverDialogVisible(false) + setDeliverOrder(null) + setDeliverImg(undefined) + }} + > + + 到达收货点后,可选拍照留存,再点确认送达。 + + + + {deliverImg && ( + + + + + + + )} + + + + ) +} diff --git a/src/shop/category/components/GoodsList.tsx b/src/shop/category/components/GoodsList.tsx index bf2eb4a..f84ae9e 100644 --- a/src/shop/category/components/GoodsList.tsx +++ b/src/shop/category/components/GoodsList.tsx @@ -1,60 +1,54 @@ import {Image} 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 {CmsNavigation} from "@/api/cms/cmsNavigation/model" import './GoodsList.scss' -interface GoodsListProps { - data: ShopGoods[] - nav?: CmsNavigation -} -const GoodsList = (props: GoodsListProps) => { +const GoodsList = (props: any) => { return ( <> - - - - {props.data?.map((item, index) => { - return ( - - - Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})} - /> - - {item.name} - - {item.comments} + + + {props.data?.map((item: any, index: number) => { + return ( + + Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> + + + {item.name} + + {item.comments} + 已售 {item.sales} + + + + + {item.price} + 会员价 + ¥{item.salePrice} - - - - {item.price} + + + Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> - - 已售 {item.sales} + Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买 - ) - })} - + + ) + })} ) } -export default GoodsList \ No newline at end of file +export default GoodsList diff --git a/src/shop/category/index.tsx b/src/shop/category/index.tsx index b397525..7899e20 100644 --- a/src/shop/category/index.tsx +++ b/src/shop/category/index.tsx @@ -42,7 +42,7 @@ function Category() { useShareAppMessage(() => { return { - title: `${nav?.categoryName}_WebSoft Inc.`, + title: `${nav?.categoryName}_桂乐淘`, path: `/shop/category/index?id=${categoryId}`, success: function () { console.log('分享成功'); diff --git a/src/passport/register.config.ts b/src/shop/comments/index.config.ts similarity index 62% rename from src/passport/register.config.ts rename to src/shop/comments/index.config.ts index 77ed0bd..6d901e3 100644 --- a/src/passport/register.config.ts +++ b/src/shop/comments/index.config.ts @@ -1,4 +1,4 @@ export default definePageConfig({ - navigationBarTitleText: '注册账号', + navigationBarTitleText: '全部评论', navigationBarTextStyle: 'black' }) diff --git a/src/shop/comments/index.tsx b/src/shop/comments/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/shop/goodsDetail/index.tsx b/src/shop/goodsDetail/index.tsx index be35024..df66a4b 100644 --- a/src/shop/goodsDetail/index.tsx +++ b/src/shop/goodsDetail/index.tsx @@ -15,6 +15,7 @@ import SpecSelector from "@/components/SpecSelector"; import "./index.scss"; import {useCart} from "@/hooks/useCart"; import {useConfig} from "@/hooks/useConfig"; +import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite"; const GoodsDetail = () => { const [statusBarHeight, setStatusBarHeight] = useState(44); @@ -39,6 +40,24 @@ const GoodsDetail = () => { const {cartCount, addToCart} = useCart() const {config} = useConfig() + // 如果从分享链接进入(携带 inviter/source/t),且当前未登录,则暂存邀请信息用于注册后绑定关系 + useEffect(() => { + try { + const currentUserId = Taro.getStorageSync('UserId') + if (currentUserId) return + + const inviteParams = parseInviteParams({query: router?.params}) + if (inviteParams?.inviter) { + saveInviteParams(inviteParams) + trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter)) + } + } catch (e) { + // 邀请参数解析/存储失败不影响正常浏览商品 + console.error('商品详情页处理邀请参数失败:', e) + } + // router 在 Taro 中可能不稳定;这里仅在 goodsId 变化时尝试处理一次即可 + }, [goodsId]) + // 处理加入购物车 const handleAddToCart = () => { if (!goods) return; @@ -186,10 +205,16 @@ const GoodsDetail = () => { // 分享给好友 useShareAppMessage(() => { + const inviter = Taro.getStorageSync('UserId') + const sharePath = + inviter + ? `/shop/goodsDetail/index?id=${goodsId}&inviter=${inviter}&source=goods_share&t=${Date.now()}` + : `/shop/goodsDetail/index?id=${goodsId}` + return { title: goods?.name || '精选商品', - path: `/shop/goodsDetail/index?id=${goodsId}`, - imageUrl: goods?.image, // 分享图片 + path: sharePath, + imageUrl: goods?.image ? `${goods.image}?x-oss-process=image/resize,w_500,h_400,m_fill` : undefined, // 分享图片,调整为5:4比例 success: function (res: any) { console.log('分享成功', res); Taro.showToast({ @@ -280,8 +305,10 @@ const GoodsDetail = () => { <> - - {goods.price} + + {goods.price} + 会员价 + ¥{goods.salePrice} 已售 {goods.sales} @@ -342,9 +369,7 @@ const GoodsDetail = () => { )} - - - + diff --git a/src/shop/orderConfirm/index.tsx b/src/shop/orderConfirm/index.tsx index 58fe1c1..6ac3bd5 100644 --- a/src/shop/orderConfirm/index.tsx +++ b/src/shop/orderConfirm/index.tsx @@ -74,20 +74,20 @@ const OrderConfirm = () => { const getGoodsTotal = () => { if (!goods) return 0 const price = parseFloat(goods.price || '0') - const total = price * quantity + // const total = price * quantity // 🔍 详细日志,用于排查数值精度问题 - console.log('💵 商品总价计算:', { - goodsPrice: goods.price, - goodsPriceType: typeof goods.price, - parsedPrice: price, - quantity: quantity, - total: total, - totalFixed2: total.toFixed(2), - totalString: total.toString() - }) + // console.log('💵 商品总价计算:', { + // goodsPrice: goods.price, + // goodsPriceType: typeof goods.price, + // parsedPrice: price, + // quantity: quantity, + // total: total, + // totalFixed2: total.toFixed(2), + // totalString: total.toString() + // }) - return total + return price * quantity } // 计算优惠券折扣 @@ -418,9 +418,9 @@ const OrderConfirm = () => { couponId: parseInt(String(bestCoupon.id), 10) } ); - + console.log('🎯 使用推荐优惠券的订单数据:', updatedOrderData); - + // 执行支付 await PaymentHandler.pay(updatedOrderData, currentPaymentType); return; // 提前返回,避免重复执行支付 @@ -437,7 +437,7 @@ const OrderConfirm = () => { quantity, address.id, { - comments: goods.name, + comments: '网宿软件', deliveryType: 0, buyerRemarks: orderRemark, // 🔧 确保 couponId 是正确的数字类型,且不传递 undefined diff --git a/src/shop/orderDetail/index.tsx b/src/shop/orderDetail/index.tsx index c51595e..6350bcb 100644 --- a/src/shop/orderDetail/index.tsx +++ b/src/shop/orderDetail/index.tsx @@ -1,5 +1,5 @@ import {useEffect, useState} from "react"; -import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro' +import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-taro' import Taro from '@tarojs/taro' import {View} from '@tarojs/components' import {ShopOrder} from "@/api/shop/shopOrder/model"; @@ -13,6 +13,7 @@ import './index.scss' const OrderDetail = () => { const [order, setOrder] = useState(null); const [orderGoodsList, setOrderGoodsList] = useState([]); + const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false) const router = Taro.getCurrentInstance().router; const orderId = router?.params?.orderId; @@ -40,6 +41,52 @@ const OrderDetail = () => { } }; + // 申请退款 + const handleApplyRefund = async () => { + if (order) { + try { + // 更新订单状态为"退款申请中" + await updateShopOrder({ + orderId: order.orderId, + orderStatus: 4 // 退款申请中 + }); + + // 更新本地状态 + setOrder(prev => prev ? {...prev, orderStatus: 4} : null); + + // 跳转到退款申请页面 + Taro.navigateTo({ + url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}` + }); + } catch (error) { + console.error('更新订单状态失败:', error); + Taro.showToast({ + title: '操作失败,请重试', + icon: 'none' + }); + } + } + }; + + // 确认收货(客户) + const handleConfirmReceive = async () => { + if (!order?.orderId) return + try { + setConfirmReceiveDialogVisible(false) + await updateShopOrder({ + orderId: order.orderId, + deliveryStatus: order.deliveryStatus, // 10未发货 20已发货 30部分发货 + orderStatus: 1 // 已完成 + }) + Taro.showToast({title: '确认收货成功', icon: 'success'}) + setOrder(prev => (prev ? {...prev, orderStatus: 1} : prev)) + } catch (e) { + console.error('确认收货失败:', e) + Taro.showToast({title: '确认收货失败', icon: 'none'}) + setConfirmReceiveDialogVisible(true) + } + } + const getOrderStatusText = (order: ShopOrder) => { // 优先检查订单状态 if (order.orderStatus === 2) return '已取消'; @@ -54,8 +101,15 @@ const OrderDetail = () => { // 已付款后检查发货状态 if (order.deliveryStatus === 10) return '待发货'; - if (order.deliveryStatus === 20) return '待收货'; - if (order.deliveryStatus === 30) return '已收货'; + if (order.deliveryStatus === 20) { + // 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据 + if (order.riderId) { + if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货'; + return '配送中'; + } + return '待收货'; + } + if (order.deliveryStatus === 30) return '部分发货'; // 最后检查订单完成状态 if (order.orderStatus === 1) return '已完成'; @@ -106,6 +160,15 @@ const OrderDetail = () => { return
加载中...
; } + const currentUserId = Number(Taro.getStorageSync('UserId')) + const isOwner = !!currentUserId && currentUserId === order.userId + const canConfirmReceive = + isOwner && + order.payStatus && + order.orderStatus !== 1 && + order.deliveryStatus === 20 && + (!order.riderId || !!order.sendEndTime) + return (
{/* 支付倒计时显示 - 详情页实时更新 */} @@ -162,12 +225,26 @@ const OrderDetail = () => { {!order.payStatus && } {!order.payStatus && } - {order.orderStatus === 1 && } - {order.deliveryStatus === 20 && - } + {order.orderStatus === 1 && } + {canConfirmReceive && ( + + )} + + setConfirmReceiveDialogVisible(false)} + > + 确定已经收到商品了吗?确认后订单将完成。 +
); }; diff --git a/src/store/index.config.ts b/src/store/index.config.ts new file mode 100644 index 0000000..5ec6c4d --- /dev/null +++ b/src/store/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '门店中心' +}) diff --git a/src/store/index.scss b/src/store/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/store/index.tsx b/src/store/index.tsx new file mode 100644 index 0000000..37ef85a --- /dev/null +++ b/src/store/index.tsx @@ -0,0 +1,281 @@ +import React, {useCallback, useState} from 'react' +import {View, Text} from '@tarojs/components' +import {Avatar, Button, ConfigProvider, Grid} from '@nutui/nutui-react-taro' +import {Location, Scan, Shop, Shopping, User} from '@nutui/icons-react-taro' +import Taro, {useDidShow} from '@tarojs/taro' +import {useThemeStyles} from '@/hooks/useTheme' +import {useUser} from '@/hooks/useUser' +import {getSelectedStoreFromStorage} from '@/utils/storeSelection' +import {listShopStoreUser} from '@/api/shop/shopStoreUser' +import {getShopStore} from '@/api/shop/shopStore' +import type {ShopStore as ShopStoreModel} from '@/api/shop/shopStore/model' + +const StoreIndex: React.FC = () => { + const themeStyles = useThemeStyles() + const {isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName, getRoleName, hasRole} = useUser() + + const [boundStoreId, setBoundStoreId] = useState(undefined) + const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage()) + const [store, setStore] = useState(selectedStore) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const storeId = boundStoreId || selectedStore?.id + + const parseStoreCoords = (s: ShopStoreModel): {lng: number; lat: number} | null => { + const raw = (s.lngAndLat || s.location || '').trim() + if (!raw) return null + + const parts = raw.split(/[,\s]+/).filter(Boolean) + if (parts.length < 2) return null + + const a = parseFloat(parts[0]) + const b = parseFloat(parts[1]) + if (Number.isNaN(a) || Number.isNaN(b)) return null + + // 常见格式是 "lng,lat";这里做一个简单兜底 + const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90 + const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180 + if (looksLikeLngLat) return {lng: a, lat: b} + if (looksLikeLatLng) return {lng: b, lat: a} + return null + } + + const navigateToPage = (url: string) => { + if (!isLoggedIn) { + Taro.showToast({title: '请先登录', icon: 'none', duration: 1500}) + return + } + Taro.navigateTo({url}) + } + + const refresh = useCallback(async () => { + setError(null) + setLoading(true) + try { + const latestSelectedStore = getSelectedStoreFromStorage() + setSelectedStore(latestSelectedStore) + + const userIdRaw = Number(Taro.getStorageSync('UserId')) + const userId = Number.isFinite(userIdRaw) && userIdRaw > 0 ? userIdRaw : undefined + + let foundStoreId: number | undefined = undefined + if (userId) { + // 优先按“店员绑定关系”确定门店归属 + try { + const list = await listShopStoreUser({userId}) + const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId) + foundStoreId = first?.storeId + setBoundStoreId(foundStoreId) + } catch { + // fallback to SelectedStore + foundStoreId = undefined + setBoundStoreId(undefined) + } + } else { + foundStoreId = undefined + setBoundStoreId(undefined) + } + + const nextStoreId = (foundStoreId || latestSelectedStore?.id) + if (!nextStoreId) { + setStore(latestSelectedStore) + return + } + + // 获取门店详情(用于展示门店名称/地址/仓库等) + const full = await getShopStore(nextStoreId) + setStore(full || (latestSelectedStore?.id === nextStoreId ? latestSelectedStore : ({id: nextStoreId} as ShopStoreModel))) + } catch (e: any) { + const msg = e?.message || '获取门店信息失败' + setError(msg) + } finally { + setLoading(false) + } + }, []) + + // 返回/切换到该页面时,同步最新的已选门店与绑定门店 + useDidShow(() => { + refresh().catch(() => {}) + }) + + const openStoreLocation = () => { + if (!store?.id) { + return Taro.showToast({title: '请先选择门店', icon: 'none'}) + } + const coords = parseStoreCoords(store) + if (!coords) { + return Taro.showToast({title: '门店未配置定位', icon: 'none'}) + } + Taro.openLocation({ + latitude: coords.lat, + longitude: coords.lng, + name: store.name || '门店', + address: store.address || '' + }) + } + + if (!isLoggedIn && !userLoading) { + return ( + + + 请先登录后再进入门店中心 + + + + + + ) + } + + return ( + + {/* 头部信息 */} + + + + + + + } + className="mr-4" + style={{border: '2px solid rgba(255, 255, 255, 0.3)'}} + /> + + + {getDisplayName()} + + + {hasRole('store') ? '门店' : hasRole('rider') ? '配送员' : getRoleName()} + + + + + + + {/* 门店信息 */} + + + 当前门店 + Taro.switchTab({url: '/pages/index/index'})} + > + 切换门店 + + + + {!storeId ? ( + + + 未选择门店,请先去首页选择门店。 + + + + + + ) : ( + + + {store?.name || `门店ID: ${storeId}`} + + {!!store?.address && ( + + {store.address} + + )} + {!!store?.warehouseName && ( + + 默认仓库:{store.warehouseName} + + )} + {!!error && ( + + {error} + + )} + + )} + + + {/* 功能入口 */} + + 门店工具 + + + navigateToPage('/store/orders/index')}> + + + + + + + + navigateToPage('/user/store/verification')}> + + + + + + + + + + + + + + + + Taro.switchTab({url: '/pages/index/index'})}> + + + + + + + + + + + + + ) +} + +export default StoreIndex diff --git a/src/store/orders/index.config.ts b/src/store/orders/index.config.ts new file mode 100644 index 0000000..4de19d9 --- /dev/null +++ b/src/store/orders/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '门店订单', + navigationBarTextStyle: 'black' +} diff --git a/src/store/orders/index.tsx b/src/store/orders/index.tsx new file mode 100644 index 0000000..bcd5e8f --- /dev/null +++ b/src/store/orders/index.tsx @@ -0,0 +1,83 @@ +import {useEffect, useMemo, useState} from 'react' +import Taro from '@tarojs/taro' +import {Button} from '@nutui/nutui-react-taro' +import {View, Text} from '@tarojs/components' +import OrderList from '@/user/order/components/OrderList' +import {getSelectedStoreFromStorage} from '@/utils/storeSelection' +import {listShopStoreUser} from '@/api/shop/shopStoreUser' + +export default function StoreOrders() { + const [boundStoreId, setBoundStoreId] = useState(undefined) + + const isLoggedIn = useMemo(() => { + return !!Taro.getStorageSync('access_token') && !!Taro.getStorageSync('UserId') + }, []) + + const selectedStore = useMemo(() => getSelectedStoreFromStorage(), []) + const storeId = boundStoreId || selectedStore?.id + + useEffect(() => { + }, []) + + useEffect(() => { + // 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单 + const userId = Number(Taro.getStorageSync('UserId')) + if (!Number.isFinite(userId) || userId <= 0) return + listShopStoreUser({userId}).then(list => { + const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId) + if (first?.storeId) setBoundStoreId(first.storeId) + }).catch(() => { + // fallback to SelectedStore + }) + }, []) + + if (!isLoggedIn) { + return ( + + + 请先登录 + + + + + + ) + } + + return ( + + + + + 当前门店: + + {boundStoreId + ? (selectedStore?.id === boundStoreId ? (selectedStore?.name || `门店ID: ${boundStoreId}`) : `门店ID: ${boundStoreId}`) + : (selectedStore?.name || '未选择门店')} + + + + {!storeId ? ( + + + 请先在首页左上角选择门店,再查看门店订单。 + + + + + + ) : ( + + )} + + + ) +} diff --git a/src/styles/gradients.ts b/src/styles/gradients.ts index 2ad1cb4..11ab23a 100644 --- a/src/styles/gradients.ts +++ b/src/styles/gradients.ts @@ -40,9 +40,9 @@ export const gradientThemes: GradientTheme[] = [ }, { name: 'nature', - primary: '#43e97b', - secondary: '#38f9d7', - background: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', + primary: '#03605c', + secondary: '#18ae4f', + background: 'linear-gradient(135deg, #03605c 0%, #18ae4f 100%)', textColor: '#ffffff', description: '自然绿青 - 生机与成长' }, diff --git a/src/types/giftCard.ts b/src/types/giftCard.ts index 9178651..3736e25 100644 --- a/src/types/giftCard.ts +++ b/src/types/giftCard.ts @@ -4,7 +4,7 @@ /** 礼品卡类型枚举 */ export enum GiftCardType { - /** 实物礼品卡 */ + /** 礼品劵 */ PHYSICAL = 10, /** 虚拟礼品卡 */ VIRTUAL = 20, diff --git a/src/user/about/index.tsx b/src/user/about/index.tsx index fec73d9..5011331 100644 --- a/src/user/about/index.tsx +++ b/src/user/about/index.tsx @@ -9,15 +9,14 @@ import {listCmsNavigation} from "@/api/cms/cmsNavigation"; import {View, RichText} from '@tarojs/components' import {listCmsDesign} from "@/api/cms/cmsDesign"; import {CmsDesign} from "@/api/cms/cmsDesign/model"; -import {type Config} from "@/api/cms/cmsWebsiteField/model"; -import {configWebsiteField} from "@/api/cms/cmsWebsiteField"; +import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook const Helper = () => { const [nav, setNav] = useState() const [design, setDesign] = useState() const [category, setCategory] = useState([]) - const [config, setConfig] = useState() + const { config } = useConfig(); // 使用新的Hook const reload = async () => { const navs = await listCmsNavigation({model: 'page', parentId: 0}); @@ -33,9 +32,7 @@ const Helper = () => { category[index].articles = await listCmsArticle({categoryId: item.navigationId}); }) setCategory(category) - // 查询字段 - const configInfo = await configWebsiteField({}) - setConfig(configInfo) + // 注意:config 现在通过 useConfig Hook 获取,不再在这里调用 configWebsiteField } } diff --git a/src/user/chat/message/add.tsx b/src/user/chat/message/add.tsx index cadf847..a455fd0 100644 --- a/src/user/chat/message/add.tsx +++ b/src/user/chat/message/add.tsx @@ -110,7 +110,7 @@ const AddMessage = () => { ) : '选择发送对象'} extra={( )} - onClick={() => navTo(`/doctor/team/index`, true)}/> + onClick={() => navTo(`/dealer/team/index`, true)}/>
{ ) : '选择发送对象'} extra={( )} - onClick={() => navTo(`/doctor/team/index`, true)}/> + onClick={() => navTo(`/dealer/team/index`, true)}/> diff --git a/src/user/gift/detail.tsx b/src/user/gift/detail.tsx index e2ab775..b94841e 100644 --- a/src/user/gift/detail.tsx +++ b/src/user/gift/detail.tsx @@ -43,10 +43,10 @@ const GiftCardDetail = () => { // 获取礼品卡类型文本 const getGiftTypeText = (type?: number) => { switch (type) { - case 10: return '实物礼品卡' + case 10: return '礼品劵' case 20: return '虚拟礼品卡' case 30: return '服务礼品卡' - default: return '礼品卡' + default: return '水票' } } diff --git a/src/user/gift/index.config.ts b/src/user/gift/index.config.ts index c3474f2..521bff3 100644 --- a/src/user/gift/index.config.ts +++ b/src/user/gift/index.config.ts @@ -1,5 +1,5 @@ export default definePageConfig({ - navigationBarTitleText: '我的礼品卡', + navigationBarTitleText: '我的水票', navigationBarTextStyle: 'black', navigationBarBackgroundColor: '#ffffff' }) diff --git a/src/user/gift/index.tsx b/src/user/gift/index.tsx index aaae782..d7f5fab 100644 --- a/src/user/gift/index.tsx +++ b/src/user/gift/index.tsx @@ -1,7 +1,7 @@ import {useState} from "react"; import Taro, {useDidShow} from '@tarojs/taro' import {Button, Empty, ConfigProvider,SearchBar, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro' -import {Gift, Retweet, Board, QrCode} from '@nutui/icons-react-taro' +import {Gift, Retweet, QrCode} from '@nutui/icons-react-taro' import {View} from '@tarojs/components' import {ShopGift} from "@/api/shop/shopGift/model"; import {getUserGifts} from "@/api/shop/shopGift"; @@ -24,7 +24,7 @@ const GiftCardManage = () => { // sortOrder: 'desc' as 'asc' | 'desc' // }) - // 获取礼品卡状态过滤条件 + // 获取水票状态过滤条件 const getStatusFilter = () => { switch (String(activeTab)) { case '0': // 未使用 @@ -52,7 +52,7 @@ const GiftCardManage = () => { } } - // 根据状态过滤条件加载礼品卡 + // 根据状态过滤条件加载水票 const loadGiftsByStatus = async (statusFilter: any) => { setLoading(true) try { @@ -72,9 +72,9 @@ const GiftCardManage = () => { setHasMore(false) } } catch (error) { - console.error('获取礼品卡失败:', error) + console.error('获取水票失败:', error) Taro.showToast({ - title: '获取礼品卡失败', + title: '获取水票失败', icon: 'error' }) } finally { @@ -125,9 +125,9 @@ const GiftCardManage = () => { // setTotal(0) } } catch (error) { - console.error('获取礼品卡失败:', error) + console.error('获取水票失败:', error) Taro.showToast({ - title: '获取礼品卡失败', + title: '获取水票失败', icon: 'error' }); } finally { @@ -162,11 +162,11 @@ const GiftCardManage = () => { loadGiftsByStatus(statusFilter) } - // 转换礼品卡数据为GiftCard组件所需格式 + // 转换水票数据为GiftCard组件所需格式 const transformGiftData = (gift: ShopGift): GiftCardProps => { return { id: gift.id || 0, - name: gift.name || '礼品卡', // 礼品卡名称 + name: gift.name || '水票', // 水票名称 goodsName: gift.goodsName, // 商品名称(新增字段) description: gift.description || gift.instructions, // 使用说明作为描述 code: gift.code, @@ -180,23 +180,23 @@ const GiftCardManage = () => { contactInfo: gift.contactInfo, // 添加商品信息 goodsInfo: { - // 如果有商品名称或商品ID,说明是关联商品的礼品卡 + // 如果有商品名称或商品ID,说明是关联商品的水票 ...((gift.goodsName || gift.goodsId) && { - specification: `礼品卡面值:¥${gift.faceValue}`, + specification: `水票面值:¥${gift.faceValue}`, category: getTypeText(gift.type), tags: [ getTypeText(gift.type), gift.status === 0 ? '未使用' : gift.status === 1 ? '已使用' : '失效', - ...(gift.goodsName ? ['商品礼品卡'] : []) + ...(gift.goodsName ? ['商品水票'] : []) ].filter(Boolean), instructions: gift.instructions ? [gift.instructions] : [ '请在有效期内使用', '出示兑换码即可使用', '不可兑换现金', - ...(gift.goodsName ? ['此礼品卡关联具体商品'] : []) + ...(gift.goodsName ? ['此水票关联具体商品'] : []) ], notices: [ - '礼品卡一经使用不可退换', + '水票一经使用不可退换', '请妥善保管兑换码', '如有疑问请联系客服', ...(gift.goodsName ? ['商品以实际为准'] : []) @@ -213,34 +213,34 @@ const GiftCardManage = () => { } } - // 获取礼品卡类型文本 + // 获取水票类型文本 const getTypeText = (type?: number): string => { switch (type) { - case 10: return '实物礼品卡' - case 20: return '虚拟礼品卡' - case 30: return '服务礼品卡' - default: return '礼品卡' + case 10: return '实物水票' + case 20: return '虚拟水票' + case 30: return '服务水票' + default: return '水票' } } - // 根据礼品卡类型获取主题色 + // 根据水票类型获取主题色 const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => { switch (type) { - case 10: return 'gold' // 实物礼品卡 - 金色 - case 20: return 'blue' // 虚拟礼品卡 - 蓝色 - case 30: return 'green' // 服务礼品卡 - 绿色 + case 10: return 'gold' // 实物水票 - 金色 + case 20: return 'blue' // 虚拟水票 - 蓝色 + case 30: return 'green' // 服务水票 - 绿色 default: return 'purple' // 默认使用紫色主题,更美观 } } - // 使用礼品卡 + // 使用水票 const handleUseGift = (gift: ShopGift) => { Taro.showModal({ - title: '使用礼品卡', + title: '使用水票', content: `确定要使用"${gift.name}"吗?`, success: (res) => { if (res.confirm) { - // 跳转到礼品卡使用页面 + // 跳转到水票使用页面 Taro.navigateTo({ url: `/user/gift/use?id=${gift.id}` }) @@ -249,35 +249,35 @@ const GiftCardManage = () => { }) } - // 礼品卡点击事件 + // 水票点击事件 const handleGiftClick = (gift: GiftCardProps, index: number) => { console.log(gift.code) const originalGift = list[index] if (originalGift) { - // 显示礼品卡详情 + // 显示水票详情 handleGiftDetail(originalGift) } } - // 显示礼品卡详情 + // 显示水票详情 const handleGiftDetail = (gift: ShopGift) => { - // 跳转到礼品卡详情页 + // 跳转到水票详情页 Taro.navigateTo({ url: `/user/gift/detail?id=${gift.id}` }) } - // 加载礼品卡统计数据 + // 加载水票统计数据 // const loadGiftStats = async () => { // try { - // // 并行获取各状态的礼品卡数量 + // // 并行获取各状态的水票数量 // const [availableRes, usedRes, expiredRes] = await Promise.all([ // getUserGifts({ page: 1, limit: 1, useStatus: 0 }), // getUserGifts({ page: 1, limit: 1, useStatus: 1 }), // getUserGifts({ page: 1, limit: 1, useStatus: 2 }) // ]) // - // // 计算总价值(仅可用礼品卡) + // // 计算总价值(仅可用水票) // const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 }) // const totalValue = availableGifts?.list?.reduce((sum, gift) => { // return sum + parseFloat(gift.faceValue || '0') @@ -290,7 +290,7 @@ const GiftCardManage = () => { // totalValue // }) // } catch (error) { - // console.error('获取礼品卡统计失败:', error) + // console.error('获取水票统计失败:', error) // } // } @@ -300,21 +300,21 @@ const GiftCardManage = () => { // available: '0', // used: '1', // expired: '2', - // total: '0' // 总价值点击跳转到可用礼品卡 + // total: '0' // 总价值点击跳转到可用水票 // } // if (tabMap[type]) { // handleTabChange(tabMap[type]) // } // } - // 兑换礼品卡 + // 兑换水票 const handleRedeemGift = () => { Taro.navigateTo({ url: '/user/gift/redeem' }) } - // 扫码兑换礼品卡 + // 扫码兑换水票 const handleScanRedeem = () => { Taro.scanCode({ success: (res) => { @@ -377,14 +377,14 @@ const GiftCardManage = () => { > 扫码 - + {/*}*/} + {/* onClick={() => setShowGuide(true)}*/} + {/*>*/} + {/* 帮助*/} + {/**/} @@ -400,7 +400,7 @@ const GiftCardManage = () => { - {/* 礼品卡列表 */} + {/* 水票列表 */} { @@ -450,14 +450,14 @@ const GiftCardManage = () => { - 暂无未使用礼品卡 + 暂无未使用水票 */} - {/*)}*/} + {item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && ( + + )} {/* 待收货状态:显示查看物流和确认收货 */} - {item.deliveryStatus === 20 && item.orderStatus !== 2 && ( + {item.deliveryStatus === 20 && (!item.riderId || !!item.sendEndTime) && item.orderStatus !== 2 && ( - {/**/} + */} - {/**/} + )} @@ -795,6 +833,7 @@ function OrderList(props: OrderListProps) { }}>再次购买 )} + )} ) diff --git a/src/user/order/order.tsx b/src/user/order/order.tsx index a9c985d..d018696 100644 --- a/src/user/order/order.tsx +++ b/src/user/order/order.tsx @@ -4,7 +4,7 @@ import {Space, NavBar, Button, Input} from '@nutui/nutui-react-taro' import {Search, Filter, ArrowLeft} from '@nutui/icons-react-taro' import {View} from '@tarojs/components'; import OrderList from "./components/OrderList"; -import {useRouter} from '@tarojs/taro' +import {useDidShow, useRouter} from '@tarojs/taro' import {ShopOrderParam} from "@/api/shop/shopOrder/model"; import './order.scss' @@ -72,6 +72,17 @@ function Order() { reload().then() }, []); + // 页面从其它页面返回/重新展示时,刷新一次列表数据 + // 典型场景:微信支付取消后返回到待支付列表,需要重新拉取订单/商品信息,避免使用旧数据再次支付失败 + useDidShow(() => { + const statusFilter = + params.statusFilter != undefined && params.statusFilter !== '' + ? parseInt(params.statusFilter) + : -1; + // 同步路由上的statusFilter,并触发子组件重新拉取列表 + setSearchParams(prev => ({ ...prev, statusFilter })); + }); + return ( diff --git a/src/user/order/refund/index.tsx b/src/user/order/refund/index.tsx index da16d92..b484c09 100644 --- a/src/user/order/refund/index.tsx +++ b/src/user/order/refund/index.tsx @@ -13,6 +13,8 @@ import { Empty, InputNumber } from '@nutui/nutui-react-taro' +import { applyAfterSale } from '@/api/afterSale' +import { updateShopOrder } from '@/api/shop/shopOrder' import './index.scss' // 订单商品信息 @@ -232,23 +234,55 @@ const RefundPage: React.FC = () => { setSubmitting(true) - // 模拟API调用 - await new Promise(resolve => setTimeout(resolve, 2000)) + // 构造请求参数 + const params = { + orderId: orderId || '', + type: 'refund' as const, + reason: refundApp.refundReason, + description: refundApp.refundDescription, + amount: refundApp.refundAmount, + contactPhone: refundApp.contactPhone, + evidenceImages: refundApp.evidenceImages, + goodsItems: refundApp.refundGoods.filter(item => item.refundNum > 0).map(item => ({ + goodsId: item.goodsId, + quantity: item.refundNum + })) + } - Taro.showToast({ - title: '退款申请提交成功', - icon: 'success' - }) + // 调用API提交退款申请 + const result = await applyAfterSale(params) + + if (result.success) { + // 更新订单状态为"退款申请中" + if (orderId) { + try { + await updateShopOrder({ + orderId: parseInt(orderId), + orderStatus: 4 // 退款申请中 + }) + } catch (updateError) { + console.error('更新订单状态失败:', updateError) + // 即使更新订单状态失败,也继续执行后续操作 + } + } - // 延迟返回上一页 - setTimeout(() => { - Taro.navigateBack() - }, 1500) + Taro.showToast({ + title: '退款申请提交成功', + icon: 'success' + }) + + // 延迟返回上一页 + setTimeout(() => { + Taro.navigateBack() + }, 1500) + } else { + throw new Error(result.message || '提交失败') + } } catch (error) { console.error('提交退款申请失败:', error) Taro.showToast({ - title: '提交失败,请重试', + title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) } finally { @@ -420,4 +454,4 @@ const RefundPage: React.FC = () => { ) } -export default RefundPage +export default RefundPage \ No newline at end of file diff --git a/src/user/store/orders/index.config.ts b/src/user/store/orders/index.config.ts new file mode 100644 index 0000000..4de19d9 --- /dev/null +++ b/src/user/store/orders/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '门店订单', + navigationBarTextStyle: 'black' +} diff --git a/src/user/store/orders/index.tsx b/src/user/store/orders/index.tsx new file mode 100644 index 0000000..d555dcc --- /dev/null +++ b/src/user/store/orders/index.tsx @@ -0,0 +1,73 @@ +import {useEffect, useMemo, useState} from 'react' +import Taro from '@tarojs/taro' +import {NavBar, Button} from '@nutui/nutui-react-taro' +import {ArrowLeft} from '@nutui/icons-react-taro' +import {View, Text} from '@tarojs/components' +import OrderList from '@/user/order/components/OrderList' +import {getSelectedStoreFromStorage} from '@/utils/storeSelection' +import {listShopStoreUser} from '@/api/shop/shopStoreUser' + +export default function StoreOrders() { + const [statusBarHeight, setStatusBarHeight] = useState(0) + + const [boundStoreId, setBoundStoreId] = useState(undefined) + const store = useMemo(() => getSelectedStoreFromStorage(), []) + const storeId = boundStoreId || store?.id + + useEffect(() => { + Taro.getSystemInfo({ + success: (res) => setStatusBarHeight(res.statusBarHeight ?? 0) + }) + }, []) + + useEffect(() => { + // 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单 + const userId = Number(Taro.getStorageSync('UserId')) + if (!Number.isFinite(userId) || userId <= 0) return + listShopStoreUser({userId}).then(list => { + const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId) + if (first?.storeId) setBoundStoreId(first.storeId) + }).catch(() => { + // fallback to SelectedStore + }) + }, []) + + return ( + + + Taro.navigateBack()}/>} + > + 门店订单 + + + + + 当前门店: + {store?.name || (boundStoreId ? `门店ID: ${boundStoreId}` : '未选择门店')} + + + {!storeId ? ( + + + 请先在首页左上角选择门店,再查看门店订单。 + + + + + + ) : ( + + )} + + + ) +} diff --git a/src/user/store/verification.tsx b/src/user/store/verification.tsx index cb2cdb6..b5ea72a 100644 --- a/src/user/store/verification.tsx +++ b/src/user/store/verification.tsx @@ -161,13 +161,13 @@ const StoreVerification: React.FC = () => { const getTypeText = (type: number) => { switch (type) { case 10: - return '实物礼品卡' + return '礼品劵' case 20: return '虚拟礼品卡' case 30: return '服务礼品卡' default: - return '礼品卡' + return '水票' } } diff --git a/src/user/theme/index.tsx b/src/user/theme/index.tsx index ec41396..5abe11d 100644 --- a/src/user/theme/index.tsx +++ b/src/user/theme/index.tsx @@ -11,13 +11,13 @@ const ThemeSelector: React.FC = () => { // 获取当前主题 useEffect(() => { - const savedTheme = Taro.getStorageSync('user_theme') || 'auto' + const savedTheme = Taro.getStorageSync('user_theme') || 'nature' setSelectedTheme(savedTheme) if (savedTheme === 'auto') { // 自动主题:根据用户ID生成 - const userId = Taro.getStorageSync('userId') || '1' - const theme = gradientUtils.getThemeByUserId(userId) + const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1' + const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10)) setCurrentTheme(theme) } else { // 手动选择的主题 @@ -33,8 +33,8 @@ const ThemeSelector: React.FC = () => { setSelectedTheme(themeName) if (themeName === 'auto') { - const userId = Taro.getStorageSync('userId') || '1' - const theme = gradientUtils.getThemeByUserId(userId) + const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1' + const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10)) setCurrentTheme(theme) } else { const theme = gradientThemes.find(t => t.name === themeName) @@ -61,8 +61,8 @@ const ThemeSelector: React.FC = () => { // 预览主题 const previewTheme = (themeName: string) => { if (themeName === 'auto') { - const userId = Taro.getStorageSync('userId') || '1' - const theme = gradientUtils.getThemeByUserId(userId) + const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1' + const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10)) setCurrentTheme(theme) } else { const theme = gradientThemes.find(t => t.name === themeName) diff --git a/src/utils/customerStatus.ts b/src/utils/customerStatus.ts deleted file mode 100644 index d77f63b..0000000 --- a/src/utils/customerStatus.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 客户状态管理工具函数 - */ - -// 客户状态类型定义 -export type CustomerStatus = 'all' | 'pending' | 'signed' | 'cancelled'; - -// 客户状态配置 -export const CUSTOMER_STATUS_CONFIG = { - all: { - label: '全部', - color: '#666666', - tagType: 'default' as const - }, - pending: { - label: '跟进中', - color: '#ff8800', - tagType: 'warning' as const - }, - signed: { - label: '已签约', - color: '#52c41a', - tagType: 'success' as const - }, - cancelled: { - label: '已取消', - color: '#999999', - tagType: 'default' as const - } -}; - -/** - * 获取状态文本 - */ -export const getStatusText = (status: CustomerStatus): string => { - return CUSTOMER_STATUS_CONFIG[status]?.label || ''; -}; - -/** - * 获取状态标签类型 - */ -export const getStatusTagType = (status: CustomerStatus) => { - return CUSTOMER_STATUS_CONFIG[status]?.tagType || 'default'; -}; - -/** - * 获取状态颜色 - */ -export const getStatusColor = (status: CustomerStatus): string => { - return CUSTOMER_STATUS_CONFIG[status]?.color || '#666666'; -}; - -/** - * 获取所有状态选项 - */ -export const getStatusOptions = () => { - return Object.entries(CUSTOMER_STATUS_CONFIG).map(([value, config]) => ({ - value: value as CustomerStatus, - label: config.label - })); -}; - -/** - * 将数字状态映射为字符串状态 - */ -export const mapApplyStatusToCustomerStatus = (applyStatus: number): CustomerStatus => { - switch (applyStatus) { - case 10: - return 'pending'; // 跟进中 - case 20: - return 'signed'; // 已签约 - case 30: - return 'cancelled'; // 已取消 - default: - return 'pending'; // 默认为跟进中 - } -}; - -/** - * 将字符串状态映射为数字状态 - */ -export const mapCustomerStatusToApplyStatus = (customerStatus: CustomerStatus): number | undefined => { - switch (customerStatus) { - case 'pending': - return 10; // 跟进中 - case 'signed': - return 20; // 已签约 - case 'cancelled': - return 30; // 已取消 - case 'all': - return undefined; // 全部,不筛选 - default: - return undefined; - } -}; - -/** - * 临时函数:生成随机状态(实际项目中应该删除,从数据库获取真实状态) - */ -export const getRandomStatus = (): CustomerStatus => { - const statuses: CustomerStatus[] = ['pending', 'signed', 'cancelled']; - return statuses[Math.floor(Math.random() * statuses.length)]; -}; diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts deleted file mode 100644 index 126e75f..0000000 --- a/src/utils/dateUtils.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * 日期格式化工具函数 - * 用于处理各种日期格式转换 - */ - -/** - * 格式化日期为数据库格式 YYYY-MM-DD HH:mm:ss - * @param dateStr 输入的日期字符串,支持多种格式 - * @returns 数据库格式的日期字符串 - */ -export const formatDateForDatabase = (dateStr: string): string => { - if (!dateStr) return '' - - let parts: string[] = [] - - // 处理不同的日期格式 - if (dateStr.includes('/')) { - // 处理 YYYY/MM/DD 或 YYYY/M/D 格式 - parts = dateStr.split('/') - } else if (dateStr.includes('-')) { - // 处理 YYYY-MM-DD 或 YYYY-M-D 格式 - parts = dateStr.split('-') - } else { - return dateStr - } - - if (parts.length !== 3) return dateStr - - const year = parts[0] - const month = parts[1].padStart(2, '0') - const day = parts[2].padStart(2, '0') - - return `${year}-${month}-${day} 00:00:00` -} - -/** - * 从数据库格式提取日期部分用于Calendar组件显示 - * @param dateTimeStr 数据库格式的日期时间字符串 - * @returns Calendar组件需要的格式 (YYYY-M-D) - */ -export const extractDateForCalendar = (dateTimeStr: string): string => { - if (!dateTimeStr) return '' - - // 处理不同的输入格式 - let dateStr = '' - if (dateTimeStr.includes(' ')) { - // 从 "YYYY-MM-DD HH:mm:ss" 格式中提取日期部分 - dateStr = dateTimeStr.split(' ')[0] - } else { - dateStr = dateTimeStr - } - - // 转换为Calendar组件需要的格式 (YYYY-M-D) - if (dateStr.includes('-')) { - const parts = dateStr.split('-') - if (parts.length === 3) { - const year = parts[0] - const month = parseInt(parts[1]).toString() // 去掉前导0 - const day = parseInt(parts[2]).toString() // 去掉前导0 - return `${year}-${month}-${day}` - } - } - - return dateStr -} - -/** - * 格式化日期为用户友好的显示格式 YYYY-MM-DD - * @param dateStr 输入的日期字符串 - * @returns 用户友好的日期格式 - */ -export const formatDateForDisplay = (dateStr: string): string => { - if (!dateStr) return '' - - // 如果是数据库格式,先提取日期部分 - let dateOnly = dateStr - if (dateStr.includes(' ')) { - dateOnly = dateStr.split(' ')[0] - } - - // 如果已经是标准格式,直接返回 - if (/^\d{4}-\d{2}-\d{2}$/.test(dateOnly)) { - return dateOnly - } - - // 处理其他格式 - let parts: string[] = [] - if (dateOnly.includes('/')) { - parts = dateOnly.split('/') - } else if (dateOnly.includes('-')) { - parts = dateOnly.split('-') - } else { - return dateStr - } - - if (parts.length !== 3) return dateStr - - const year = parts[0] - const month = parts[1].padStart(2, '0') - const day = parts[2].padStart(2, '0') - - return `${year}-${month}-${day}` -} - -/** - * 获取当前日期的字符串格式 - * @param format 'database' | 'display' | 'calendar' - * @returns 格式化的当前日期 - */ -export const getCurrentDate = (format: 'database' | 'display' | 'calendar' = 'display'): string => { - const now = new Date() - const year = now.getFullYear() - const month = now.getMonth() + 1 - const day = now.getDate() - - switch (format) { - case 'database': - return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} 00:00:00` - case 'display': - return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}` - case 'calendar': - return `${year}-${month}-${day}` - default: - return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}` - } -} diff --git a/src/utils/invite.ts b/src/utils/invite.ts index 34975d6..cfd88a4 100644 --- a/src/utils/invite.ts +++ b/src/utils/invite.ts @@ -235,6 +235,7 @@ export function getSourceDisplayName(source: string): string { 'qrcode': '小程序码', 'link': '分享链接', 'share': '好友分享', + 'goods_share': '商品分享', 'poster': '海报分享', 'unknown': '未知来源' } diff --git a/src/utils/orderGoods.ts b/src/utils/orderGoods.ts new file mode 100644 index 0000000..ad12e37 --- /dev/null +++ b/src/utils/orderGoods.ts @@ -0,0 +1,32 @@ +import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model'; + +/** + * Normalize order goods data returned by the order/page API. + * + * In practice different backends may return different field names (orderGoods/orderGoodsList/goodsList...), + * and the item fields can also differ (goodsName/title/name, totalNum/quantity, etc.). + * + * We normalize them to ShopOrderGoods so list pages can render without doing N+1 requests per order. + */ +export const normalizeOrderGoodsList = (order: any): ShopOrderGoods[] => { + const raw = + order?.orderGoods || + order?.orderGoodsList || + order?.goodsList || + order?.goods || + []; + if (!Array.isArray(raw)) return []; + + return raw.map((g: any) => ({ + ...g, + goodsId: g?.goodsId ?? g?.itemId ?? g?.goods_id, + skuId: g?.skuId ?? g?.sku_id, + // When the API returns minimal fields, fall back to order title to avoid blank names. + goodsName: g?.goodsName ?? g?.goodsTitle ?? g?.title ?? g?.name ?? order?.title ?? '商品', + image: g?.image ?? g?.goodsImage ?? g?.cover ?? g?.pic, + spec: g?.spec ?? g?.specInfo ?? g?.spec_name, + totalNum: g?.totalNum ?? g?.quantity ?? g?.num ?? g?.count, + price: g?.price ?? g?.payPrice ?? g?.goodsPrice ?? g?.unitPrice + })); +}; + diff --git a/src/utils/payment.ts b/src/utils/payment.ts index 29b5b4f..de0e5d1 100644 --- a/src/utils/payment.ts +++ b/src/utils/payment.ts @@ -1,6 +1,10 @@ import Taro from '@tarojs/taro'; import { createOrder, WxPayResult } from '@/api/shop/shopOrder'; import { OrderCreateRequest } from '@/api/shop/shopOrder/model'; +import { getSelectedStoreFromStorage, getSelectedStoreIdFromStorage } from '@/utils/storeSelection'; +import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model'; +import type { ShopWarehouse } from '@/api/shop/shopWarehouse/model'; +import request from '@/utils/request'; /** * 支付类型枚举 @@ -24,6 +28,9 @@ export interface PaymentCallback { * 统一支付处理类 */ export class PaymentHandler { + // 简单缓存,避免频繁请求(小程序单次运行生命周期内有效) + private static storeRidersCache = new Map(); + private static warehousesCache: ShopWarehouse[] | null = null; /** * 执行支付 @@ -39,6 +46,36 @@ export class PaymentHandler { Taro.showLoading({ title: '支付中...' }); try { + // 若调用方未指定门店,则自动注入“已选门店”,用于订单门店归属/统计。 + if (orderData.storeId === undefined || orderData.storeId === null) { + const storeId = getSelectedStoreIdFromStorage(); + if (storeId) { + orderData.storeId = storeId; + } + } + if (!orderData.storeName) { + const store = getSelectedStoreFromStorage(); + if (store?.name) { + orderData.storeName = store.name; + } + } + + // 自动派单:按门店骑手优先级(dispatchPriority)选择 riderId(不覆盖手动指定) + if ((orderData.riderId === undefined || orderData.riderId === null) && orderData.storeId) { + const riderUserId = await this.pickRiderUserIdForStore(orderData.storeId); + if (riderUserId) { + orderData.riderId = riderUserId; + } + } + + // 仓库选择:若未指定 warehouseId,则按“离门店最近”兜底选择一个(不覆盖手动指定) + if ((orderData.warehouseId === undefined || orderData.warehouseId === null) && orderData.storeId) { + const warehouseId = await this.pickWarehouseIdForStore(orderData.storeId); + if (warehouseId) { + orderData.warehouseId = warehouseId; + } + } + // 设置支付类型 orderData.payType = paymentType; @@ -119,6 +156,126 @@ export class PaymentHandler { } } + private static parseLngLat(raw: string | undefined): { lng: number; lat: number } | null { + const text = (raw || '').trim(); + if (!text) return null; + const parts = text.split(/[,\s]+/).filter(Boolean); + if (parts.length < 2) return null; + const a = parseFloat(parts[0]); + const b = parseFloat(parts[1]); + if (Number.isNaN(a) || Number.isNaN(b)) return null; + const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90; + const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180; + if (looksLikeLngLat) return { lng: a, lat: b }; + if (looksLikeLatLng) return { lng: b, lat: a }; + return null; + } + + private static distanceMeters(a: { lng: number; lat: number }, b: { lng: number; lat: number }) { + const toRad = (x: number) => (x * Math.PI) / 180; + const R = 6371000; + const dLat = toRad(b.lat - a.lat); + const dLng = toRad(b.lng - a.lng); + const lat1 = toRad(a.lat); + const lat2 = toRad(b.lat); + const sin1 = Math.sin(dLat / 2); + const sin2 = Math.sin(dLng / 2); + const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2; + return 2 * R * Math.asin(Math.min(1, Math.sqrt(h))); + } + + private static async getRidersForStore(storeId: number): Promise { + const cached = this.storeRidersCache.get(storeId); + if (cached) return cached; + + // 后端字段可能叫 dealerId 或 storeId,这里都带上,服务端忽略未知字段即可。 + // 这里做一次路径兼容(camel vs kebab),避免接口路径不一致导致整单失败。 + const list = await this.listByCompatEndpoint( + ['/shop/shop-store-rider'], + { + storeId: storeId, + status: 1 + } + ); + + const usable = (list || []).filter(r => r?.isDelete !== 1 && (r.status === undefined || r.status === 1)); + this.storeRidersCache.set(storeId, usable); + return usable; + } + + private static async pickRiderUserIdForStore(storeId: number): Promise { + const riders = await this.getRidersForStore(storeId); + if (!riders.length) return undefined; + + // 优先:启用 + 在线 + 自动派单,再按 dispatchPriority 由高到低 + const score = (r: ShopStoreRider) => { + const enabled = (r.status === undefined || r.status === 1) ? 1 : 0; + const online = r.workStatus === 1 ? 1 : 0; + const auto = r.autoDispatchEnabled === 1 ? 1 : 0; + const p = typeof r.dispatchPriority === 'number' ? r.dispatchPriority : 0; + return enabled * 1000 + online * 100 + auto * 10 + p; + }; + + const sorted = [...riders].sort((a, b) => score(b) - score(a)); + return sorted[0]?.userId; + } + + private static async getWarehouses(): Promise { + if (this.warehousesCache) return this.warehousesCache; + const list = await this.listByCompatEndpoint( + ['/shop/shop-warehouse'], + {} + ); + const usable = (list || []).filter(w => w?.isDelete !== 1 && (w.status === undefined || w.status === 1)); + this.warehousesCache = usable; + return usable; + } + + private static async pickWarehouseIdForStore(storeId: number): Promise { + const store = getSelectedStoreFromStorage(); + if (!store?.id || store.id !== storeId) return undefined; + + // 一门店一默认仓库:优先使用门店自带的 warehouseId + if (store.warehouseId) return store.warehouseId; + + const storeCoords = this.parseLngLat(store.lngAndLat || store.location); + if (!storeCoords) return undefined; + + const warehouses = await this.getWarehouses(); + if (!warehouses.length) return undefined; + + // 优先选择“门店仓”,否则选最近的任意仓库 + const candidates = warehouses.filter(w => w.type?.includes('门店') || w.type?.includes('门店仓')); + const list = candidates.length ? candidates : warehouses; + + const withDistance = list + .map(w => { + const coords = this.parseLngLat(w.lngAndLat); + if (!coords) return { w, d: Number.POSITIVE_INFINITY }; + return { w, d: this.distanceMeters(storeCoords, coords) }; + }) + .sort((a, b) => a.d - b.d); + + return withDistance[0]?.w?.id; + } + + private static async listByCompatEndpoint( + urls: string[], + params: Record + ): Promise { + for (const url of urls) { + try { + const res: any = await (request as any).get(url, params, { showError: false }); + if (res?.code === 0 && Array.isArray(res?.data)) { + return res.data as T[]; + } + } catch (_e) { + // try next + } + } + return []; + } + /** * 处理微信支付 */ diff --git a/src/utils/request.ts b/src/utils/request.ts index 628a9d6..58b5ac9 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -53,29 +53,7 @@ const DEFAULT_CONFIG = { showError: true }; -// 获取API基础地址的函数 -const getBaseUrl = (): string => { - // 尝试从本地存储获取后台配置的API地址 - try { - const configStr = Taro.getStorageSync('config'); - if (configStr) { - // 如果是字符串,需要解析为对象 - const config = typeof configStr === 'string' ? JSON.parse(configStr) : configStr; - // console.log('获取后台配置API地址:', config); - // 注意属性名是 ApiUrl(首字母大写),不是 apiUrl - if (config && config.ApiUrl) { - // console.log('使用后台配置的API地址:', config.ApiUrl); - return config.ApiUrl; - } - } - } catch (error) { - console.warn('获取后台配置API地址失败:', error); - } - - // 如果后台没有配置API地址,则使用本地配置 - console.log('使用本地配置的API地址:', BaseUrl); - return BaseUrl; -}; +let baseUrl = BaseUrl; // 开发环境配置 if (process.env.NODE_ENV === 'development') { @@ -325,11 +303,8 @@ export async function request(options: RequestConfig): Promise { // 构建完整URL const buildUrl = (url: string): string => { - // 每次构建URL时都检查最新的baseUrl - const currentBaseUrl = getBaseUrl(); - if (url.indexOf('http') === -1) { - return currentBaseUrl + url; + return baseUrl + url; } return url; }; diff --git a/src/utils/storeSelection.ts b/src/utils/storeSelection.ts new file mode 100644 index 0000000..91438a7 --- /dev/null +++ b/src/utils/storeSelection.ts @@ -0,0 +1,27 @@ +import Taro from '@tarojs/taro'; +import type { ShopStore } from '@/api/shop/shopStore/model'; + +export const SELECTED_STORE_STORAGE_KEY = 'SelectedStore'; + +export function getSelectedStoreFromStorage(): ShopStore | null { + try { + const raw = Taro.getStorageSync(SELECTED_STORE_STORAGE_KEY); + if (!raw) return null; + return (typeof raw === 'string' ? JSON.parse(raw) : raw) as ShopStore; + } catch (_e) { + return null; + } +} + +export function saveSelectedStoreToStorage(store: ShopStore | null) { + if (!store) { + Taro.removeStorageSync(SELECTED_STORE_STORAGE_KEY); + return; + } + Taro.setStorageSync(SELECTED_STORE_STORAGE_KEY, store); +} + +export function getSelectedStoreIdFromStorage(): number | undefined { + return getSelectedStoreFromStorage()?.id; +} +