Compare commits

..

10 Commits

Author SHA1 Message Date
2681ccc94b 完善功能 2025-12-01 10:14:44 +08:00
d8011065d9 feat(config): update API base URLs for production and test environments
- Changed production API base URL from mp-api to clinic-api
- Updated test environment API base URL from mp-api to clinic-api
- Maintained existing APP_NAME and DEBUG configurations
2025-11-17 09:32:01 +08:00
e70eb5de69 refactor(clinicPrescription): 优化订单列表查询逻辑并移除冗余代码
- 移除页面中对 pageShopOrder 和 listShopOrderGoods 接口的引用
- 调整搜索条件,使用 doctorId 替代 userId 作为查询参数
- 删除未使用的 Taro 引入及状态栏高度相关逻辑
- 移除未生效的 props.searchParams 扩展属性
- 精简组件代码结构,提高可维护性
2025-11-15 14:01:48 +08:00
a1f9167a42 feat(clinic): 添加药方支付和订单状态字段
- 在药方模型中新增支付状态字段 (payStatus)
- 在药方模型中新增订单状态字段 (orderStatus)
- 更新相关接口以支持新字段的数据传输
2025-11-03 14:04:50 +08:00
4098f2a7e2 feat(clinic): 实现处方订单管理功能
- 新增处方订单列表展示组件
- 实现处方订单支付功能,调用统一支付接口
- 添加订单状态显示及待支付订单过滤逻辑
- 实现处方订单支付成功后状态更新
- 新增订单详情跳转和复制订单号功能
- 添加处方订单删除、编辑、详情操作
- 实现处方订单页面标题更新为"处方管理"
- 新增处方订单页面样式优化,包括搜索框、按钮等样式
- 实现订单列表组件,支持订单状态筛选和分页加载
- 添加订单支付倒计时和订单状态标签显示
- 实现订单取消、确认收货等操作功能
- 新增订单再次购买、申请退款等扩展功能
- 实现订单物流查看和评价商品功能- 添加订单退款进度查看和撤销申请功能
2025-11-03 14:02:54 +08:00
5749fab9e8 feat(payment): 初始化支付模块核心代码
- 添加支付常量类PaymentConstants,定义支付状态、微信、支付宝、银联等相关常量
- 创建微信支付类型常量类WechatPayType,支持JSAPI、NATIVE、H5、APP支付方式
- 新增支付控制器PaymentController,提供创建支付、查询状态、退款等统一接口
- 实现支付回调控制器PaymentNotifyController,处理微信、支付宝、银联异步通知
- 添加支付请求数据传输对象PaymentRequest,支持多种支付方式参数校验
- 定义支付响应、状态更新请求等相关DTO类- 集成Swagger注解,完善接口文档说明- 添加参数校验和异常处理机制,确保支付流程安全可靠
2025-11-03 12:31:47 +08:00
894b4bf7ce refactor(clinic): 优化处方支付逻辑并清理无用代码
- 移除处方列表中冗余的商品相关导入
- 简化支付处理函数,直接使用处方对象构建订单数据
- 删除首页菜单组件引用
- 清理角色页面中未使用的UI组件和导航逻辑
- 移除角色页面中多余的加载状态和错误处理属性
- 优化页面结构,提升代码可读性
2025-11-03 11:33:14 +08:00
32811faf54 feat(clinic): 添加患者管理和处方开立功能
- 新增患者管理页面,支持搜索、查看、编辑备注和删除患者
- 实现选择患者功能,用于处方开立时关联患者信息- 添加处方开立页面,支持诊断结果、治疗方案输入及图片上传- 新增用药订单页面,展示患者的历史处方订单并支持支付
- 在处方模型中增加医生姓名字段,优化处方数据显示- 扩展处方查询参数,支持按医生和用户ID筛选
2025-11-03 06:03:15 +08:00
a5efb6250f feat(clinic): 完善处方管理功能
- 处方模型增加患者基本信息字段(姓名、性别、年龄、身高、体重)- 处方列表页优化UI展示,增加处方类型、状态标签和药品数量显示
- 实现处方分页查询功能,替换原有列表查询接口
- 添加处方编辑和删除功能,支持跳转到编辑页面
- 处方列表项增加诊断结果、治疗方案、订单金额等信息展示
-优化空状态提示文案和新增按钮样式
- 页面标题文案调整,统一为“处方”相关表述
- 在医生端首页添加跳转到处方管理页面的入口
- 移除冗余的订单创建逻辑,专注处方核心功能
- 在应用配置中注册处方相关页面路由
2025-11-03 04:15:01 +08:00
ca3ff9dc9e refactor(clinic): 重构处方订单相关功能
- 删除废弃的处方订单模型定义文件
- 删除处方订单相关的API接口文件
- 更新处方API返回类型为ClinicPrescription
- 移除新增处方订单页面配置文件
- 移除新增处方订单页面组件文件
- 移除处方订单管理页面配置文件
- 移除处方订单管理页面组件文件
- 修改医生确认订单页面逻辑,分离处方与订单创建流程- 更新医生订单列表页面,使用处方数据替代经销商订单数据- 调整订单数据结构,关联处方ID和处方编号
-优化订单创建流程,先创建处方再创建订单
- 移除冗余的经销商订单相关导入和类型定义
2025-11-03 03:34:36 +08:00
62 changed files with 5447 additions and 850 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ yarn-error.log*
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
/java/

View File

@@ -2,19 +2,23 @@
export const ENV_CONFIG = { export const ENV_CONFIG = {
// 开发环境 // 开发环境
development: { development: {
API_BASE_URL: 'http://127.0.0.1:9200/api', API_BASE_URL: 'https://clinic-api.websoft.top/api',
// API_BASE_URL: 'http://127.0.0.1:9200/api',
WS_URL: 'ws://127.0.0.1:9200/api/chat',
APP_NAME: '开发环境', APP_NAME: '开发环境',
DEBUG: 'true', DEBUG: 'true',
}, },
// 生产环境 // 生产环境
production: { production: {
API_BASE_URL: 'https://mp-api.websoft.top/api', API_BASE_URL: 'https://clinic-api.websoft.top/api',
WS_URL: 'wss://clinic-api.websoft.top/api/chat',
APP_NAME: '通源堂健康生态平台', APP_NAME: '通源堂健康生态平台',
DEBUG: 'false', DEBUG: 'false',
}, },
// 测试环境 // 测试环境
test: { test: {
API_BASE_URL: 'https://mp-api.websoft.top/api', API_BASE_URL: 'https://clinic-api.websoft.top/api',
WS_URL: 'wss://clinic-api.websoft.top/api/chat',
APP_NAME: '测试环境', APP_NAME: '测试环境',
DEBUG: 'true', DEBUG: 'true',
} }
@@ -37,6 +41,7 @@ export function getEnvConfig() {
// 导出环境变量 // 导出环境变量
export const { export const {
API_BASE_URL, API_BASE_URL,
WS_URL,
APP_NAME, APP_NAME,
DEBUG DEBUG
} = getEnvConfig() } = getEnvConfig()

View File

@@ -99,3 +99,13 @@ export async function getClinicDoctorUser(id: number) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
export async function getClinicDoctorUserByUserId(id: number) {
const res = await request.get<ApiResult<ClinicDoctorUser>>(
'/clinic/clinic-doctor-user/getByUserId/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,101 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { ClinicOrder, ClinicOrderParam } from './model';
/**
* 分页查询处方订单
*/
export async function pageClinicOrder(params: ClinicOrderParam) {
const res = await request.get<ApiResult<PageResult<ClinicOrder>>>(
'/clinic/clinic-order/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询处方订单列表
*/
export async function listClinicOrder(params?: ClinicOrderParam) {
const res = await request.get<ApiResult<ClinicOrder[]>>(
'/clinic/clinic-order',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加处方订单
*/
export async function addClinicOrder(data: ClinicOrder) {
const res = await request.post<ApiResult<unknown>>(
'/clinic/clinic-order',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改处方订单
*/
export async function updateClinicOrder(data: ClinicOrder) {
const res = await request.put<ApiResult<unknown>>(
'/clinic/clinic-order',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除处方订单
*/
export async function removeClinicOrder(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/clinic/clinic-order/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除处方订单
*/
export async function removeBatchClinicOrder(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/clinic/clinic-order/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询处方订单
*/
export async function getClinicOrder(id: number) {
const res = await request.get<ApiResult<ClinicOrder>>(
'/clinic/clinic-order/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,167 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 处方订单
*/
export interface ClinicOrder {
// 订单号
orderId?: number;
// 订单编号
orderNo?: string;
// 订单类型0商城订单 1预定订单/外卖 2会员卡
type?: number;
// 订单标题
title?: string;
// 快递/自提
deliveryType?: number;
// 下单渠道0小程序预定 1俱乐部训练场 3活动订场
channel?: number;
// 微信支付交易号号
transactionId?: string;
// 微信退款订单号
refundOrder?: string;
// 商户ID
merchantId?: number;
// 商户名称
merchantName?: string;
// 商户编号
merchantCode?: string;
// 使用的优惠券id
couponId?: number;
// 使用的会员卡id
cardId?: string;
// 关联管理员id
adminId?: number;
// 核销管理员id
confirmId?: number;
// IC卡号
icCard?: string;
// 真实姓名
realName?: string;
// 关联收货地址
addressId?: number;
// 收货地址
address?: string;
//
addressLat?: string;
//
addressLng?: string;
// 买家留言
buyerRemarks?: string;
// 自提店铺id
selfTakeMerchantId?: number;
// 自提店铺
selfTakeMerchantName?: string;
// 配送开始时间
sendStartTime?: string;
// 配送结束时间
sendEndTime?: string;
// 发货店铺id
expressMerchantId?: number;
// 发货店铺
expressMerchantName?: string;
// 订单总额
totalPrice?: string;
// 减少的金额使用VIP会员折扣、优惠券抵扣、优惠券折扣后减去的价格
reducePrice?: string;
// 实际付款
payPrice?: string;
// 用于统计
price?: string;
// 价钱,用于积分赠送
money?: string;
// 取消时间
cancelTime?: string;
// 取消原因
cancelReason?: string;
// 退款金额
refundMoney?: string;
// 教练价格
coachPrice?: string;
// 购买数量
totalNum?: number;
// 教练id
coachId?: number;
// 商品ID
formId?: number;
// 支付的用户id
payUserId?: number;
// 0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付
payType?: number;
// 微信支付子类型JSAPI小程序支付NATIVE扫码支付
wechatPayType?: string;
// 0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付
friendPayType?: number;
// 0未付款1已付款
payStatus?: string;
// 0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款
orderStatus?: number;
// 发货状态(10未发货 20已发货 30部分发货)
deliveryStatus?: number;
// 无需发货备注
deliveryNote?: string;
// 发货时间
deliveryTime?: string;
// 评价状态(0未评价 1已评价)
evaluateStatus?: number;
// 评价时间
evaluateTime?: string;
// 优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
couponType?: number;
// 优惠说明
couponDesc?: string;
// 二维码地址,保存订单号,支付成功后才生成
qrcode?: string;
// vip月卡年卡、ic月卡年卡回退次数
returnNum?: number;
// vip充值回退金额
returnMoney?: string;
// 预约详情开始时间数组
startTime?: string;
// 是否已开具发票0未开发票1已开发票2不能开具发票
isInvoice?: string;
// 发票流水号
invoiceNo?: string;
// 商家留言
merchantRemarks?: string;
// 支付时间
payTime?: string;
// 退款时间
refundTime?: string;
// 申请退款时间
refundApplyTime?: string;
// 过期时间
expirationTime?: string;
// 自提码
selfTakeCode?: string;
// 是否已收到赠品
hasTakeGift?: string;
// 对账情况0=未对账1=已对账3=已对账金额对不上4=未查询到该订单
checkBill?: number;
// 订单是否已结算(0未结算 1已结算)
isSettled?: number;
// 系统版本号 0当前版本 value=其他版本
version?: number;
// 用户id
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 修改时间
updateTime?: string;
// 创建时间
createTime?: string;
}
/**
* 处方订单搜索条件
*/
export interface ClinicOrderParam extends PageParam {
orderId?: number;
keywords?: string;
}

View File

@@ -16,6 +16,20 @@ export async function pageClinicPatientUser(params: ClinicPatientUserParam) {
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
/**
* 分页查询患者
*/
export async function userPageClinicPatientUser(params: ClinicPatientUserParam) {
const res = await request.get<ApiResult<PageResult<ClinicPatientUser>>>(
'/clinic/clinic-patient-user/userPage',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/** /**
* 查询患者列表 * 查询患者列表
*/ */
@@ -99,3 +113,13 @@ export async function getClinicPatientUser(id: number) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
export async function clinicPatientUserByPatientUserId(id: number) {
const res = await request.get<ApiResult<ClinicPatientUser>>(
'/clinic/clinic-patient-user/getByPatientUserId/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -10,6 +10,7 @@ export interface ClinicPatientUser {
type?: number; type?: number;
// 自增ID // 自增ID
userId?: number; userId?: number;
patientUserId?: number;
// 姓名 // 姓名
realName?: string; realName?: string;
// 头像 // 头像

View File

@@ -37,12 +37,12 @@ export async function listClinicPrescription(params?: ClinicPrescriptionParam) {
*/ */
export async function addClinicPrescription(data: ClinicPrescription) { export async function addClinicPrescription(data: ClinicPrescription) {
const res = await request.post<ApiResult<unknown>>( const res = await request.post<ApiResult<ClinicPrescription>>(
'/clinic/clinic-prescription', '/clinic/clinic-prescription',
data data
); );
if (res.code === 0) { if (res.code === 0) {
return res.message; return res.data; // 返回处方数据包含处方ID
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
@@ -106,3 +106,40 @@ export async function getClinicPrescription(id: number) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
/**
* 微信支付返回数据
*/
export interface WxPayResult {
prepayId: string;
orderNo: string;
timeStamp: string;
nonceStr: string;
package: string;
signType: string;
paySign: string;
}
/**
* 处方订单创建请求
*/
export interface PrescriptionOrderRequest {
// 处方ID
prescriptionId: number;
// 支付方式 0余额支付, 1微信支付3支付宝
payType: number;
}
/**
* 创建处方订单并支付
*/
export async function createPrescriptionOrder(data: PrescriptionOrderRequest) {
const res = await request.post<ApiResult<WxPayResult>>(
'/clinic/clinic-prescription/order',
data
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,5 +1,6 @@
import type { PageParam } from '@/api/index'; import type { PageParam } from '@/api/index';
import {ClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem/model"; import {ClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem/model";
import {ShopOrder} from "@/api/shop/shopOrder/model";
/** /**
* 处方主表 * 处方主表
@@ -10,8 +11,20 @@ export interface ClinicPrescription {
id?: number; id?: number;
// 患者 // 患者
userId?: number; userId?: number;
// 姓名
realName?: string;
// 性别 0男 1女
sex?: number;
// 年龄
age?: string;
// 身高
height?: string;
// 体重
weight?: string;
// 医生 // 医生
doctorId?: number; doctorId?: number;
// 姓名
doctorName?: string;
// 订单编号 // 订单编号
orderNo?: string; orderNo?: string;
// 关联就诊表 // 关联就诊表
@@ -50,6 +63,11 @@ export interface ClinicPrescription {
updateTime?: string; updateTime?: string;
// 药方信息 // 药方信息
items?: ClinicPrescriptionItem[]; items?: ClinicPrescriptionItem[];
// 支付状态
payStatus?: boolean;
// 订单状态
orderStatus?: number;
shopOrder?: ShopOrder
} }
/** /**
@@ -58,5 +76,8 @@ export interface ClinicPrescription {
*/ */
export interface ClinicPrescriptionParam extends PageParam { export interface ClinicPrescriptionParam extends PageParam {
id?: number; id?: number;
doctorId?: number;
userId?: number;
keywords?: string; keywords?: string;
withDoctor?: boolean;
} }

View File

@@ -46,6 +46,16 @@ export async function addClinicPrescriptionItem(data: ClinicPrescriptionItem) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
export async function batchAddClinicPrescriptionItem(data: ClinicPrescriptionItem[]) {
const res = await request.post<ApiResult<unknown>>(
'/clinic/clinic-prescription-item/batch',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/** /**
* 修改处方明细表 * 修改处方明细表

13
src/api/payment/index.ts Normal file
View File

@@ -0,0 +1,13 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
/**
* 统一支付
*/
export async function pay(data: any) {
const res = await request.post<ApiResult<any>>('/payment/create-with-order',data);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,29 @@
/**
* 首页布局样式
*/
export interface Layout {
// 内容区域的宽度
width?: string;
// 文字颜色
color?: string;
// 高亮颜色
hover?: string;
// 背景颜色
backgroundColor?: string;
headerStyle?: any;
siteNameStyle?: any;
showBanner?: boolean;
// 背景图片
banner?: string;
}
/**
* 修改密码参数
*/
export interface UpdatePasswordParam {
// 新密码
password: string;
// 原始密码
oldPassword: string;
}

View File

@@ -34,12 +34,12 @@ export async function listShopChatConversation(params?: ShopChatConversationPara
* 添加聊天会话表 * 添加聊天会话表
*/ */
export async function addShopChatConversation(data: ShopChatConversation) { export async function addShopChatConversation(data: ShopChatConversation) {
const res = await request.post<ApiResult<unknown>>( const res = await request.post<ApiResult<ShopChatConversation>>(
'/shop/shop-chat-conversation', '/shop/shop-chat-conversation',
data data
); );
if (res.code === 0) { if (res.code === 0) {
return res.message; return res.data;
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
@@ -99,3 +99,13 @@ export async function getShopChatConversation(id: number) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
export async function chatConversationByBothUserId(userId: string) {
const res = await request.get<ApiResult<ShopChatConversation>>(
'/shop/shop-chat-conversation/getByBothUserId/' + userId
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -30,6 +30,8 @@ export interface ShopChatMessage {
type?: string; type?: string;
// 消息内容 // 消息内容
content?: string; content?: string;
// 会话ID
conversationId?: number;
// 屏蔽接收方 // 屏蔽接收方
sideTo?: number; sideTo?: number;
// 屏蔽发送方 // 屏蔽发送方
@@ -52,6 +54,8 @@ export interface ShopChatMessage {
createTime?: string; createTime?: string;
// 修改时间 // 修改时间
updateTime?: string; updateTime?: string;
// 是否本人消息
isMine?: boolean;
} }
/** /**
@@ -59,5 +63,6 @@ export interface ShopChatMessage {
*/ */
export interface ShopChatMessageParam extends PageParam { export interface ShopChatMessageParam extends PageParam {
id?: number; id?: number;
conversationId?: number;
keywords?: string; keywords?: string;
} }

View File

@@ -53,7 +53,7 @@ export async function updateShopOrder(data: ShopOrder) {
data data
); );
if (res.code === 0) { if (res.code === 0) {
return res.message; return res;
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }

View File

@@ -147,14 +147,15 @@ export interface ShopOrder {
hasTakeGift?: string; hasTakeGift?: string;
// 订单商品项 // 订单商品项
orderGoods?: OrderGoods[]; orderGoods?: OrderGoods[];
makePay?: boolean
} }
/** /**
* 订单商品项 * 订单商品项
*/ */
export interface OrderGoodsItem { export interface OrderGoodsItem {
goodsId: number; goodsId?: number;
quantity: number; quantity?: number;
skuId?: number; skuId?: number;
specInfo?: string; specInfo?: string;
} }
@@ -164,7 +165,7 @@ export interface OrderGoodsItem {
*/ */
export interface OrderCreateRequest { export interface OrderCreateRequest {
// 商品信息列表 // 商品信息列表
goodsItems: OrderGoodsItem[]; goodsItems?: OrderGoodsItem[];
// 收货地址ID // 收货地址ID
addressId?: number; addressId?: number;
// 支付方式 // 支付方式
@@ -185,8 +186,8 @@ export interface OrderCreateRequest {
* 订单商品项 * 订单商品项
*/ */
export interface OrderGoodsItem { export interface OrderGoodsItem {
goodsId: number; goodsId?: number;
quantity: number; quantity?: number;
skuId?: number; skuId?: number;
specInfo?: string; specInfo?: string;
} }
@@ -195,8 +196,10 @@ export interface OrderGoodsItem {
* 创建订单请求 * 创建订单请求
*/ */
export interface OrderCreateRequest { export interface OrderCreateRequest {
// 订单编号
orderNo?: string;
// 商品信息列表 // 商品信息列表
goodsItems: OrderGoodsItem[]; goodsItems?: OrderGoodsItem[];
// 收货地址ID // 收货地址ID
addressId?: number; addressId?: number;
// 支付方式 // 支付方式
@@ -209,6 +212,8 @@ export interface OrderCreateRequest {
deliveryType?: number; deliveryType?: number;
// 自提店铺ID // 自提店铺ID
selfTakeMerchantId?: number; selfTakeMerchantId?: number;
// 订单类型
type?: number;
} }
/** /**

View File

@@ -44,6 +44,17 @@ export async function addShopOrderGoods(data: ShopOrderGoods) {
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
export async function batchAddShopOrderGoods(data: ShopOrderGoods[]) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-order-goods/batch',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/** /**
* 修改商品信息 * 修改商品信息
*/ */

View File

@@ -82,6 +82,7 @@ export interface Order {
updateTime?: string; updateTime?: string;
// 创建时间 // 创建时间
createTime?: string; createTime?: string;
makePay?: boolean;
} }
/** /**

View File

@@ -104,8 +104,18 @@ export default {
"root": "clinic", "root": "clinic",
"pages": [ "pages": [
"index", "index",
"clinicPatientUser/index",
"clinicPatientUser/add", "clinicPatientUser/add",
"clinicDoctorUser/add" "clinicPatientUser/selectPatient",
"clinicPatientUser/prescription",
"clinicPatientUser/detail",
"clinicDoctorUser/index",
"clinicDoctorUser/add",
"clinicPrescription/index",
"clinicPrescription/add",
"clinicPrescription/selectPrescription",
"clinicPrescription/confirm",
"clinicPrescription/detail"
] ]
}, },
{ {

View File

@@ -1,158 +1,284 @@
import {useState, useEffect} from 'react' import {useState, useEffect, useRef} from 'react'
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro' import Taro, {useDidShow, useRouter, useLoad} from '@tarojs/taro'
import {useRouter} from '@tarojs/taro' import {Input, Button} from '@nutui/nutui-react-taro'
import { import {Voice, FaceMild} from '@nutui/icons-react-taro'
Loading, import {getClinicDoctorUserByUserId} from "@/api/clinic/clinicDoctorUser";
InfiniteLoading, import {addShopChatMessage, listShopChatMessage} from "@/api/shop/shopChatMessage";
Empty,
Space,
Input,
Avatar,
Tag,
Divider,
Button
} from '@nutui/nutui-react-taro'
import { Voice, FaceMild, AddCircle } from '@nutui/icons-react-taro'
import {getClinicDoctorUser} from "@/api/clinic/clinicDoctorUser";
import {ClinicDoctorUser} from "@/api/clinic/clinicDoctorUser/model";
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
import navTo from "@/utils/common";
import {pageShopChatMessage} from "@/api/shop/shopChatMessage";
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model"; import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
import Line from "@/components/Gap"; import {addShopChatConversation, chatConversationByBothUserId} from "@/api/shop/shopChatConversation";
// @ts-ignore
import {WS_URL} from "@/config/env";
import {clinicPatientUserByPatientUserId} from "@/api/clinic/clinicPatientUser";
const CustomerIndex = () => { const CustomerIndex = () => {
const {params} = useRouter(); const {params} = useRouter()
const [doctor, setDoctor] = useState<ClinicDoctorUser>() const [messages, setMessages] = useState<ShopChatMessage[]>([])
const [list, setList] = useState<ShopChatMessage[]>([]) const [conversationId, setConversationId] = useState<number | null>(null)
const [friendUserId, setFriendUserId] = useState<string>('')
const [messageInput, setMessageInput] = useState<string>('')
const [sending, setSending] = useState<boolean>(false)
const [isDoctor, setIsDoctor] = useState<boolean>(false) const [isDoctor, setIsDoctor] = useState<boolean>(false)
const [doctors, setDoctors] = useState<ClinicDoctorUser[]>([]) const socketRef = useRef<Taro.SocketTask | null>(null)
const [patientUsers, setPatientUsers] = useState<ClinicPatientUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 获取列表数据 const quickActions = [
const fetchData = async () => { {label: '开方', type: 'prescription'},
setLoading(true); {label: '快捷回复', type: 'quickReply'},
if (Taro.getStorageSync('Doctor')) { {label: '发问诊单', type: 'sendInquiry'}
setIsDoctor(true) ]
}
const doctorUser = await getClinicDoctorUser(Number(params.id))
if (doctorUser) {
setDoctor(doctorUser)
Taro.setNavigationBarTitle({title: `${doctorUser.realName}`});
}
const messages = await pageShopChatMessage({})
const handleQuickAction = (actionType: string) => {
switch (actionType) {
case 'prescription':
if (friendUserId) {
Taro.navigateTo({url: `/doctor/orders/add?id=${friendUserId}`})
} else {
Taro.showToast({title: '请选择患者', icon: 'none'})
}
break
case 'quickReply':
Taro.showToast({title: '快捷回复敬请期待', icon: 'none'})
break
case 'sendInquiry':
Taro.showToast({title: '问诊单功能敬请期待', icon: 'none'})
break
}
} }
// 初始化数据 const fetchData = async (userId?: string) => {
if (!userId) return
try {
console.log("Taro.getStorageSync('Doctor')", Taro.getStorageSync('Doctor'))
if (Taro.getStorageSync('Doctor')) {
setIsDoctor(true)
}
const isDoctorData = Taro.getStorageSync('Doctor')
if (!isDoctorData) {
const doctorUser = await getClinicDoctorUserByUserId(Number(params.userId))
if (doctorUser?.realName) {
await Taro.setNavigationBarTitle({title: `${doctorUser.realName}`})
}
} else {
const patient = await clinicPatientUserByPatientUserId(Number(params.userId))
if (patient?.realName) {
await Taro.setNavigationBarTitle({title: `${patient.realName}`})
}
}
let conversation = await chatConversationByBothUserId(userId)
if (!conversation) {
conversation = await addShopChatConversation({
friendId: parseInt(userId, 10),
content: ''
})
}
if (conversation?.id) {
setConversationId(conversation.id)
const messageList = await listShopChatMessage({conversationId: conversation.id})
setMessages(messageList || [])
} else {
setMessages([])
}
} catch (error) {
console.error('加载聊天数据失败:', error)
Taro.showToast({title: '聊天加载失败', icon: 'none'})
}
}
const connectWebSocket = async (id: number) => {
const base = (WS_URL || '').replace(/\/$/, '')
if (!base) {
console.warn('WS_URL 未配置')
return
}
if (socketRef.current) {
socketRef.current.close({})
}
const userId = Taro.getStorageSync('UserId')
const result = Taro.connectSocket({
url: `${base}/${id}_${userId}`
})
const socketTask = typeof (result as Promise<any>)?.then === 'function'
? await (result as Promise<Taro.SocketTask>)
: (result as Taro.SocketTask)
if (!socketTask) {
return
}
socketRef.current = socketTask
socketTask.onOpen(() => {
console.log('WebSocket连接成功')
})
socketTask.onMessage((event) => {
console.log('收到消息:', event,)
try {
if (event.data === '连接成功' || event.data === 'pong') return
const payload = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
if (!payload) return
if (Array.isArray(payload)) {
setMessages(prev => [...prev, ...payload])
} else {
payload.isMine = parseInt(payload.fromUserId) !== parseInt(params?.userId)
setMessages(prev => [...prev, payload])
}
} catch (err) {
console.error('解析消息失败:', err)
}
})
socketTask.onError((err) => {
console.error('WebSocket异常:', err)
})
socketTask.onClose(() => {
socketRef.current = null
})
}
const handleSendMessage = async () => {
const content = messageInput.trim()
if (!content) {
Taro.showToast({title: '请输入内容', icon: 'none'})
return
}
if (!conversationId || !friendUserId) {
Taro.showToast({title: '会话未初始化', icon: 'none'})
return
}
if (sending) {
return
}
try {
setSending(true)
await addShopChatMessage({
content,
conversationId,
toUserId: parseInt(friendUserId, 10),
type: 'text'
})
// const localMessage: ShopChatMessage = {
// id: Date.now(),
// content,
// conversationId,
// toUserId: parseInt(friendUserId, 10),
// type: 'text',
// isMine: true
// }
//
// setMessages(prev => [...prev, localMessage])
setMessageInput('')
} catch (error) {
console.error('发送消息失败:', error)
Taro.showToast({title: '发送失败,请重试', icon: 'none'})
} finally {
setSending(false)
}
}
useLoad((options) => {
if (options?.userId) {
const userId = String(options.userId)
setFriendUserId(userId)
fetchData(userId)
}
console.log('Taro.getStorageSync(\'UserId\')', Taro.getStorageSync('UserId'))
})
useEffect(() => { useEffect(() => {
fetchData().then() if (conversationId) {
}, []); connectWebSocket(conversationId).catch(err => {
console.error('WebSocket连接失败:', err)
// 监听页面显示,当从其他页面返回时刷新数据 })
useDidShow(() => { }
// 刷新当前tab的数据和统计信息 return () => {
fetchData().then(); socketRef.current?.close()
}); socketRef.current = null
}
// 渲染医师项 }, [conversationId])
const renderDoctorItem = (item: ClinicDoctorUser) => (
<View key={item.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center">
<View className="flex-1 flex justify-between items-center">
<View className="flex justify-between">
<Avatar src={item.avatar} size={'large'}/>
<View className={'flex flex-col mx-3'}>
<Text className="font-semibold text-gray-800 mr-2">
{item.realName}
</Text>
<View>
<Tag background="#f3f3f3" color="#999999"></Tag>
</View>
<View className={'my-1'}>
<Text className={'text-gray-400 text-xs'}> 1 </Text>
<Divider direction="vertical"/>
<Text className={'text-gray-400 text-xs'}> 3 </Text>
</View>
</View>
</View>
<Button type="warning"></Button>
</View>
</View>
</View>
);
// 渲染患者项
const renderPatientUserItem = (item: ClinicPatientUser) => (
<View key={item.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center">
<View className="flex-1 flex justify-between items-center">
<View className="flex justify-between">
<Avatar src={item.avatar} size={'large'}/>
<View className={'flex flex-col mx-3'}>
<Text className="font-semibold text-gray-800 mr-2">
{item.realName}
</Text>
<View>
{
<Text
className={'text-gray-400 text-xs'}>{item.sex}</Text>
}
{
item.age && (
<>
<Divider direction="vertical"/>
<Text className={'text-gray-400 text-xs'}>{item.age}</Text>
</>
)
}
{
item.weight && (
<>
<Divider direction="vertical"/>
<Text className={'text-gray-400 text-xs'}>{item.weight}</Text>
</>
)
}
</View>
<View>
<Text className={'text-gray-400 text-xs'}>{item.allergyHistory}</Text>
</View>
</View>
</View>
<Button type="warning" onClick={() => navTo(`/doctor/orders/add?id=${item.userId}`)}></Button>
</View>
</View>
</View>
);
return ( return (
<View className="min-h-screen bg-gray-50 w-full"> <View className="min-h-screen bg-gray-50 w-full pb-24">
<View className={'p-4'}> <View className="px-4 pt-4 pb-24">
{list?.map(renderPatientUserItem)} {messages.length === 0 ? (
<View className="mt-10 text-center text-gray-400 text-sm">
<Text></Text>
</View>
) : (
messages.map((item, index) => (
<View
key={item.id || `msg-${index}`}
className={`flex mb-4 ${item.isMine ? 'justify-end' : 'justify-start'}`}
>
<View
className={`px-3 py-2 rounded-2xl text-sm leading-relaxed break-words ${item.isMine ? 'bg-amber-400 text-white' : 'bg-white text-gray-800'}`}
style={{maxWidth: '75%'}}
>
<Text>{item.content}</Text>
</View>
</View>
))
)}
</View> </View>
<View className={'fixed bottom-0 w-full bg-orange-50 pt-2 pb-8'}>
<View className={'flex flex-1 items-center justify-between'}> <View className="fixed bottom-0 left-0 right-0 bg-orange-50 pt-2 pb-8 px-3 border-t border-orange-100">
<Voice className={'mx-2'} /> <View className="flex gap-3 mb-2 overflow-x-auto">
<Input className={'w-full'} style={{ {quickActions.map(action => (
borderRadius: '6px', <View
paddingLeft: '12px', key={action.type}
paddingRight: '12px' onClick={() => handleQuickAction(action.type)}
}} /> style={{
<FaceMild size={26} className={'ml-2'} /> padding: '6px 12px',
<AddCircle size={26} className={'mx-2'} /> borderRadius: '20px',
backgroundColor: '#fff',
fontSize: '14px',
color: '#8b5a2b',
boxShadow: '0 4px 10px rgba(0,0,0,0.05)',
display: 'flex',
alignItems: 'center',
flexShrink: 0
}}
>
<Text>{action.label}</Text>
</View>
))}
</View>
<View className="flex flex-1 items-center justify-between">
<Voice className="mx-2"/>
<Input
className="w-full"
style={{
borderRadius: '6px',
paddingLeft: '12px',
paddingRight: '12px'
}}
value={messageInput}
onChange={(value) => setMessageInput(value)}
confirmType="send"
onConfirm={handleSendMessage}
/>
<FaceMild size={26} className="ml-2"/>
<Button
size="small"
type="primary"
className="ml-2"
loading={sending}
onClick={handleSendMessage}
>
</Button>
</View> </View>
</View> </View>
</View> </View>
); )
}; }
export default CustomerIndex; export default CustomerIndex;

View File

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

View File

@@ -0,0 +1,363 @@
import {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, Button, SearchBar} from '@nutui/nutui-react-taro'
import {Phone} from '@nutui/icons-react-taro'
import type {ClinicPatientUser as PatientUserType} from "@/api/clinic/clinicPatientUser/model";
import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common";
import {
pageClinicPatientUser,
removeClinicPatientUser,
updateClinicPatientUser
} from "@/api/clinic/clinicPatientUser";
// 扩展患者类型
interface PatientUser extends PatientUserType {
}
const PatientIndex = () => {
const [list, setList] = useState<PatientUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 复制手机号
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 = (patient: PatientUser) => {
Taro.showModal({
title: '编辑备注',
// @ts-ignore
editable: true,
placeholderText: '请输入备注信息',
content: patient.comments || '',
success: async (res) => {
// @ts-ignore
if (res.confirm && res.content !== undefined) {
try {
// 更新备注
await updateClinicPatientUser({
...patient,
// @ts-ignore
comments: res.content.trim()
});
Taro.showToast({
title: '更新成功',
icon: 'success'
});
// 刷新列表
setList([]);
setPage(1);
setHasMore(true);
fetchPatientData(true);
} catch (error) {
console.error('更新备注失败:', error);
Taro.showToast({
title: '更新失败,请重试',
icon: 'error'
});
}
}
}
});
};
// 获取患者数据
const fetchPatientData = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数
const params: any = {
page: currentPage
};
// 添加搜索关键词
if (displaySearchValue.trim()) {
params.keywords = displaySearchValue.trim();
}
const res = await pageClinicPatientUser(params);
if (res?.list && res.list.length > 0) {
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(res.list);
} else {
setList(prevList => prevList.concat(res.list));
}
// 正确判断是否还有更多数据
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, displaySearchValue]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchPatientData(false, nextPage);
}
// 防抖搜索功能
useEffect(() => {
const timer = setTimeout(() => {
setDisplaySearchValue(searchValue);
}, 300); // 300ms 防抖
return () => clearTimeout(timer);
}, [searchValue]);
// 删除患者
const handleDelete = (patient: PatientUser) => {
removeClinicPatientUser(patient.id).then(() => {
Taro.showToast({
title: '删除成功',
icon: 'success'
});
// 刷新数据
setList([]);
setPage(1);
setHasMore(true);
fetchPatientData(true).then();
})
}
// 初始化数据
useEffect(() => {
fetchPatientData(true).then();
}, [displaySearchValue]);
// 监听页面显示,当从其他页面返回时刷新数据
useDidShow(() => {
// 刷新数据
setList([]);
setPage(1);
setHasMore(true);
fetchPatientData(true);
});
// 渲染患者项
const renderPatientItem = (patient: PatientUser) => (
<View key={patient.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center mb-3">
<View className="flex-1">
<View className="flex items-center mb-1">
<Space>
<Text className="font-semibold text-gray-800 mr-2">
{patient.realName || '未命名'}
</Text>
<Text className={'text-gray-400 font-normal'}>{patient.age}</Text>
<Text className={'text-gray-400 font-normal'}>{patient.sex == '1' ? '男' : ''}{patient.sex == '2' ? '女' : ''}</Text>
</Space>
</View>
<View className="flex items-center mb-1">
<Space direction="vertical">
<View className="flex items-center">
<Text className="text-xs text-gray-500" onClick={(e) => {
e.stopPropagation();
makePhoneCall(patient.phone || '');
}}>{patient.phone || '未提供'}</Text>
<View className="flex items-center ml-2">
<Phone
size={12}
className="text-green-500 mr-2"
onClick={(e) => {
e.stopPropagation();
makePhoneCall(patient.phone || '');
}}
/>
<Text
className="text-xs text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
copyPhone(patient.phone || '');
}}
>
</Text>
</View>
</View>
<Text className="text-xs text-gray-500">
{patient.createTime || '未知'}
</Text>
</Space>
</View>
{/* 显示 comments 字段 */}
<Space className="flex items-center">
<Text className="text-xs text-gray-500">{patient.comments || '暂无'}</Text>
<Text
className="text-xs text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
editComments(patient);
}}
>
</Text>
</Space>
</View>
</View>
{/* 操作按钮 */}
<Space className="flex justify-end">
<Button
size="small"
onClick={() => navTo(`/doctor/orders/add?id=${patient.userId}`, true)}
style={{marginRight: '8px', backgroundColor: '#ff4d4f', color: 'white'}}
>
</Button>
<Button
size="small"
onClick={() => navTo(`/doctor/customer/add?id=${patient.id}`, true)}
style={{marginRight: '8px', backgroundColor: '#1890ff', color: 'white'}}
>
</Button>
<Button
size="small"
onClick={() => handleDelete(patient)}
style={{backgroundColor: '#ff4d4f', color: 'white'}}
>
</Button>
</Space>
</View>
);
// 渲染患者列表
const renderPatientList = () => {
const isSearching = displaySearchValue.trim().length > 0;
return (
<View className="flex-1">
{/* 搜索结果统计 */}
{isSearching && (
<View className="bg-white px-4 py-2 border-b border-gray-100">
<Text className="text-sm text-gray-600">
"{displaySearchValue}" {list.length}
</Text>
</View>
)}
<View className="p-4" style={{
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
// 滚动事件处理
}}
onScrollToUpper={() => {
// 滚动到顶部事件处理
}}
loadingText={
<>
...
</>
}
loadMoreText={
list.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无患者数据"}
/>
) : (
<View className={'h-3 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && list.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
list.map(renderPatientItem)
)}
</InfiniteLoading>
</View>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 搜索栏 */}
<View className="bg-white py-2 border-b border-gray-100">
<SearchBar
value={searchValue}
placeholder="搜索患者姓名、手机号"
onChange={(value) => setSearchValue(value)}
onClear={() => {
setSearchValue('');
setDisplaySearchValue('');
}}
clearable
/>
</View>
{/* 患者列表 */}
{renderPatientList()}
<FixedButton text={'添加患者'} onClick={() => Taro.navigateTo({url: '/doctor/customer/add'})}/>
</View>
);
};
export default PatientIndex;

View File

@@ -1,98 +0,0 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {ClinicOrder} from "@/api/clinic/clinicOrder/model";
import {getClinicOrder, listClinicOrder, updateClinicOrder, addClinicOrder} from "@/api/clinic/clinicOrder";
const AddClinicOrder = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<ClinicOrder>({})
const formRef = useRef<any>(null)
const reload = async () => {
if (params.id) {
const data = await getClinicOrder(Number(params.id))
setFormData(data)
} else {
setFormData({})
}
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
if (params.id) {
// 编辑模式
await updateClinicOrder({
...values,
id: Number(params.id)
})
} else {
// 新增模式
await addClinicOrder(values)
}
Taro.showToast({
title: `操作成功`,
icon: 'success'
})
setTimeout(() => {
return Taro.navigateBack()
}, 1000)
} catch (error) {
Taro.showToast({
title: `操作失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, []);
if (loading) {
return <Loading className={'px-2'}></Loading>
}
return (
<>
<Form
ref={formRef}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button
nativeType="submit"
type="success"
size="large"
className={'w-full'}
block
>
{params.id ? '更新' : '保存'}
</Button>
</div>
}
>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="orderId" label="订单号" initialValue={FormData.orderId} required>

View File

@@ -1,64 +0,0 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ClinicOrder} from "@/api/clinic/clinicOrder/model";
import {listClinicOrder, removeClinicOrder, updateClinicOrder} from "@/api/clinic/clinicOrder";
const ClinicOrderList = () => {
const [list, setList] = useState<ClinicOrder[]>([])
const reload = () => {
listClinicOrder({
// 添加查询条件
})
.then(data => {
setList(data || [])
})
.catch(() => {
Taro.showToast({
title: '获取数据失败',
icon: 'error'
});
})
}
const onDel = async (id?: number) => {
await removeClinicOrder(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
}
useDidShow(() => {
reload()
});
if (list.length == 0) {
return (
<ConfigProvider>
<div className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="暂无数据"
/>
<Space>
<Button onClick={() => Taro.navigateTo({url: '/clinic/clinicOrder/add'})}></Button>
</Space>
</div>
</ConfigProvider>
)
}
return (
<>
{list.map((item, _) => (
<Cell.Group key={item.

View File

@@ -1,4 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '处方订单管理', navigationBarTitleText: '开方详情',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

View File

@@ -0,0 +1,72 @@
.prescription-detail-page {
min-height: 100vh;
background: #f7f8fa;
padding: 12px;
box-sizing: border-box;
font-size: 25rpx;
line-height: 1.6;
}
.detail-header-card {
background: #ffffff;
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
}
.detail-header-card__time {
font-weight: 600;
color: #1f2c3d;
font-size: 25rpx;
}
.detail-card {
background: #ffffff;
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
font-size: 25rpx;
}
.detail-action-row {
margin-top: 12px;
display: flex;
gap: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
color: #4a5568;
font-size: 25rpx;
}
.detail-row strong {
color: #1f2c3d;
}
.detail-medicine-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.detail-medicine-chip {
padding: 6px 10px;
background: #f5f7fa;
border-radius: 12px;
color: #1f2c3d;
font-size: 25rpx;
}
.detail-section-title {
font-weight: 600;
margin-bottom: 8px;
color: #1f2c3d;
font-size: 25rpx;
}

View File

@@ -0,0 +1,159 @@
import {useEffect, useState} from "react";
import {View, Text} from '@tarojs/components'
import {Button, Cell, CellGroup, Loading, Space, Tag} from '@nutui/nutui-react-taro'
import Taro, {useRouter} from '@tarojs/taro'
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {getClinicPrescription} from "@/api/clinic/clinicPrescription";
import {copyText} from "@/utils/common";
import './detail.scss'
const statusMap: Record<number, string> = {
0: '待开方',
1: '已完成',
2: '已支付',
3: '已取消'
}
const PrescriptionDetail = () => {
const {params} = useRouter()
const [detail, setDetail] = useState<ClinicPrescription>()
const [loading, setLoading] = useState<boolean>(false)
const loadDetail = async () => {
if (!params.id) return
try {
setLoading(true)
const data = await getClinicPrescription(Number(params.id))
setDetail(data)
} catch (error) {
Taro.showToast({
title: '加载详情失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
useEffect(() => {
loadDetail()
}, [params.id])
const getPatientDesc = () => {
if (!detail) return ''
const sexText = detail.sex === 0 ? '男' : detail.sex === 1 ? '女' : ''
const ageText = detail.age ? `${detail.age}` : ''
return [detail.realName, sexText, ageText].filter(Boolean).join(' ')
}
if (loading || !detail) {
return (
<View className="flex items-center justify-center h-full py-10">
<Loading>...</Loading>
</View>
)
}
const medicines = detail.items || []
// const shopOrder: any = (detail as any).shopOrder
return (
<View className="prescription-detail-page">
<View className="detail-header-card">
<View>
<Text className="detail-header-card__time">{detail.createTime || ''}</Text>
<View className="text-gray-500 mt-1" style={{fontSize: '25rpx'}}></View>
</View>
<Text className="text-gray-700" style={{fontSize: '25rpx'}}>{detail.doctorName || ''}</Text>
</View>
{/*<View className="detail-card">*/}
{/* <Text className="detail-card__title">处方状态</Text>*/}
{/* <Text className="detail-status">{statusMap[detail.status || 0] || '待处理'}</Text>*/}
{/* {shopOrder?.address && (*/}
{/* <Text className="detail-address">*/}
{/* {shopOrder.address}*/}
{/* </Text>*/}
{/* )}*/}
{/* <View className="detail-action-row">*/}
{/* <Button size="small" plain type="primary" onClick={() => Taro.showToast({title: '敬请期待', icon: 'none'})}>*/}
{/* 查看物流*/}
{/* </Button>*/}
{/* <Button size="small" plain type="success" onClick={() => Taro.showToast({title: '已复制开方信息', icon: 'none'})}>*/}
{/* 复诊开方*/}
{/* </Button>*/}
{/* </View>*/}
{/*</View>*/}
<View className="detail-card">
<View className="detail-row" style={{fontSize: '25rpx'}}>
<Text></Text>
<Space>
<Text className="text-gray-700" style={{fontSize: '25rpx'}}>{detail.orderNo || '-'}</Text>
{detail.orderNo && (
<Text className="text-green-600" style={{fontSize: '25rpx'}} onClick={() => copyText(detail.orderNo || '')}></Text>
)}
</Space>
</View>
</View>
<View className="detail-card">
<Text className="detail-section-title"></Text>
<View className="detail-row">
<Text className="detail-cell-text" style={{fontSize: '25rpx'}}>{getPatientDesc()}</Text>
<Text className="text-green-600" style={{fontSize: '25rpx'}} onClick={() => Taro.showToast({title: '患者档案敬请期待', icon: 'none'})}>
</Text>
</View>
{detail.diagnosis && (
<View className="detail-row">
<Text className="detail-cell-text" style={{fontSize: '25rpx'}}>{detail.diagnosis}</Text>
<Text className="text-green-600" style={{fontSize: '25rpx'}} onClick={() => Taro.showToast({title: '诊断详情敬请期待', icon: 'none'})}>
</Text>
</View>
)}
{detail.treatmentPlan && (
<Text className="detail-cell-text mt-2" style={{fontSize: '25rpx'}}>{detail.treatmentPlan}</Text>
)}
{detail.decoctionInstructions && (
<Text className="detail-cell-text mt-2" style={{fontSize: '25rpx'}}>{detail.decoctionInstructions}</Text>
)}
</View>
<View className="detail-card">
<View className="flex justify-between mb-2">
<Text className="detail-section-title"></Text>
<Text className="text-green-600" style={{fontSize: '25rpx'}} onClick={() => Taro.showToast({title: '已设为常用', icon: 'success'})}></Text>
</View>
<View className="detail-medicine-list">
{medicines.length === 0 && (
<Text className="text-gray-400" style={{fontSize: '25rpx'}}></Text>
)}
{medicines.map((item, index) => (
<View key={index} className="detail-medicine-chip">
{item.medicineName} {item.quantity || item.dosage || ''}
</View>
))}
</View>
</View>
<CellGroup>
<Cell title="剂数" extra={`${medicines.length} 味药`} titleStyle={{fontSize: '25rpx'}} descriptionStyle={{fontSize: '25rpx'}}/>
<Cell title="服用方式" extra={detail.decoctionInstructions || '遵医嘱'} titleStyle={{fontSize: '25rpx'}} descriptionStyle={{fontSize: '25rpx'}}/>
<Cell title="订单备注" extra={detail.comments || '无'} titleStyle={{fontSize: '25rpx'}} descriptionStyle={{fontSize: '25rpx'}}/>
<Cell
title="合计"
extra={(
<Space>
<Text className="text-red-500 font-semibold">¥{detail.orderPrice || '0.00'}</Text>
<Text className="text-green-600" style={{fontSize: '25rpx'}} onClick={() => Taro.showToast({title: '暂无明细', icon: 'none'})}></Text>
</Space>
)}
/>
</CellGroup>
</View>
)
}
export default PrescriptionDetail;

View File

@@ -9,12 +9,12 @@ import {
getStatusText, getStatusText,
getStatusTagType, getStatusTagType,
getStatusOptions, getStatusOptions,
mapApplyStatusToCustomerStatus,
mapCustomerStatusToApplyStatus mapCustomerStatusToApplyStatus
} from '@/utils/customerStatus'; } from '@/utils/customerStatus';
import FixedButton from "@/components/FixedButton"; import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common"; import navTo from "@/utils/common";
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply"; import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
import {userPageClinicPatientUser} from "@/api/clinic/clinicPatientUser";
// 扩展User类型添加客户状态和保护天数 // 扩展User类型添加客户状态和保护天数
interface CustomerUser extends UserType { interface CustomerUser extends UserType {
@@ -102,69 +102,6 @@ const CustomerIndex = () => {
}); });
}; };
// 计算剩余保护天数(基于过期时间)
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) => { const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
@@ -174,22 +111,22 @@ const CustomerIndex = () => {
// 构建API参数根据状态筛选 // 构建API参数根据状态筛选
const params: any = { const params: any = {
type: 4, // type: 4,
page: currentPage page: currentPage,
}; };
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab); const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
if (applyStatus !== undefined) { if (applyStatus !== undefined) {
params.applyStatus = applyStatus; params.applyStatus = applyStatus;
} }
const res = await pageShopDealerApply(params); const res = await userPageClinicPatientUser(params);
if (res?.list && res.list.length > 0) { if (res?.list && res.list.length > 0) {
// 正确映射状态并计算保护天数 // 正确映射状态并计算保护天数
const mappedList = res.list.map(customer => ({ const mappedList = res.list.map(customer => ({
...customer, ...customer,
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10), // customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '') // protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
})); }));
// 如果是重置页面或第一页,直接设置新数据;否则追加数据 // 如果是重置页面或第一页,直接设置新数据;否则追加数据

View File

@@ -1,4 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '新增处方订单', navigationBarTitleText: '用药订单',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

View File

@@ -0,0 +1,415 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag, Popup} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {
pageClinicPrescription,
WxPayResult
} from "@/api/clinic/clinicPrescription";
import {copyText} from "@/utils/common";
import {updateShopOrder} from "@/api/shop/shopOrder";
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
const ClinicPrescriptionList = () => {
const [list, setList] = useState<ClinicPrescription[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [addressList, setAddressList] = useState<ShopUserAddress[]>([])
const [addressPopupVisible, setAddressPopupVisible] = useState<boolean>(false)
const [addressLoading, setAddressLoading] = useState<boolean>(false)
const [addressSaving, setAddressSaving] = useState<boolean>(false)
const [selectedAddressId, setSelectedAddressId] = useState<number | null>(null)
const [pendingOrder, setPendingOrder] = useState<ClinicPrescription | null>(null)
const reload = () => {
setLoading(true)
pageClinicPrescription({
userId: Taro.getStorageSync('UserId'),
withDoctor: true,
})
.then(data => {
setList(data?.list || [])
})
.catch(() => {
Taro.showToast({
title: '获取数据失败',
icon: 'error'
});
})
.finally(() => {
setLoading(false)
})
}
const fetchAddressList = async () => {
try {
setAddressLoading(true)
const data = await listShopUserAddress({
userId: Taro.getStorageSync('UserId')
})
const addressData = data || []
setAddressList(addressData)
if (addressData.length > 0) {
setSelectedAddressId(addressData[0].id || null)
} else {
setSelectedAddressId(null)
}
return addressData
} catch (error) {
console.error('加载收货地址失败:', error)
Taro.showToast({
title: '加载收货地址失败',
icon: 'error'
})
setAddressList([])
setSelectedAddressId(null)
return []
} finally {
setAddressLoading(false)
}
}
const closeAddressPopup = () => {
setAddressPopupVisible(false)
setPendingOrder(null)
}
const formatFullAddress = (address?: ShopUserAddress) => {
if (!address) return ''
return address.fullAddress || `${address.province || ''}${address.city || ''}${address.region || ''}${address.address || ''}`
}
const ensureAddressBeforePay = async (item: ClinicPrescription) => {
setPendingOrder(item)
const addresses = await fetchAddressList()
if (addresses.length === 0) {
Taro.showModal({
title: '暂无收货地址',
content: '请先添加收货地址后再支付',
confirmText: '去添加',
success: (res) => {
if (res.confirm) {
Taro.navigateTo({url: '/user/address/index'})
}
}
})
return
}
setAddressPopupVisible(true)
}
const handleAddressConfirm = async () => {
if (!selectedAddressId) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
})
return
}
if (!pendingOrder || !(pendingOrder as any).shopOrder || !(pendingOrder as any).shopOrder.orderId) {
Taro.showToast({
title: '订单信息缺失',
icon: 'error'
})
return
}
const targetAddress = addressList.find(addr => addr.id === selectedAddressId)
if (!targetAddress) {
Taro.showToast({
title: '地址不存在',
icon: 'error'
})
return
}
try {
setAddressSaving(true)
await updateShopOrder({
orderId: (pendingOrder as any).shopOrder.orderId,
addressId: targetAddress.id,
realName: targetAddress.name,
address: formatFullAddress(targetAddress)
} as any)
Taro.showToast({
title: '地址已更新',
icon: 'success'
})
const orderToPay = pendingOrder
closeAddressPopup()
await onPay(orderToPay, true)
} catch (error: any) {
console.error('更新收货地址失败:', error)
Taro.showToast({
title: error?.message || '更新地址失败',
icon: 'error'
})
} finally {
setAddressSaving(false)
}
}
/**
* 处理微信支付
*/
const handleWechatPay = async (result: WxPayResult): Promise<void> => {
console.log('处理微信支付:', result);
if (!result) {
throw new Error('微信支付参数错误');
}
try {
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType as any,
paySign: result.paySign,
});
console.log('微信支付成功');
} catch (payError: any) {
console.error('微信支付失败:', payError);
// 处理微信支付特定错误
if (payError.errMsg) {
if (payError.errMsg.includes('cancel')) {
throw new Error('用户取消支付');
} else if (payError.errMsg.includes('fail')) {
throw new Error('微信支付失败,请重试');
}
}
throw new Error('微信支付失败');
}
};
/**
* 统一支付入口
*/
const onPay = async (item: ClinicPrescription | null, skipAddressCheck: boolean = false) => {
if (!item) {
Taro.showToast({
title: '处方信息缺失',
icon: 'error'
});
return;
}
if (!item.id) {
Taro.showToast({
title: '处方信息缺失',
icon: 'error'
});
return;
}
const shopOrder = (item as any).shopOrder
if (!skipAddressCheck) {
if (shopOrder && (!shopOrder.addressId || shopOrder.addressId === 0)) {
await ensureAddressBeforePay(item)
return
}
}
await Taro.showLoading({title: '支付中...'});
try {
// 调用统一支付接口
// @ts-ignore
const {data} = await updateShopOrder(
{
orderId: shopOrder.orderId,
makePay: true
}
);
const result = data as WxPayResult;
console.log('订单创建结果:', result);
// 调用微信支付
await handleWechatPay(result);
// 支付成功
// console.log('支付成功,订单号:', result.orderNo);
// await updateClinicPrescription({
// id: item.id,
// orderNo: result.orderNo,
// status: 2
// })
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// 延迟刷新列表
setTimeout(() => {
reload();
}, 2000);
} catch (error: any) {
console.error('支付失败:', error);
// 获取错误信息
const errorMessage = error.message || '支付失败,请重试';
Taro.showToast({
title: errorMessage,
icon: 'error'
});
} finally {
Taro.hideLoading();
}
};
useDidShow(() => {
reload()
});
if (list.length === 0 && !loading) {
return (
<ConfigProvider>
<View className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="暂无用药订单"
/>
</View>
</ConfigProvider>
)
}
return (
<>
<View className="p-3">
{list.map((item) => (
<CellGroup key={item.id} className="mb-3">
<Cell
title={`${item.id}`}
extra={
<Tag type={'warning'} className="font-medium">{item?.shopOrder.payStatus == 1 ? '已支付' : '待支付'}</Tag>
}
onClick={() => copyText(`${item.orderNo}`)}
/>
{item.diagnosis && (
<Cell
title="开方信息"
extra={
<Text className="text-gray-600 text-sm">
{item.diagnosis.length > 20
? `${item.diagnosis.substring(0, 20)}...`
: item.diagnosis}
</Text>
}
/>
)}
<Cell
title={'开方医生'}
extra={
<Space>
<Text className="font-medium">{item.doctorName}</Text>
</Space>
}
/>
<Cell
title="订单金额"
extra={
<Text className="text-red-500 font-medium">
¥{item.orderPrice || '0.00'}
</Text>
}
/>
<Cell
title="开方时间"
extra={
<Text className="text-gray-500 text-xs">
{item.createTime}
</Text>
}
/>
<Cell>
<Space className="w-full justify-end">
<Button
size="small"
type="warning"
onClick={() => Taro.navigateTo({url: `/clinic/clinicPatientUser/detail?id=${item.id}`})}
>
</Button>
{item?.shopOrder?.payStatus == 0 && (
<Button
type={'danger'}
size="small"
onClick={() => onPay(item)}
>
</Button>
)}
</Space>
</Cell>
</CellGroup>
))}
</View>
<Popup
visible={addressPopupVisible}
position="bottom"
round
onClose={closeAddressPopup}
>
<View className="p-4 bg-white">
<View className="text-lg font-medium mb-3"></View>
{addressLoading ? (
<View className="text-center text-gray-500 py-6">...</View>
) : addressList.length === 0 ? (
<View className="text-center text-gray-500 py-6"></View>
) : (
<View style={{maxHeight: '300px', overflow: 'auto'}}>
{addressList.map(address => (
<View
key={address.id}
className={`border rounded-lg p-3 mb-2 ${selectedAddressId === address.id ? 'border-orange-400 bg-orange-50' : 'border-gray-200'}`}
onClick={() => setSelectedAddressId(address.id || null)}
>
<View className="flex justify-between mb-1">
<Text className="font-medium">{address.name}</Text>
<Text className="text-gray-600">{address.phone}</Text>
</View>
<Text className="text-gray-500 text-sm">{formatFullAddress(address)}</Text>
{address.isDefault && (
<Text className="text-orange-500 text-xs mt-1"></Text>
)}
</View>
))}
</View>
)}
<View className="flex gap-3 mt-4">
<Button
className="flex-1"
onClick={() => Taro.navigateTo({url: '/user/address/index'})}
>
</Button>
<Button
type="primary"
className="flex-1"
loading={addressSaving}
onClick={handleAddressConfirm}
>
</Button>
</View>
</View>
</Popup>
</>
)
}
export default ClinicPrescriptionList;

View File

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

View File

@@ -0,0 +1,285 @@
import {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, SearchBar, Button} from '@nutui/nutui-react-taro'
import {Phone} from '@nutui/icons-react-taro'
import type {ClinicPatientUser as PatientUserType} from "@/api/clinic/clinicPatientUser/model";
import {
userPageClinicPatientUser
} from "@/api/clinic/clinicPatientUser";
// 患者类型
interface PatientUser extends PatientUserType {
}
const SelectPatient = () => {
const [list, setList] = useState<PatientUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 复制手机号
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 fetchPatientData = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数
const params: any = {
page: currentPage
};
// 添加搜索关键词
if (displaySearchValue.trim()) {
params.keywords = displaySearchValue.trim();
}
const res = await userPageClinicPatientUser(params);
if (res?.list && res.list.length > 0) {
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(res.list);
} else {
setList(prevList => [...prevList, ...res.list]);
}
// 正确判断是否还有更多数据
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, displaySearchValue]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchPatientData(false, nextPage);
}
// 防抖搜索功能
useEffect(() => {
const timer = setTimeout(() => {
setDisplaySearchValue(searchValue);
}, 300); // 300ms 防抖
return () => clearTimeout(timer);
}, [searchValue]);
// 初始化数据
useEffect(() => {
fetchPatientData(true).then();
}, [displaySearchValue]);
// 监听页面显示,当从其他页面返回时刷新数据
useDidShow(() => {
// 刷新数据
setList([]);
setPage(1);
setHasMore(true);
fetchPatientData(true);
});
// 选择患者
const selectPatient = (patient: PatientUser) => {
// 将选中的患者信息传递回上一个页面
const pages = Taro.getCurrentPages();
if (pages.length > 1) {
const prevPage = pages[pages.length - 2];
// @ts-ignore
if (prevPage && typeof prevPage.setSelectedPatient === 'function') {
// @ts-ignore
prevPage.setSelectedPatient(patient);
}
}
// 同时存储到本地存储,作为备选方案
try {
Taro.setStorageSync('selectedPatient', JSON.stringify(patient));
} catch (e) {
console.error('存储患者信息失败:', e);
}
Taro.navigateBack();
};
// 渲染患者项
const renderPatientItem = (patient: PatientUser) => (
<View key={patient.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center mb-3">
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<Text className="font-semibold text-gray-800 mr-2">
{patient.realName || '未命名'}
</Text>
</View>
<View className="flex items-center mb-1">
<Space direction="vertical">
<View className="flex items-center">
<Text className="text-xs text-gray-500" onClick={(e) => {
e.stopPropagation();
makePhoneCall(patient.phone || '');
}}>{patient.phone || '未提供'}</Text>
<View className="flex items-center ml-2">
<Phone
size={12}
className="text-green-500 mr-2"
onClick={(e) => {
e.stopPropagation();
makePhoneCall(patient.phone || '');
}}
/>
<Text
className="text-xs text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
copyPhone(patient.phone || '');
}}
>
</Text>
</View>
</View>
<Text className="text-xs text-gray-500">
{patient.createTime || '未知'}
</Text>
</Space>
</View>
{/* 显示 comments 字段 */}
<View className="flex items-center">
<Text className="text-xs text-gray-500">{patient.comments || '暂无'}</Text>
</View>
</View>
</View>
{/* 选择按钮 */}
<Button
size="small"
onClick={() => selectPatient(patient)}
style={{backgroundColor: '#1890ff', color: 'white'}}
>
</Button>
</View>
);
// 渲染患者列表
const renderPatientList = () => {
const isSearching = displaySearchValue.trim().length > 0;
return (
<View className="flex-1">
{/* 搜索结果统计 */}
{isSearching && (
<View className="bg-white px-4 py-2 border-b border-gray-100">
<Text className="text-sm text-gray-600">
"{displaySearchValue}" {list.length}
</Text>
</View>
)}
<View className="p-4" style={{
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
loadingText="加载中..."
loadMoreText={
list.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无患者数据"}
/>
) : (
<View className={'h-3 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && list.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
list.map(renderPatientItem)
)}
</InfiniteLoading>
</View>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 搜索栏 */}
<View className="bg-white py-2 border-b border-gray-100">
<SearchBar
value={searchValue}
placeholder="搜索患者姓名、手机号"
onChange={(value) => setSearchValue(value)}
onClear={() => {
setSearchValue('');
setDisplaySearchValue('');
}}
clearable
/>
</View>
{/* 患者列表 */}
{renderPatientList()}
</View>
);
};
export default SelectPatient;

View File

@@ -0,0 +1,201 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag} from '@nutui/nutui-react-taro'
import {Del, Edit} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {
pageClinicPrescription,
removeClinicPrescription
} from "@/api/clinic/clinicPrescription";
import FixedButton from "@/components/FixedButton";
import {copyText} from "@/utils/common";
const ClinicPrescriptionList = () => {
const [list, setList] = useState<ClinicPrescription[]>([])
const [loading, setLoading] = useState<boolean>(false)
const reload = () => {
setLoading(true)
pageClinicPrescription({
// 添加查询条件
doctorId: Taro.getStorageSync('UserId'),
})
.then(data => {
setList(data?.list || [])
})
.catch(() => {
Taro.showToast({
title: '获取数据失败',
icon: 'error'
});
})
.finally(() => {
setLoading(false)
})
}
const onDel = async (item: ClinicPrescription) => {
const res = await Taro.showModal({
title: '确认删除',
content: `确定要删除处方编号「${item.orderNo}」吗?`,
})
if (res.confirm) {
try {
await removeClinicPrescription(item.id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
} catch (error) {
Taro.showToast({
title: '删除失败',
icon: 'error'
});
}
}
}
const onEdit = (item: ClinicPrescription) => {
Taro.navigateTo({
url: `/clinic/clinicPrescription/add?id=${item.id}`
})
}
const onDetail = (item: ClinicPrescription) => {
Taro.navigateTo({
url: `/clinic/clinicPrescription/detail?id=${item.id}`
})
}
const getSexName = (sex?: number) => {
return sex === 0 ? '男' : sex === 1 ? '女' : ''
}
useDidShow(() => {
reload()
});
if (list.length === 0 && !loading) {
return (
<ConfigProvider>
<View className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="暂无处方数据"
/>
<Space style={{marginTop: '20px'}}>
<Button
type="primary"
onClick={() => Taro.navigateTo({url: '/clinic/clinicPrescription/add'})}
>
</Button>
</Space>
</View>
</ConfigProvider>
)
}
return (
<>
<View className="p-3">
{list.map((item) => (
<CellGroup key={item.id} className="mb-3">
<Cell
title={item.orderNo}
extra={
<Tag type={'warning'} className="font-medium"></Tag>
}
onClick={() => copyText(`${item.orderNo}`)}
/>
<Cell
title={'患者名称'}
extra={
<Space>
<Text className="font-medium">{item.realName}</Text>
<Text className="font-medium">{item.age}</Text>
<Text className="font-medium">{getSexName(item.sex)}</Text>
</Space>
}
/>
{/*<Cell*/}
{/* title="处方类型"*/}
{/* extra={*/}
{/* <Tag type="info">*/}
{/* {getPrescriptionTypeText(item.prescriptionType)}*/}
{/* </Tag>*/}
{/* }*/}
{/*/>*/}
{item.diagnosis && (
<Cell
title="诊断结果"
extra={
<Text className="text-gray-600 text-sm">
{item.diagnosis.length > 20
? `${item.diagnosis.substring(0, 20)}...`
: item.diagnosis}
</Text>
}
/>
)}
<Cell
title="订单金额"
extra={
<Text className="text-red-500 font-medium">
¥{item.orderPrice || '0.00'}
</Text>
}
/>
<Cell
title="创建时间"
extra={
<Text className="text-gray-500 text-xs">
{item.createTime}
</Text>
}
/>
<Cell>
<Space className="w-full justify-end">
<Button
size="small"
icon={<Edit/>}
onClick={() => onDetail(item)}
>
</Button>
<Button
size="small"
icon={<Edit/>}
onClick={() => onEdit(item)}
>
</Button>
<Button
size="small"
icon={<Del/>}
onClick={() => onDel(item)}
>
</Button>
</Space>
</Cell>
</CellGroup>
))}
</View>
<FixedButton
text="开处方"
onClick={() => Taro.navigateTo({url: '/clinic/clinicPrescription/add'})}
/>
</>
)
}
export default ClinicPrescriptionList;

View File

@@ -1,5 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '新增处方主表 navigationBarTitleText: '开处方',
',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

View File

@@ -1,51 +1,273 @@
import {useEffect, useState, useRef} from "react"; import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro' import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro' import {
Loading,
Button,
Form,
Cell,
Avatar,
Input,
Space,
TextArea
} from '@nutui/nutui-react-taro'
import {ArrowRight} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {View} from '@tarojs/components' import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common";
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {getClinicPrescription, listClinicPrescription, updateClinicPrescription, addClinicPrescription} from "@/api/clinic/clinicPrescription"; import {TenantId} from "@/config/app";
import {getClinicPatientUser} from "@/api/clinic/clinicPatientUser";
const AddClinicPrescription = () => { // 图片数据接口
interface UploadedImageData {
url?: string;
src?: string;
name?: string;
uid?: string;
message?: string;
type?: string;
}
const AddClinicOrder = () => {
const {params} = useRouter(); const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true) const [toUser, setToUser] = useState<ClinicPatientUser>()
const [FormData, setFormData] = useState<ClinicPrescription>({}) const [loading, setLoading] = useState<boolean>(false)
const formRef = useRef<any>(null) const formRef = useRef<any>()
const [fileList, setFileList] = useState<UploadedImageData[]>([]) // 图片文件列表
// 患者和处方状态
const [selectedPatient, setSelectedPatient] = useState<ClinicPatientUser>()
const [selectedPrescription, setSelectedPrescription] = useState<ClinicPrescription>()
// 表单数据
const [formData, setFormData] = useState<ClinicPrescription>({
userId: undefined,
doctorId: undefined,
diagnosis: '',
treatmentPlan: '',
orderPrice: '',
decoctionInstructions: '',
items: [],
image: '' // 添加image字段
})
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const toUserId = params.id ? Number(params.id) : undefined
const reload = async () => { const reload = async () => {
if (params.id) { if (toUserId) {
const data = await getClinicPrescription(Number(params.id)) getClinicPatientUser(Number(toUserId)).then(data => {
setFormData(data) setToUser(data)
} else { })
setFormData({})
} }
} }
// 提交表单 // 设置选中的患者(供其他页面调用)
// @ts-ignore
const setSelectedPatientFunc = (patient: ClinicPatientUser) => {
console.log('设置选中的患者:', patient)
setToUser(patient)
setSelectedPatient(patient)
}
// 设置选中的处方(供其他页面调用)
// @ts-ignore
const setSelectedPrescriptionFunc = (prescription: ClinicPrescription) => {
console.log('设置选中的处方:', prescription)
setSelectedPrescription(prescription)
}
// 处理表单字段变化
const handleFormChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
// 选择并上传图片
const handleChooseImage = () => {
if (fileList.length >= 5) { // 修正最大图片数量为5
Taro.showToast({
title: '最多只能上传5张图片',
icon: 'none'
})
return
}
Taro.chooseImage({
count: 5 - fileList.length, // 剩余可选择的数量
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择图片成功:', res)
// 逐个上传选中的图片
res.tempFilePaths.forEach((filePath, index) => {
uploadSingleImage(filePath, index)
})
},
fail: (err) => {
console.log('选择图片失败:', err)
Taro.showToast({
title: '选择图片失败',
icon: 'error'
})
}
})
}
// 处理文件删除
const handleFileRemove = (file: any) => {
console.log('删除文件:', file)
const newFileList = fileList.filter(f => f.uid !== file.uid)
setFileList(newFileList)
// 更新表单数据 - 使用JSON格式存储
if (newFileList.length === 0) {
setFormData(prev => ({
...prev,
image: ''
}))
} else {
const imageData: UploadedImageData[] = newFileList.map(f => ({
url: f.url,
src: f.url,
name: f.name,
uid: f.uid
}))
setFormData(prev => ({
...prev,
image: JSON.stringify(imageData)
}))
}
}
// 上传单张图片
const uploadSingleImage = (filePath: any, index: number) => {
Taro.uploadFile({
url: 'https://server.websoft.top/api/oss/upload',
filePath: filePath,
name: 'file',
header: {
'content-type': 'multipart/form-data',
TenantId
},
success: (res) => {
try {
const data = JSON.parse(res.data);
console.log('上传成功', data)
if (data.code === 0 && data.data && data.data.url) {
// 更新文件列表
const newFile = {
name: `图片${Date.now()}_${index}`,
url: data.data.url,
status: 'success',
message: '上传成功',
type: 'image',
uid: `${Date.now()}_${index}`,
}
setFileList(prev => {
const newList = [...prev, newFile]
// 同时更新表单数据 - 使用JSON格式存储
const imageData: UploadedImageData[] = newList.map(f => ({
url: f.url,
name: f.name,
uid: f.uid
}))
setFormData(prevForm => ({
...prevForm,
image: JSON.stringify(imageData)
}))
return newList
})
Taro.showToast({
title: '上传成功',
icon: 'success'
})
} else {
Taro.showToast({
title: data.message || '上传失败',
icon: 'error'
})
}
} catch (error) {
console.error('解析响应失败:', error)
Taro.showToast({
title: '上传失败: 数据格式错误',
icon: 'error'
})
}
},
fail: (err) => {
console.log('上传请求失败', err);
Taro.showToast({
title: '上传失败: 网络错误',
icon: 'error'
})
}
})
}
// 提交表单 - 修改为跳转到确认页
const submitSucceed = async (values: any) => { const submitSucceed = async (values: any) => {
try { try {
if (params.id) { console.log('提交数据:', values)
// 编辑模式
await updateClinicPrescription({ // 参数校验
...values, if (!toUser && !selectedPatient) {
id: Number(params.id) Taro.showToast({
}) title: `请选择发送对象或患者`,
} else { icon: 'error'
// 新增模式 });
await addClinicPrescription(values) return false;
} }
Taro.showToast({ if (!values.diagnosis) {
title: `操作成功`, Taro.showToast({
icon: 'success' title: `请输入诊断结果`,
icon: 'error'
});
return false;
}
if (!values.treatmentPlan) {
Taro.showToast({
title: `请输入治疗方案`,
icon: 'error'
});
return false;
}
// 构建订单数据
const orderData = {
patient: toUser || selectedPatient,
prescription: selectedPrescription,
diagnosis: values.diagnosis,
treatmentPlan: values.treatmentPlan,
decoctionInstructions: values.decoctionInstructions || formData.decoctionInstructions,
images: fileList,
orderPrice: selectedPrescription?.orderPrice || '0.00'
}
// 保存到本地存储
Taro.setStorageSync('tempOrderData', JSON.stringify(orderData))
console.log('跳转到订单确认页,订单数据:', orderData)
// 跳转到确认页
Taro.navigateTo({
url: '/clinic/clinicPrescription/confirm'
}) })
setTimeout(() => { } catch (error: any) {
return Taro.navigateBack() console.error('数据处理失败:', error);
}, 1000)
} catch (error) {
Taro.showToast({ Taro.showToast({
title: `操作失败`, title: `数据处理失败: ${error.message || error || '未知错误'}`,
icon: 'error' icon: 'error'
}); });
} }
@@ -59,7 +281,45 @@ const AddClinicPrescription = () => {
reload().then(() => { reload().then(() => {
setLoading(false) setLoading(false)
}) })
}, []);
// 设置页面实例的方法,供其他页面调用
try {
// @ts-ignore
if (Taro.getCurrentInstance() && Taro.getCurrentInstance().page) {
// @ts-ignore
Taro.getCurrentInstance().page.setSelectedPatient = setSelectedPatientFunc;
// @ts-ignore
Taro.getCurrentInstance().page.setSelectedPrescription = setSelectedPrescriptionFunc;
}
} catch (error) {
console.error('设置页面实例方法失败:', error);
}
// 从本地存储获取之前选择的患者和处方
try {
const storedPatient = Taro.getStorageSync('selectedPatient');
if (storedPatient) {
const parsedPatient = JSON.parse(storedPatient);
setSelectedPatient(parsedPatient);
Taro.removeStorageSync('selectedPatient');
}
} catch (error) {
console.error('解析存储的患者数据失败:', error);
Taro.removeStorageSync('selectedPatient');
}
try {
const storedPrescription = Taro.getStorageSync('selectedPrescription');
if (storedPrescription) {
const parsedPrescription = JSON.parse(storedPrescription);
setSelectedPrescription(parsedPrescription);
Taro.removeStorageSync('selectedPrescription');
}
} catch (error) {
console.error('解析存储的处方数据失败:', error);
Taro.removeStorageSync('selectedPrescription');
}
}, [isEditMode]);
if (loading) { if (loading) {
return <Loading className={'px-2'}></Loading> return <Loading className={'px-2'}></Loading>
@@ -67,32 +327,188 @@ const AddClinicPrescription = () => {
return ( return (
<> <>
<Form {/* 显示已选择的用户(如果有的话) */}
ref={formRef} <Cell title={(
divider <View className={'flex items-center'}>
initialValues={FormData} <Avatar src={toUser?.avatar}/>
labelPosition="left" <View className={'ml-2 flex flex-col'}>
onFinish={(values) => submitSucceed(values)} <Text>{toUser?.realName || '请选择'}</Text>
onFinishFailed={(errors) => submitFailed(errors)} <Text className={'text-gray-300'}>{toUser?.phone}</Text>
footer={ </View>
<div </View>
style={{ )} extra={(
display: 'flex', <ArrowRight color="#cccccc" className={'mt-2'} size={20}/>
justifyContent: 'center', )} onClick={() => navTo(`/clinic/clinicPatientUser/selectPatient`, true)}
width: '100%' />
}}
{toUser && (
<Form
ref={formRef}
divider
initialValues={formData}
labelPosition={'top'}
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
>
{/* 基本信息 */}
<Form.Item
name="diagnosis"
label={'诊断结果'}
required
rules={[{required: true, message: '请填写诊断结果'}]}
initialValue={formData.diagnosis}
> >
<Button <Input placeholder="请输入诊断结果" style={{
nativeType="submit" backgroundColor: '#f9f9f9',
type="success" padding: '4px',
size="large" }}/>
className={'w-full'} </Form.Item>
block <Form.Item
> name="treatmentPlan"
{params.id ? '更新' : '保存'} label={'治疗方案'}
</Button> required
</div> rules={[{required: true, message: '请填写治疗方案'}]}
} initialValue={formData.treatmentPlan}
> >
<CellGroup style={{padding: '4px 0'}}> <TextArea
<Form.Item name="userId" label="患者" initialValue={FormData.userId} required> value={formData.treatmentPlan}
onChange={(value) => handleFormChange('treatmentPlan', value)}
placeholder="请填写治疗方案"
style={{
backgroundColor: '#f9f9f9',
padding: '4px',
}}
/>
</Form.Item>
<Form.Item
label={'拍照上传'}
name="image"
rules={[{message: '请上传照片'}]}
>
<View style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: '8px',
padding: '0'
}}>
{/* 显示已上传的图片 */}
{fileList.map((file) => (
<View key={file.uid} style={{
position: 'relative',
width: '80px',
height: '80px',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid #d9d9d9'
}}>
<img
src={file.url}
alt={file.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/>
<Button
size="small"
type="default"
style={{
position: 'absolute',
top: '-8px',
right: '-8px',
width: '20px',
height: '20px',
borderRadius: '10px',
fontSize: '12px',
minWidth: '20px',
padding: 0,
lineHeight: '20px'
}}
onClick={() => handleFileRemove(file)}
>
×
</Button>
</View>
))}
{/* 添加图片按钮 */}
{fileList.length < 5 && (
<View
onClick={handleChooseImage}
style={{
width: '80px',
height: '80px',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '2px dashed #d9d9d9',
backgroundColor: '#fafafa',
cursor: 'pointer'
}}
>
<span style={{fontSize: '20px', color: '#d9d9d9'}}>+</span>
<span style={{fontSize: '10px', marginTop: '2px', color: '#666'}}>
</span>
</View>
)}
</View>
<View className="text-xs text-gray-500">
//{fileList.length}
</View>
</Form.Item>
</Form>
)}
{/* 选择处方 */}
<Cell
title="选择处方"
extra={selectedPrescription ? (
<View className={'flex items-center'}>
<Text className={'mr-2'}>{selectedPrescription.treatmentPlan || '未知处方'}</Text>
<ArrowRight color="#cccccc" size={18}/>
</View>
) : (
<Text className={'text-gray-400'}></Text>
)}
onClick={() => navTo(`/clinic/clinicPrescription/selectPrescription`, true)}
/>
{/* 药方信息 */}
{selectedPrescription && (
<>
<Cell extra={'药方信息'}>
<View className={'flex flex-col'}>
<View className={'py-3'}>RP: {selectedPrescription.prescriptionType === 0 ? '中药' : '西药'} {selectedPrescription.items?.length}{selectedPrescription.orderPrice}</View>
<Space className={'flex flex-wrap'}>
{selectedPrescription.items?.map(item => (
<Button>{item.medicineName} 105</Button>
))}
</Space>
</View>
</Cell>
{/* 煎药说明 */}
<TextArea
value={formData.decoctionInstructions}
onChange={(value) => handleFormChange('decoctionInstructions', value)}
placeholder="请填写用药说明"
rows={2}
maxLength={200}
/>
</>
)}
{/* 底部浮动按钮 */}
<FixedButton text={'下一步:确认订单信息'} onClick={() => formRef.current?.submit()}/>
</>
);
};
export default AddClinicOrder;

View File

@@ -0,0 +1,809 @@
import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro'
import {useEffect, useState, useCallback, CSSProperties} from "react";
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
import {updateShopOrder, createOrder} from "@/api/shop/shopOrder";
import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import {copyText} from "@/utils/common";
import PaymentCountdown from "@/components/PaymentCountdown";
import {PaymentType} from "@/utils/payment";
import {goTo} from "@/utils/navigation";
import {pageClinicPrescription} from "@/api/clinic/clinicPrescription";
import Prescription from "@/clinic/clinicPatientUser/prescription";
// 判断订单是否支付已过期
const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => {
if (!createTime) return false;
const createTimeObj = dayjs(createTime);
const expireTime = createTimeObj.add(timeoutHours, 'hour');
const now = dayjs();
return now.isAfter(expireTime);
};
const getInfiniteUlStyle = (showSearch: boolean = false): CSSProperties => ({
marginTop: showSearch ? '0' : '0', // 如果显示搜索框,增加更多的上边距
height: showSearch ? '75vh' : '84vh', // 相应调整高度
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden'
// 注意:小程序不支持 boxShadow
})
// 统一的订单状态标签配置,与后端 statusFilter 保持一致
const tabs = [
{
index: 0,
key: '全部',
title: '全部',
description: '所有订单',
statusFilter: -1 // 使用-1表示全部订单
},
{
index: 1,
key: '待付款',
title: '待付款',
description: '等待付款的订单',
statusFilter: 0 // 对应后端pay_status = false
},
{
index: 2,
key: '待发货',
title: '待发货',
description: '已付款待发货的订单',
statusFilter: 1 // 对应后端pay_status = true AND delivery_status = 10
},
{
index: 3,
key: '待收货',
title: '待收货',
description: '已发货待收货的订单',
statusFilter: 3 // 对应后端pay_status = true AND delivery_status = 20
},
{
index: 4,
key: '已完成',
title: '已完成',
description: '已完成的订单',
statusFilter: 5 // 对应后端order_status = 1
},
{
index: 5,
key: '退货/售后',
title: '退货/售后',
description: '退货/售后的订单',
statusFilter: 6 // 对应后端order_status = 6 (已退款)
}
]
// 扩展订单接口,包含商品信息
interface OrderWithGoods extends ShopOrder {
orderGoods?: ShopOrderGoods[];
}
interface OrderListProps {
onReload?: () => void;
searchParams?: ShopOrderParam;
showSearch?: boolean;
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
}
function OrderList(props: OrderListProps) {
const [list, setList] = useState<OrderWithGoods[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 根据传入的statusFilter设置初始tab索引
const getInitialTabIndex = () => {
if (props.searchParams?.statusFilter !== undefined) {
const tab = tabs.find(t => t.statusFilter === props.searchParams?.statusFilter);
return tab ? tab.index : 0;
}
return 0;
};
const [tapIndex, setTapIndex] = useState<number>(() => {
const initialIndex = getInitialTabIndex();
console.log('初始化tapIndex:', initialIndex, '对应statusFilter:', props.searchParams?.statusFilter);
return initialIndex;
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [cancelDialogVisible, setCancelDialogVisible] = useState(false)
const [orderToCancel, setOrderToCancel] = useState<ShopOrder | null>(null)
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
// 获取订单状态文本
const getOrderStatusText = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 4) return '退款申请中';
if (order.orderStatus === 5) return '退款被拒绝';
if (order.orderStatus === 6) return '退款成功';
if (order.orderStatus === 7) return '客户端申请退款';
// 检查支付状态 (payStatus为boolean类型false/0表示未付款true/1表示已付款)
if (!order.payStatus) return '等待买家付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已完成';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
if (order.orderStatus === 0) return '未使用';
return '未知状态';
};
// 获取订单状态颜色
const getOrderStatusColor = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return 'text-gray-500'; // 已取消
if (order.orderStatus === 4) return 'text-orange-500'; // 退款申请中
if (order.orderStatus === 5) return 'text-red-500'; // 退款被拒绝
if (order.orderStatus === 6) return 'text-green-500'; // 退款成功
if (order.orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
// 检查支付状态
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
// 最后检查订单完成状态
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
if (order.orderStatus === 0) return 'text-gray-500'; // 未使用
return 'text-gray-600'; // 默认颜色
};
// 使用后端统一的 statusFilter 进行筛选
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
// 添加用户ID过滤
params.userId = Taro.getStorageSync('UserId');
// 获取当前tab的statusFilter配置
const currentTab = tabs.find(tab => tab.index === Number(index));
if (currentTab && currentTab.statusFilter !== undefined) {
params.statusFilter = currentTab.statusFilter;
}
// 注意当statusFilter为undefined时不要添加到params中这样API请求就不会包含这个参数
console.log(`Tab ${index} (${currentTab?.title}) 筛选参数:`, params);
return params;
};
const reload = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
setError(null); // 清除之前的错误
const currentPage = resetPage ? 1 : (targetPage || page);
const statusParams = getOrderStatusParams(tapIndex);
// 合并搜索条件tab的statusFilter优先级更高
const searchConditions: any = {
page: currentPage,
doctorId: statusParams.userId, // 用户ID
// ...props.searchParams, // 搜索关键词等其他条件
};
// statusFilter总是添加到搜索条件中包括-1表示全部
if (statusParams.statusFilter !== undefined) {
searchConditions.statusFilter = statusParams.statusFilter;
}
console.log('订单筛选条件:', {
tapIndex,
statusParams,
searchConditions,
finalStatusFilter: searchConditions.statusFilter
});
try {
const res = await pageClinicPrescription(searchConditions);
let newList: OrderWithGoods[];
if (res?.list && res?.list.length > 0) {
// 使用函数式更新避免依赖 list
setList(res?.list);
// 正确判断是否还有更多数据
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
setList(prevList => resetPage ? [] : prevList);
setHasMore(false);
}
setPage(currentPage);
setLoading(false);
} catch (error) {
console.error('加载订单失败:', error);
setLoading(false);
setError('加载订单失败,请重试');
// 添加错误提示
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
}
}, [tapIndex, page, props.searchParams]); // 移除 list 依赖
const reloadMore = useCallback(async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
setPage(nextPage);
await reload(false, nextPage);
}, [loading, hasMore, page, reload]);
// 确认收货 - 显示确认对话框
const confirmReceive = (order: ShopOrder) => {
setOrderToConfirmReceive(order);
setConfirmReceiveDialogVisible(true);
};
// 确认收货 - 执行收货操作
const handleConfirmReceive = async () => {
if (!orderToConfirmReceive) return;
try {
setConfirmReceiveDialogVisible(false);
await updateShopOrder({
...orderToConfirmReceive,
deliveryStatus: 30, // 已收货
orderStatus: 1 // 已完成
});
Taro.showToast({
title: '确认收货成功',
icon: 'success'
});
await reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
// 清空状态
setOrderToConfirmReceive(null);
} catch (error) {
console.error('确认收货失败:', error);
Taro.showToast({
title: '确认收货失败',
icon: 'none'
});
// 重新显示对话框
setConfirmReceiveDialogVisible(true);
}
};
// 取消确认收货对话框
const handleCancelReceiveDialog = () => {
setConfirmReceiveDialogVisible(false);
setOrderToConfirmReceive(null);
};
// 申请退款 (待发货状态)
const applyRefund = (order: ShopOrder) => {
// 跳转到退款申请页面
Taro.navigateTo({
url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
});
};
// 查看物流 (待收货状态)
const viewLogistics = (order: ShopOrder) => {
// 跳转到物流查询页面
Taro.navigateTo({
url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF`
});
};
// 再次购买 (已完成状态)
const buyAgain = (order: ShopOrder) => {
console.log('再次购买:', order);
goTo(`/shop/orderConfirm/index?goodsId=${order.orderGoods[0].goodsId}`)
// Taro.showToast({
// title: '再次购买功能开发中',
// icon: 'none'
// });
};
// 评价商品 (已完成状态)
const evaluateGoods = (order: ShopOrder) => {
// 跳转到评价页面
Taro.navigateTo({
url: `/user/order/evaluate/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
});
};
// 查看进度 (退款/售后状态)
const viewProgress = (order: ShopOrder) => {
// 根据订单状态确定售后类型
let afterSaleType = 'refund' // 默认退款
if (order.orderStatus === 4) {
afterSaleType = 'refund' // 退款申请中
} else if (order.orderStatus === 7) {
afterSaleType = 'return' // 退货申请中
}
// 跳转到售后进度页面
Taro.navigateTo({
url: `/user/order/progress/index?orderId=${order.orderId}&orderNo=${order.orderNo}&type=${afterSaleType}`
});
};
// 撤销申请 (退款/售后状态)
const cancelApplication = (order: ShopOrder) => {
console.log('撤销申请:', order);
Taro.showToast({
title: '撤销申请功能开发中',
icon: 'none'
});
};
// 取消订单
const cancelOrder = (order: ShopOrder) => {
setOrderToCancel(order);
setCancelDialogVisible(true);
};
// 确认取消订单
const handleConfirmCancel = async () => {
if (!orderToCancel) return;
try {
setCancelDialogVisible(false);
// 更新订单状态为已取消,而不是删除订单
await updateShopOrder({
...orderToCancel,
orderStatus: 2 // 已取消
});
Taro.showToast({
title: '订单已取消',
icon: 'success'
});
void reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
} catch (error) {
console.error('取消订单失败:', error);
Taro.showToast({
title: '取消订单失败',
icon: 'error'
});
} finally {
setOrderToCancel(null);
}
};
// 取消对话框的取消操作
const handleCancelDialog = () => {
setCancelDialogVisible(false);
setOrderToCancel(null);
};
// 立即支付
// @ts-ignore
const payOrder = async (order: Prescription) => {
try {
if (!order.id || !order.orderNo) {
Taro.showToast({
title: '订单信息错误',
icon: 'error'
});
return;
}
// 检查订单是否已过期
if (order.createTime && isPaymentExpired(order.createTime)) {
Taro.showToast({
title: '订单已过期,无法支付',
icon: 'error'
});
return;
}
// 检查订单状态
if (order.payStatus) {
Taro.showToast({
title: '订单已支付',
icon: 'none'
});
return;
}
if (order.orderStatus === 2) {
Taro.showToast({
title: '订单已取消,无法支付',
icon: 'error'
});
return;
}
Taro.showLoading({title: '发起支付...'});
// 构建商品数据
const goodsItems = order.items?.map(goods => ({
goodsId: goods.medicineId,
quantity: goods.totalNum || 1
})) || [];
// 对于已存在的订单,我们需要重新发起支付
// 构建支付请求数据,包含完整的商品信息
const paymentData = {
orderId: order.id,
orderNo: order.orderNo,
goodsItems: goodsItems,
// addressId: order.addressId,
payType: PaymentType.WECHAT,
type: 3
};
console.log('重新支付数据:', paymentData);
// 直接调用createOrder API进行重新支付
const result = await createOrder(paymentData as any);
if (!result) {
throw new Error('支付发起失败');
}
// 验证微信支付必要参数
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
throw new Error('微信支付参数不完整');
}
// 调用微信支付
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
paySign: result.paySign,
});
// 支付成功
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// 重新加载订单列表
void reload(true);
props.onReload?.();
// 跳转到订单页面
setTimeout(() => {
Taro.navigateTo({url: '/user/order/order'});
}, 2000);
} catch (error: any) {
console.error('支付失败:', error);
let errorMessage = '支付失败,请重试';
if (error.message) {
if (error.message.includes('cancel')) {
errorMessage = '用户取消支付';
} else if (error.message.includes('余额不足')) {
errorMessage = '账户余额不足';
} else {
errorMessage = error.message;
}
}
Taro.showToast({
title: errorMessage,
icon: 'error'
});
} finally {
Taro.hideLoading();
}
};
useEffect(() => {
void reload(true); // 首次加载或tab切换时重置页码
}, [tapIndex]); // 只监听tapIndex变化避免reload依赖循环
// 监听外部statusFilter变化同步更新tab索引
useEffect(() => {
// 获取当前的statusFilter如果未定义则默认为-1全部
const currentStatusFilter = props.searchParams?.statusFilter !== undefined
? props.searchParams.statusFilter
: -1;
const tab = tabs.find(t => t.statusFilter === currentStatusFilter);
const targetTabIndex = tab ? tab.index : 0;
console.log('外部statusFilter变化:', {
statusFilter: currentStatusFilter,
originalStatusFilter: props.searchParams?.statusFilter,
currentTapIndex: tapIndex,
targetTabIndex,
shouldUpdate: targetTabIndex !== tapIndex
});
if (targetTabIndex !== tapIndex) {
setTapIndex(targetTabIndex);
// 不需要调用reload因为tapIndex变化会触发reload
}
}, [props.searchParams?.statusFilter, tapIndex]); // 监听statusFilter变化
return (
<>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{
zIndex: 998,
borderBottom: '1px solid #e5e5e5'
}}
tabStyle={{
backgroundColor: '#ffffff'
// 注意:小程序不支持 boxShadow
}}
value={tapIndex}
onChange={(paneKey) => {
console.log('Tab切换:', paneKey, '类型:', typeof paneKey);
const newTapIndex = Number(paneKey);
setTapIndex(newTapIndex);
// 通知父组件更新 searchParams.statusFilter
const currentTab = tabs.find(tab => tab.index === newTapIndex);
if (currentTab && props.onSearchParamsChange) {
const newSearchParams = {
...props.searchParams,
statusFilter: currentTab.statusFilter
};
console.log('通知父组件更新searchParams:', newSearchParams);
props.onSearchParamsChange(newSearchParams);
}
}}
>
{
tabs?.map((item, _) => {
return (
<TabPane
key={item.index}
title={loading && tapIndex === item.index ? `${item.title}...` : item.title}
></TabPane>
)
})
}
</Tabs>
<View style={getInfiniteUlStyle(props.showSearch)} id="scroll">
{error ? (
<View className="flex flex-col items-center justify-center h-64">
<View className="text-gray-500 mb-4">{error}</View>
<Button
size="small"
type="primary"
onClick={() => reload(true)}
>
</Button>
</View>
) : (
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
list.length === 0 ? (
<Empty style={{backgroundColor: 'transparent'}} description="您还没有订单哦"/>
) : (
<View className={'h-24'}>
</View>
)
}
>
{/* 订单列表 */}
{list.length > 0 && list
?.filter((item) => {
// 如果是待付款标签页tapIndex === 1过滤掉支付已过期的订单
if (tapIndex === 1 && !item.payStatus && item.orderStatus !== 2 && item.createTime) {
return !isPaymentExpired(item.createTime);
}
return true;
})
?.map((item, index) => {
return (
<Cell key={index} style={{padding: '16px'}}
onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}>
<View className={'order-no flex justify-between'}>
<View className={'flex items-center'}>
<Text className={'text-gray-600 font-bold text-sm'}
onClick={(e) => {
e.stopPropagation();
copyText(`${item.orderNo}`)
}}>{item.orderNo}</Text>
</View>
{/* 右侧显示合并的状态和倒计时 */}
<View className={`${getOrderStatusColor(item)} font-medium`}>
{!item.payStatus && item.orderStatus !== 2 ? (
<PaymentCountdown
createTime={item.createTime}
payStatus={item.payStatus}
realTime={false}
showSeconds={false}
mode={'badge'}
/>
) : (
getOrderStatusText(item)
)}
</View>
</View>
<View
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</View>
{/* 商品信息 */}
<View className={'goods-info'}>
{item.orderGoods && item.orderGoods.length > 0 ? (
item.orderGoods.map((goods, goodsIndex) => (
<View key={goodsIndex} className={'flex items-center mb-2'}>
<Image
src={goods.image || '/default-goods.png'}
width="50"
height="50"
lazyLoad={false}
className={'rounded'}
/>
<View className={'ml-2 flex flex-col flex-1'}>
<Text className={'text-sm font-bold'}>{goods.goodsName}</Text>
{goods.spec && <Text className={'text-gray-500 text-xs'}>{goods.spec}</Text>}
<Text className={'text-gray-500 text-xs'}>{goods.totalNum}</Text>
</View>
<Text className={'text-sm'}>{goods.price}</Text>
</View>
))
) : (
<View className={'flex items-center'}>
<Avatar
src='/default-goods.png'
size={'50'}
shape={'square'}
/>
<View className={'ml-2'}>
<Text className={'text-sm'}>{item.title || '订单商品'}</Text>
<Text className={'text-gray-400 text-xs'}>{item.totalNum}</Text>
</View>
</View>
)}
</View>
<Text className={'w-full text-right'}>{item.payPrice}</Text>
{/* 操作按钮 */}
<Space className={'btn flex justify-end'}>
{/* 待付款状态:显示取消订单和立即支付 */}
{(!item.payStatus) && item.orderStatus !== 2 && (
<Space>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
void cancelOrder(item);
}}></Button>
{item.showPayButton && (
<Button size={'small'} type="primary" onClick={(e) => {
e.stopPropagation();
void payOrder(item);
}}></Button>
)}
</Space>
)}
{/* 待发货状态:显示申请退款 */}
{/*{item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && (*/}
{/* <Button size={'small'} onClick={(e) => {*/}
{/* e.stopPropagation();*/}
{/* applyRefund(item);*/}
{/* }}>申请退款</Button>*/}
{/*)}*/}
{/* 待收货状态:显示查看物流和确认收货 */}
{item.deliveryStatus === 20 && item.orderStatus !== 2 && (
<Space>
{/*<Button size={'small'} onClick={(e) => {*/}
{/* e.stopPropagation();*/}
{/* viewLogistics(item);*/}
{/*}}>查看物流</Button>*/}
<Button size={'small'} type="primary" onClick={(e) => {
e.stopPropagation();
confirmReceive(item);
}}></Button>
</Space>
)}
{/* 已完成状态:显示再次购买、评价商品、申请退款 */}
{item.orderStatus === 1 && (
<Space>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
buyAgain(item);
}}></Button>
{/*<Button size={'small'} onClick={(e) => {*/}
{/* e.stopPropagation();*/}
{/* evaluateGoods(item);*/}
{/*}}>评价商品</Button>*/}
{/*<Button size={'small'} onClick={(e) => {*/}
{/* e.stopPropagation();*/}
{/* applyRefund(item);*/}
{/*}}>申请退款</Button>*/}
</Space>
)}
{/* 退款/售后状态:显示查看进度和撤销申请 */}
{(item.orderStatus === 4 || item.orderStatus === 7) && (
<Space>
{/*<Button size={'small'} onClick={(e) => {*/}
{/* e.stopPropagation();*/}
{/* viewProgress(item);*/}
{/*}}>查看进度</Button>*/}
</Space>
)}
{/* 退款成功状态:显示再次购买 */}
{item.orderStatus === 6 && (
<Button size={'small'} type="primary" onClick={(e) => {
e.stopPropagation();
buyAgain(item);
}}></Button>
)}
</Space>
</Space>
</Cell>
)
})}
</InfiniteLoading>
)}
</View>
{/* 取消订单确认对话框 */}
<Dialog
title="确认取消"
visible={cancelDialogVisible}
confirmText="确认取消"
cancelText="我再想想"
onConfirm={handleConfirmCancel}
onCancel={handleCancelDialog}
>
</Dialog>
{/* 确认收货确认对话框 */}
<Dialog
title="确认收货"
visible={confirmReceiveDialogVisible}
confirmText="确认收货"
cancelText="我再想想"
onConfirm={handleConfirmReceive}
onCancel={handleCancelReceiveDialog}
>
</Dialog>
</>
)
}
export default OrderList

View File

@@ -0,0 +1,6 @@
export default {
navigationBarTitleText: '确认处方订单',
navigationBarBackgroundColor: '#fff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5'
}

View File

@@ -0,0 +1,286 @@
.doctor-order-confirm {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 80px;
// 页面提示
.confirm-tip {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #e5e7eb;
.tip-text {
font-size: 14px;
color: #059669;
font-weight: 500;
}
}
// 加载状态
.order-confirm-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 14px;
color: #999;
}
// 分组样式
.section-group {
margin-bottom: 10px;
background: #fff;
.nut-cell-group__title {
padding: 12px 16px;
font-size: 15px;
font-weight: 600;
color: #1f2937;
}
}
// 患者信息
.patient-info {
display: flex;
gap: 16px;
padding: 4px 0;
.patient-detail {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
.patient-name {
display: flex;
align-items: center;
gap: 8px;
.name {
font-size: 16px;
font-weight: 600;
color: #111827;
}
}
.patient-phone {
font-size: 14px;
color: #6b7280;
}
.patient-extra {
display: flex;
gap: 16px;
font-size: 13px;
color: #9ca3af;
.age, .idcard {
font-size: 13px;
}
}
}
}
// 诊断信息
.diagnosis-content,
.treatment-content,
.decoction-content {
padding: 8px 0;
.content-text {
font-size: 14px;
color: #374151;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
}
// 处方信息
.prescription-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
.prescription-type {
font-size: 15px;
font-weight: 600;
color: #059669;
}
}
// 药品列表
.medicine-list {
padding: 8px 0;
.medicine-item {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
&:last-child {
border-bottom: none;
}
.medicine-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.medicine-name {
font-size: 15px;
font-weight: 500;
color: #111827;
}
.medicine-price {
font-size: 15px;
font-weight: 600;
color: #dc2626;
}
}
.medicine-sub {
display: flex;
justify-content: space-between;
align-items: center;
.medicine-spec {
font-size: 13px;
color: #6b7280;
}
.medicine-subtotal {
font-size: 13px;
color: #9ca3af;
}
}
}
}
// 图片画廊
.image-gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 8px 0;
.image-item {
position: relative;
width: 100%;
padding-bottom: 100%; // 1:1 aspect ratio
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
img, image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
// 费用明细
.price-text {
font-size: 15px;
font-weight: 500;
color: #111827;
}
.total-price-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
.total-label {
font-size: 15px;
font-weight: 600;
color: #111827;
}
.total-amount {
font-size: 20px;
font-weight: 700;
color: #dc2626;
}
}
// 温馨提示
.warm-tips {
margin: 12px 16px;
padding: 16px;
background: #fffbeb;
border-radius: 8px;
border: 1px solid #fef3c7;
.tips-title {
display: block;
font-size: 14px;
font-weight: 600;
color: #d97706;
margin-bottom: 8px;
}
.tips-item {
display: block;
font-size: 13px;
color: #92400e;
line-height: 1.8;
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
}
}
// 底部操作栏
.fixed-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-top: 1px solid #e5e7eb;
padding: 12px 16px 24px;
z-index: 999;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
.bottom-price {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 12px;
.price-label {
font-size: 14px;
color: #6b7280;
}
.price-value {
font-size: 22px;
font-weight: 700;
color: #dc2626;
margin-left: 4px;
}
}
.bottom-actions {
display: flex;
gap: 12px;
.nut-button {
flex: 1;
}
}
}
}

View File

@@ -0,0 +1,410 @@
import {useEffect, useState} from "react";
import {
Button,
Cell,
CellGroup,
Avatar,
Tag,
Image,
Space
} from '@nutui/nutui-react-taro'
import {Edit} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import FixedButton from "@/components/FixedButton";
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {addClinicPrescription} from "@/api/clinic/clinicPrescription";
import {addClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem";
import './confirm.scss'
// 订单数据接口
interface OrderData {
patient: ClinicPatientUser;
prescription?: ClinicPrescription;
diagnosis: string;
treatmentPlan: string;
decoctionInstructions?: string;
images?: Array<{
url: string;
name?: string;
uid?: string;
}>;
orderPrice?: string;
}
const DoctorOrderConfirm = () => {
const [orderData, setOrderData] = useState<OrderData | null>(null)
const [loading, setLoading] = useState<boolean>(false)
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
// 计算药品总价
const getMedicinePrice = () => {
if (!orderData?.prescription?.items) return '0.00'
const total = orderData.prescription.items.reduce((sum, item) => {
const price = parseFloat(item.unitPrice || '0')
const quantity = item.quantity || 1
return sum + (price * quantity)
}, 0)
return total.toFixed(2)
}
// 计算服务费(可根据实际业务调整)
const getServiceFee = () => {
return '10.00' // 固定服务费10元
}
// 计算订单总价
const getTotalPrice = () => {
const medicinePrice = parseFloat(getMedicinePrice())
const serviceFee = parseFloat(getServiceFee())
return (medicinePrice + serviceFee).toFixed(2)
}
// 获取处方类型文本
const getPrescriptionType = () => {
if (!orderData?.prescription) return ''
return orderData.prescription.prescriptionType === 0 ? '中药' : '西药'
}
// 返回编辑
const handleBack = () => {
Taro.navigateBack()
}
// 确认并发送订单
const handleConfirmOrder = async () => {
if (!orderData) {
Taro.showToast({
title: '订单数据缺失',
icon: 'error'
})
return
}
try {
setSubmitLoading(true)
const doctorId = Taro.getStorageSync('UserId') // 当前医生ID
// 第一步:创建处方主表记录
console.log('开始创建处方记录...')
const prescriptionData: ClinicPrescription = {
userId: orderData.patient.userId,
doctorId: doctorId,
prescriptionType: orderData.prescription?.prescriptionType || 0, // 处方类型
diagnosis: orderData.diagnosis, // 诊断结果
treatmentPlan: orderData.treatmentPlan, // 治疗方案
decoctionInstructions: orderData.decoctionInstructions, // 煎药说明
image: orderData.images ? JSON.stringify(orderData.images) : '', // 病例图片
orderPrice: getTotalPrice(), // 订单总金额
price: getMedicinePrice(), // 药品单价
payPrice: getTotalPrice(), // 实付金额
status: 0, // 状态0正常
isInvalid: 0, // 未失效
isSettled: 0, // 未结算
comments: `患者:${orderData.patient.realName},年龄:${orderData.patient.age}`
}
const createdPrescription = await addClinicPrescription(prescriptionData)
console.log('处方创建成功:', createdPrescription)
if (!createdPrescription || !createdPrescription.id) {
throw new Error('处方创建失败未返回处方ID')
}
const prescriptionId = createdPrescription.id
// 第二步:创建处方明细记录(药品列表)
if (orderData.prescription?.items && orderData.prescription.items.length > 0) {
console.log('开始创建处方明细...')
for (const item of orderData.prescription.items) {
const prescriptionItemData = {
prescriptionId: prescriptionId, // 关联处方ID
prescriptionNo: createdPrescription.orderNo, // 处方编号
medicineId: item.medicineId,
medicineName: item.medicineName,
specification: item.specification,
dosage: item.dosage,
usageFrequency: item.usageFrequency,
days: item.days,
amount: item.amount,
unitPrice: item.unitPrice,
quantity: item.quantity,
userId: orderData.patient.userId,
comments: item.comments
}
await addClinicPrescriptionItem(prescriptionItemData)
}
console.log('处方明细创建成功')
}
console.log('处方创建完成处方ID:', prescriptionId)
// 清除临时数据
Taro.removeStorageSync('tempOrderData')
Taro.showToast({
title: '处方已发送给患者',
icon: 'success',
duration: 2000
})
setTimeout(() => {
// 跳转到订单列表
Taro.redirectTo({
url: '/clinic/clinicPrescription/index'
})
}, 2000)
} catch (error: any) {
console.error('创建处方/订单失败:', error)
Taro.showToast({
title: error.message || '发送失败,请重试',
icon: 'error'
})
} finally {
setSubmitLoading(false)
}
}
useEffect(() => {
try {
setLoading(true)
// 从本地存储获取订单数据
const tempData = Taro.getStorageSync('tempOrderData')
if (!tempData) {
Taro.showToast({
title: '订单数据缺失,请重新填写',
icon: 'error'
})
setTimeout(() => {
Taro.navigateBack()
}, 1500)
return
}
const parsedData = JSON.parse(tempData)
console.log('订单确认页获取数据:', parsedData)
setOrderData(parsedData)
} catch (error) {
console.error('解析订单数据失败:', error)
Taro.showToast({
title: '数据解析失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [])
if (loading || !orderData) {
return (
<View className="order-confirm-loading">
<Text>...</Text>
</View>
)
}
return (
<>
{/* 页面标题提示 */}
{/* 患者信息 */}
<CellGroup>
<View className={'p-3'}></View>
<Cell>
<Space>
<Avatar
src={orderData.patient.avatar}
size="large"
/>
<View className="flex flex-col gap-1">
<Text className="font-medium">{orderData.patient.realName}</Text>
<Text className="text-gray-500">{orderData.patient.phone}</Text>
<Space className="patient-extra">
<Text className="text-gray-500">{orderData.patient.age}</Text>
</Space>
</View>
</Space>
</Cell>
</CellGroup>
{/* 诊断信息 */}
<CellGroup>
<View className={'p-3'}></View>
<Cell
extra={
<Button
size="small"
icon={<Edit size="12"/>}
onClick={handleBack}
>
</Button>
}
>
<View className="text-gray-500">
<Text>{orderData.diagnosis}</Text>
</View>
</Cell>
<View className={'p-3'}></View>
<Cell>
<View className={'text-gray-500'}>
<Text>{orderData.treatmentPlan}</Text>
</View>
</Cell>
</CellGroup>
{/* 处方信息 */}
{orderData.prescription && (
<CellGroup>
<View className={'p-3'}></View>
<Cell>
<Space>
<Text className="text-gray-500">
RP: {getPrescriptionType()}
</Text>
<Tag type="success">
{orderData.prescription.items?.length || 0}
</Tag>
</Space>
</Cell>
{/* 药品列表 */}
{orderData.prescription.items?.map((item, index) => (
<Cell key={index}>
<View className={'flex justify-between w-full text-gray-500'}>
<Space>
<Text className="medicine-name">{item.medicineName}</Text>
<Text className="medicine-price">¥{item.unitPrice}</Text>
{!!item.specification && (
<Text className="medicine-spec">
{item.specification || '规格未知'} × {item.quantity || 1}
</Text>
)}
</Space>
<View className="medicine-sub">
<Text className="medicine-subtotal">
¥{(parseFloat(item.unitPrice || '0') * (item.quantity || 1)).toFixed(2)}
</Text>
</View>
</View>
</Cell>
))}
{/* 煎药说明 */}
{orderData.decoctionInstructions && (
<>
<View className={'p-3'}></View>
<Cell>
<Text className={'text-gray-500'}>{orderData.decoctionInstructions}</Text>
</Cell>
</>
)}
</CellGroup>
)}
{/* 上传的图片 */}
{orderData.images && orderData.images.length > 0 && (
<CellGroup title="病例图片" className="section-group">
<Cell>
<View className="image-gallery">
{orderData.images.map((image, index) => (
<View
key={image.uid || index}
className="image-item"
onClick={() => {
// 预览图片
Taro.previewImage({
urls: orderData.images!.map(img => img.url),
current: image.url
})
}}
>
<Image
src={image.url}
mode="aspectFill"
width="100%"
height="100%"
/>
</View>
))}
</View>
</Cell>
</CellGroup>
)}
{/* 费用明细 */}
<CellGroup>
<View className={'p-3'}></View>
<Cell
title={<Text className="text-gray-500"></Text>}
extra={<Text className="price-text">¥{getMedicinePrice()}</Text>}
/>
<Cell
title={<Text className="text-gray-500"></Text>}
extra={<Text className="price-text">¥{getServiceFee()}</Text>}
/>
<Cell extra={
(
<Space className="total-price-row">
<Text className="total-label"></Text>
<Text className="total-amount">¥{getTotalPrice()}</Text>
</Space>
)
}>
</Cell>
</CellGroup>
{/* 温馨提示 */}
<CellGroup>
<View className={'p-3'}>📌 </View>
<View className={'flex flex-col px-3 pb-5 text-gray-500 text-sm'}>
<Text> </Text>
<Text> </Text>
<Text> </Text>
<Text> "修改"</Text>
</View>
</CellGroup>
{/* 底部操作按钮 */}
<FixedButton
text={submitLoading ? '发送中...' : '确认并发送给患者'}
icon={<Edit/>}
onClick={handleConfirmOrder}
/>
<View className="fixed-bottom-bar">
<View className="bottom-price">
<Text className="price-label"></Text>
<Text className="price-value">¥{getTotalPrice()}</Text>
</View>
<View className="bottom-actions">
<Button
size="large"
fill="outline"
onClick={handleBack}
disabled={submitLoading}
>
</Button>
<Button
type="primary"
size="large"
onClick={handleConfirmOrder}
loading={submitLoading}
disabled={submitLoading}
style={{flex: 1}}
>
{submitLoading ? '发送中...' : '确认并发送给患者'}
</Button>
</View>
</View>
</>
)
}
export default DoctorOrderConfirm

View File

@@ -0,0 +1,6 @@
export default {
navigationBarTitleText: '开方详情',
navigationBarBackgroundColor: '#fff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5'
}

View File

@@ -0,0 +1,190 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag} from '@nutui/nutui-react-taro'
import {Del, Edit} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {
pageClinicPrescription,
removeClinicPrescription
} from "@/api/clinic/clinicPrescription";
import FixedButton from "@/components/FixedButton";
import {copyText} from "@/utils/common";
const ClinicPrescriptionList = () => {
const [list, setList] = useState<ClinicPrescription[]>([])
const [loading, setLoading] = useState<boolean>(false)
const reload = () => {
setLoading(true)
pageClinicPrescription({
// 添加查询条件
doctorId: Taro.getStorageSync('UserId'),
})
.then(data => {
setList(data?.list || [])
})
.catch(() => {
Taro.showToast({
title: '获取数据失败',
icon: 'error'
});
})
.finally(() => {
setLoading(false)
})
}
const onDel = async (item: ClinicPrescription) => {
const res = await Taro.showModal({
title: '确认删除',
content: `确定要删除处方编号「${item.orderNo}」吗?`,
})
if (res.confirm) {
try {
await removeClinicPrescription(item.id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
} catch (error) {
Taro.showToast({
title: '删除失败',
icon: 'error'
});
}
}
}
const onEdit = (item: ClinicPrescription) => {
Taro.navigateTo({
url: `/clinic/clinicPrescription/add?id=${item.id}`
})
}
const getSexName = (sex?: number) => {
return sex === 0 ? '男' : sex === 1 ? '女' : ''
}
useDidShow(() => {
reload()
});
if (list.length === 0 && !loading) {
return (
<ConfigProvider>
<View className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="暂无处方数据"
/>
<Space style={{marginTop: '20px'}}>
<Button
type="primary"
onClick={() => Taro.navigateTo({url: '/clinic/clinicPrescription/add'})}
>
</Button>
</Space>
</View>
</ConfigProvider>
)
}
return (
<>
<View className="p-3">
{list.map((item) => (
<CellGroup key={item.id} className="mb-3">
<Cell
title={item.orderNo}
extra={
<Tag type={'warning'} className="font-medium"></Tag>
}
onClick={() => copyText(`${item.orderNo}`)}
/>
<Cell
title={'患者名称'}
extra={
<Space>
<Text className="font-medium">{item.realName}</Text>
<Text className="font-medium">{item.age}</Text>
<Text className="font-medium">{getSexName(item.sex)}</Text>
</Space>
}
/>
{/*<Cell*/}
{/* title="处方类型"*/}
{/* extra={*/}
{/* <Tag type="info">*/}
{/* {getPrescriptionTypeText(item.prescriptionType)}*/}
{/* </Tag>*/}
{/* }*/}
{/*/>*/}
{item.diagnosis && (
<Cell
title="诊断结果"
extra={
<Text className="text-gray-600 text-sm">
{item.diagnosis.length > 20
? `${item.diagnosis.substring(0, 20)}...`
: item.diagnosis}
</Text>
}
/>
)}
<Cell
title="订单金额"
extra={
<Text className="text-red-500 font-medium">
¥{item.orderPrice || '0.00'}
</Text>
}
/>
<Cell
title="创建时间"
extra={
<Text className="text-gray-500 text-xs">
{item.createTime}
</Text>
}
/>
<Cell>
<Space className="w-full justify-end">
<Button
size="small"
type="primary"
icon={<Edit/>}
onClick={() => onEdit(item)}
>
</Button>
<Button
size="small"
type="danger"
icon={<Del/>}
onClick={() => onDel(item)}
>
</Button>
</Space>
</Cell>
</CellGroup>
))}
</View>
<FixedButton
text="开处方"
onClick={() => Taro.navigateTo({url: '/clinic/clinicPrescription/add'})}
/>
</>
)
}
export default ClinicPrescriptionList;

View File

@@ -1,5 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '处方主表 navigationBarTitleText: '处方管理',
管理',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

View File

@@ -0,0 +1,72 @@
page {
background: linear-gradient(to bottom, #f3f3f3, #f9fafb);
background-size: 100%;
}
.search-container {
transition: all 0.3s ease;
.nut-input {
background-color: #f8f9fa !important;
border: 1px solid #e5e5e5 !important;
border-radius: 4px !important;
&:focus {
border-color: #007bff !important;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important;
}
}
.nut-button {
border-radius: 4px !important;
&--primary {
background: linear-gradient(135deg, #007bff, #0056b3) !important;
border: none !important;
}
&--small {
padding: 6px 12px !important;
font-size: 12px !important;
}
}
}
// Tabs样式优化
.nut-tabs {
.nut-tabs__titles {
background: #ffffff !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
.nut-tabs__titles-item {
font-size: 14px !important;
font-weight: 500 !important;
&--active {
color: #007bff !important;
font-weight: 600 !important;
}
}
.nut-tabs__line {
background: #007bff !important;
height: 3px !important;
}
}
}
// 筛选提示样式
.filter-tip {
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -1,65 +1,113 @@
import {useState} from "react"; import {useState, useCallback, useRef, useEffect} from "react";
import Taro, {useDidShow} from '@tarojs/taro' import {Space, Button, Input} from '@nutui/nutui-react-taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro' import {View} from '@tarojs/components';
import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro' import OrderList from "./components/OrderList";
import {View} from '@tarojs/components' import {useRouter} from '@tarojs/taro'
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import {ShopOrderParam} from "@/api/shop/shopOrder/model";
import {listClinicPrescription, removeClinicPrescription, updateClinicPrescription} from "@/api/clinic/clinicPrescription"; import './index.scss'
const ClinicPrescriptionList = () => { function ClinicPrescriptionList() {
const [list, setList] = useState<ClinicPrescription[]>([]) const {params} = useRouter();
const [searchParams, setSearchParams] = useState<ShopOrderParam>({
statusFilter: params.statusFilter != undefined && params.statusFilter != '' ? parseInt(params.statusFilter) : -1
})
const [showSearch] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('')
const searchTimeoutRef = useRef<NodeJS.Timeout>()
const reload = () => { const reload = async (where?: ShopOrderParam) => {
listClinicPrescription({ console.log(where,'where...')
// 添加查询条件 setSearchParams(prev => ({ ...prev, ...where }))
})
.then(data => {
setList(data || [])
})
.catch(() => {
Taro.showToast({
title: '获取数据失败',
icon: 'error'
});
})
} }
// 防抖搜索函数
const debouncedSearch = useCallback((keyword: string) => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
const onDel = async (id?: number) => { searchTimeoutRef.current = setTimeout(() => {
await removeClinicPrescription(id) if (keyword.trim()) {
Taro.showToast({ handleSearch({keywords: keyword.trim()});
title: '删除成功', } else {
icon: 'success' // 如果搜索关键词为空清除keywords参数
}); const newSearchParams = { ...searchParams };
reload(); delete newSearchParams.keywords;
} setSearchParams(newSearchParams);
reload(newSearchParams).then();
useDidShow(() => { }
reload() }, 500); // 500ms防抖延迟
}); }, [searchParams]);
if (list.length == 0) { // 处理搜索
return ( const handleSearch = (where: ShopOrderParam) => {
<ConfigProvider> // 合并搜索参数保留当前的statusFilter
<div className={'h-full flex flex-col justify-center items-center'} style={{ const newSearchParams = {
height: 'calc(100vh - 300px)', ...searchParams, // 保留当前的所有参数包括statusFilter
}}> ...where // 应用新的搜索条件
<Empty };
style={{ setSearchParams(newSearchParams)
backgroundColor: 'transparent' reload(newSearchParams).then()
}}
description="暂无数据"
/>
<Space>
<Button onClick={() => Taro.navigateTo({url: '/clinic/clinicPrescription/add'})}>
</Button>
</Space>
</div>
</ConfigProvider>
)
} }
useEffect(() => {
reload().then()
}, []);
return ( return (
<> <View className="bg-gray-50 min-h-screen">
{list.map((item, _) => ( {/* 搜索组件 */}
<Cell.Group key={item. {showSearch && (
<View className="bg-white p-3 shadow-sm border-b border-gray-100">
<View className="flex items-center">
<View className="flex-1 mr-2">
<Input
placeholder="搜索订单号、商品名称"
value={searchKeyword}
onChange={(value) => {
setSearchKeyword(value);
debouncedSearch(value); // 使用防抖搜索
}}
onConfirm={() => {
if (searchKeyword.trim()) {
handleSearch({keywords: searchKeyword.trim()});
}
}}
style={{
padding: '8px 12px',
border: '1px solid #e5e5e5',
borderRadius: '4px',
backgroundColor: '#f8f9fa'
}}
/>
</View>
<Space>
<Button
type="primary"
onClick={() => {
if (searchKeyword.trim()) {
handleSearch({keywords: searchKeyword.trim()});
}
}}
>
</Button>
</Space>
</View>
</View>
)}
{/*订单列表*/}
<OrderList
onReload={() => reload(searchParams)}
searchParams={searchParams}
showSearch={showSearch}
onSearchParamsChange={(newParams) => {
console.log('父组件接收到searchParams变化:', newParams);
setSearchParams(newParams);
}}
/>
</View>
);
}
export default ClinicPrescriptionList;

View File

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

View File

@@ -0,0 +1,285 @@
import {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, SearchBar, Button} from '@nutui/nutui-react-taro'
import {Phone} from '@nutui/icons-react-taro'
import type {ClinicPatientUser as PatientUserType} from "@/api/clinic/clinicPatientUser/model";
import {
pageClinicPatientUser
} from "@/api/clinic/clinicPatientUser";
// 患者类型
interface PatientUser extends PatientUserType {
}
const SelectPatient = () => {
const [list, setList] = useState<PatientUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 复制手机号
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 fetchPatientData = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数
const params: any = {
page: currentPage
};
// 添加搜索关键词
if (displaySearchValue.trim()) {
params.keywords = displaySearchValue.trim();
}
const res = await pageClinicPatientUser(params);
if (res?.list && res.list.length > 0) {
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(res.list);
} else {
setList(prevList => [...prevList, ...res.list]);
}
// 正确判断是否还有更多数据
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, displaySearchValue]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchPatientData(false, nextPage);
}
// 防抖搜索功能
useEffect(() => {
const timer = setTimeout(() => {
setDisplaySearchValue(searchValue);
}, 300); // 300ms 防抖
return () => clearTimeout(timer);
}, [searchValue]);
// 初始化数据
useEffect(() => {
fetchPatientData(true).then();
}, [displaySearchValue]);
// 监听页面显示,当从其他页面返回时刷新数据
useDidShow(() => {
// 刷新数据
setList([]);
setPage(1);
setHasMore(true);
fetchPatientData(true);
});
// 选择患者
const selectPatient = (patient: PatientUser) => {
// 将选中的患者信息传递回上一个页面
const pages = Taro.getCurrentPages();
if (pages.length > 1) {
const prevPage = pages[pages.length - 2];
// @ts-ignore
if (prevPage && typeof prevPage.setSelectedPatient === 'function') {
// @ts-ignore
prevPage.setSelectedPatient(patient);
}
}
// 同时存储到本地存储,作为备选方案
try {
Taro.setStorageSync('selectedPatient', JSON.stringify(patient));
} catch (e) {
console.error('存储患者信息失败:', e);
}
Taro.navigateBack();
};
// 渲染患者项
const renderPatientItem = (patient: PatientUser) => (
<View key={patient.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center mb-3">
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<Text className="font-semibold text-gray-800 mr-2">
{patient.realName || '未命名'}
</Text>
</View>
<View className="flex items-center mb-1">
<Space direction="vertical">
<View className="flex items-center">
<Text className="text-xs text-gray-500" onClick={(e) => {
e.stopPropagation();
makePhoneCall(patient.phone || '');
}}>{patient.phone || '未提供'}</Text>
<View className="flex items-center ml-2">
<Phone
size={12}
className="text-green-500 mr-2"
onClick={(e) => {
e.stopPropagation();
makePhoneCall(patient.phone || '');
}}
/>
<Text
className="text-xs text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
copyPhone(patient.phone || '');
}}
>
</Text>
</View>
</View>
<Text className="text-xs text-gray-500">
{patient.createTime || '未知'}
</Text>
</Space>
</View>
{/* 显示 comments 字段 */}
<View className="flex items-center">
<Text className="text-xs text-gray-500">{patient.comments || '暂无'}</Text>
</View>
</View>
</View>
{/* 选择按钮 */}
<Button
size="small"
onClick={() => selectPatient(patient)}
style={{backgroundColor: '#1890ff', color: 'white'}}
>
</Button>
</View>
);
// 渲染患者列表
const renderPatientList = () => {
const isSearching = displaySearchValue.trim().length > 0;
return (
<View className="flex-1">
{/* 搜索结果统计 */}
{isSearching && (
<View className="bg-white px-4 py-2 border-b border-gray-100">
<Text className="text-sm text-gray-600">
"{displaySearchValue}" {list.length}
</Text>
</View>
)}
<View className="p-4" style={{
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
loadingText="加载中..."
loadMoreText={
list.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无患者数据"}
/>
) : (
<View className={'h-3 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && list.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
list.map(renderPatientItem)
)}
</InfiniteLoading>
</View>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 搜索栏 */}
<View className="bg-white py-2 border-b border-gray-100">
<SearchBar
value={searchValue}
placeholder="搜索患者姓名、手机号"
onChange={(value) => setSearchValue(value)}
onClear={() => {
setSearchValue('');
setDisplaySearchValue('');
}}
clearable
/>
</View>
{/* 患者列表 */}
{renderPatientList()}
</View>
);
};
export default SelectPatient;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '选择处方',
navigationBarTextStyle: 'black'
})

View File

@@ -0,0 +1,237 @@
import {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, SearchBar, Button} from '@nutui/nutui-react-taro'
import {
pageClinicPrescription
} from "@/api/clinic/clinicPrescription";
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
const SelectPrescription = () => {
const [list, setList] = useState<ClinicPrescription[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 获取处方数据
const fetchPrescriptionData = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数
const params: any = {
page: currentPage
};
// 添加搜索关键词
if (displaySearchValue.trim()) {
params.keywords = displaySearchValue.trim();
}
const res = await pageClinicPrescription(params);
if (res?.list && res.list.length > 0) {
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(res.list);
} else {
setList(prevList => [...prevList, ...res.list]);
}
// 正确判断是否还有更多数据
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, displaySearchValue]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchPrescriptionData(false, nextPage);
}
// 防抖搜索功能
useEffect(() => {
const timer = setTimeout(() => {
setDisplaySearchValue(searchValue);
}, 300); // 300ms 防抖
return () => clearTimeout(timer);
}, [searchValue]);
// 初始化数据
useEffect(() => {
fetchPrescriptionData(true).then();
}, [displaySearchValue]);
// 监听页面显示,当从其他页面返回时刷新数据
useDidShow(() => {
// 刷新数据
setList([]);
setPage(1);
setHasMore(true);
fetchPrescriptionData(true);
});
// 选择处方
const selectPrescription = (prescription: ClinicPrescription) => {
// 将选中的处方信息传递回上一个页面
const pages = Taro.getCurrentPages();
if (pages.length > 1) {
const prevPage = pages[pages.length - 2];
// @ts-ignore
if (prevPage && typeof prevPage.setSelectedPrescription === 'function') {
// @ts-ignore
prevPage.setSelectedPrescription(prescription);
}
}
// 同时存储到处方存储,作为备选方案
try {
Taro.setStorageSync('selectedPrescription', JSON.stringify(prescription));
} catch (e) {
console.error('存储处方信息失败:', e);
}
Taro.navigateBack();
};
// 渲染处方项
const renderPrescriptionItem = (prescription: ClinicPrescription) => (
<View key={prescription.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center mb-3">
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<Text className="font-semibold text-gray-800 mr-2">
: {prescription.orderNo || '无编号'}
</Text>
</View>
<View className="flex items-center mb-1">
<Space direction="vertical">
<Text className="text-xs text-gray-500">
: {prescription.treatmentPlan}
</Text>
<Text className="text-xs text-gray-500">
: {prescription.prescriptionType === 0 ? '中药' : prescription.prescriptionType === 1 ? '西药' : '未知'}
</Text>
{/*<Text className="text-xs text-gray-500">*/}
{/* 诊断结果: {prescription.diagnosis || '无'}*/}
{/*</Text>*/}
<Text className="text-xs text-gray-500">
: {prescription.createTime || '未知'}
</Text>
</Space>
</View>
{/* 显示备注字段 */}
<View className="flex items-center">
<Text className="text-xs text-gray-500">: {prescription.comments || '暂无'}</Text>
</View>
</View>
</View>
{/* 选择按钮 */}
<Button
size="small"
onClick={() => selectPrescription(prescription)}
style={{backgroundColor: '#1890ff', color: 'white'}}
>
</Button>
</View>
);
// 渲染处方列表
const renderPrescriptionList = () => {
const isSearching = displaySearchValue.trim().length > 0;
return (
<View className="flex-1">
{/* 搜索结果统计 */}
{isSearching && (
<View className="bg-white px-4 py-2 border-b border-gray-100">
<Text className="text-sm text-gray-600">
"{displaySearchValue}" {list.length}
</Text>
</View>
)}
<View className="p-4" style={{
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
loadingText="加载中..."
loadMoreText={
list.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无处方数据"}
/>
) : (
<View className={'h-3 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && list.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
list.map(renderPrescriptionItem)
)}
</InfiniteLoading>
</View>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 搜索栏 */}
<View className="bg-white py-2 border-b border-gray-100">
<SearchBar
value={searchValue}
placeholder="搜索处方编号、诊断结果"
onChange={(value) => setSearchValue(value)}
onClear={() => {
setSearchValue('');
setDisplaySearchValue('');
}}
clearable
/>
</View>
{/* 处方列表 */}
{renderPrescriptionList()}
</View>
);
};
export default SelectPrescription;

View File

@@ -147,7 +147,7 @@ const DealerIndex: React.FC = () => {
</View> </View>
</Grid.Item> </Grid.Item>
<Grid.Item text={'在线开方'} onClick={() => navigateToPage('/doctor/orders/add')}> <Grid.Item text={'在线开方'} onClick={() => navigateToPage('/clinic/clinicPrescription/add')}>
<View className="text-center"> <View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2"> <View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Edit color="#10b981" size="20"/> <Edit color="#10b981" size="20"/>
@@ -163,7 +163,7 @@ const DealerIndex: React.FC = () => {
</View> </View>
</Grid.Item> </Grid.Item>
<Grid.Item text={'处方管理'} onClick={() => navigateToPage('/doctor/orders/index')}> <Grid.Item text={'处方管理'} onClick={() => navigateToPage('/clinic/clinicPrescription/index')}>
<View className="text-center"> <View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2"> <View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Orderlist color="#f59e0b" size="20"/> <Orderlist color="#f59e0b" size="20"/>

View File

@@ -56,7 +56,7 @@ const AddPatient = () => {
} }
// 验证手机号格式 // 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/; const phoneRegex = /^1[2-9]\d{9}$/;
if (!phoneRegex.test(values.phone)) { if (!phoneRegex.test(values.phone)) {
Taro.showToast({ Taro.showToast({
title: '请填写正确的手机号', title: '请填写正确的手机号',

View File

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

134
src/doctor/orders/add.scss Normal file
View File

@@ -0,0 +1,134 @@
.usage-card {
margin: 12px 16px;
margin-top: 4px;
padding: 16px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 12px 32px rgba(54, 87, 142, 0.08);
}
.usage-card__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.usage-card__icon {
width: 32px;
height: 32px;
border-radius: 10px;
background: linear-gradient(135deg, #fceecd, #ffd886);
display: flex;
align-items: center;
justify-content: center;
}
.usage-card__icon-text {
font-size: 16px;
color: #8c5a00;
font-weight: 600;
}
.usage-card__title {
font-size: 16px;
font-weight: 600;
color: #1c1c1e;
}
.usage-card__dose-row {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.usage-card__label {
font-size: 14px;
color: #7c7c7c;
}
.usage-card__dose-value {
display: flex;
align-items: baseline;
gap: 4px;
}
.usage-card__dose-value .nut-input {
width: 72px;
background: transparent;
}
.usage-card__dose-value .nut-input input {
text-align: center;
font-size: 18px;
color: #1c1c1e;
}
.usage-card__grid-item--picker {
cursor: pointer;
}
.usage-card__dose-number {
font-size: 20px;
font-weight: 600;
color: #1c1c1e;
}
.usage-card__dose-unit {
font-size: 12px;
color: #a1a1a1;
}
.usage-card__grid {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.usage-card__grid-item {
flex: 1;
background: #f8f9fb;
border-radius: 12px;
padding: 10px 12px;
}
.usage-card__grid-label {
font-size: 12px;
color: #9aa1b2;
}
.usage-card__grid-value {
margin-top: 6px;
display: flex;
align-items: center;
justify-content: space-between;
}
.usage-card__grid-text {
font-size: 14px;
color: #1d1d1f;
font-weight: 500;
}
.usage-card__desc {
font-size: 12px;
color: #8c8c8c;
margin-bottom: 10px;
}
.usage-card__hint {
font-size: 12px;
color: #4b4b4d;
background: #f5f5f5;
border-radius: 12px;
padding: 12px;
margin-bottom: 8px;
}
.usage-card__hint--muted {
color: #9ba0ab;
background: #f0f2f5;
}

View File

@@ -8,9 +8,10 @@ import {
Avatar, Avatar,
Input, Input,
Space, Space,
TextArea TextArea,
Picker as NutPicker
} from '@nutui/nutui-react-taro' } from '@nutui/nutui-react-taro'
import {ArrowRight} from '@nutui/icons-react-taro' import {ArrowRight, ArrowDown} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import FixedButton from "@/components/FixedButton"; import FixedButton from "@/components/FixedButton";
@@ -18,8 +19,8 @@ import navTo from "@/utils/common";
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model"; import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {TenantId} from "@/config/app"; import {TenantId} from "@/config/app";
import {getClinicPatientUser} from "@/api/clinic/clinicPatientUser"; import {clinicPatientUserByPatientUserId} from "@/api/clinic/clinicPatientUser";
import {addClinicOrder} from "@/api/clinic/clinicOrder"; import './add.scss'
// 图片数据接口 // 图片数据接口
interface UploadedImageData { interface UploadedImageData {
@@ -31,6 +32,215 @@ interface UploadedImageData {
type?: string; type?: string;
} }
const frequencyOptions = ['一次', '两次', '三次', '四次', '五次']
const perDoseOptions = Array.from({length: 20}, (_, index) => `${(index + 1) * 5}g`)
const getFrequencyIndexFromText = (text?: string) => {
if (!text) {
return 2
}
const numberMatch = text.match(/\d+/)
if (numberMatch) {
const num = Math.min(Math.max(parseInt(numberMatch[0], 10), 1), 5)
return num - 1
}
const chineseDigits = ['一', '二', '三', '四', '五']
const foundIndex = chineseDigits.findIndex(char => text.includes(char))
if (foundIndex !== -1) {
return foundIndex
}
return 2
}
const getPerDoseIndexFromText = (text?: string) => {
if (!text) {
return 0
}
const numberMatch = text.match(/\d+/)
if (!numberMatch) {
return 0
}
const num = Math.min(Math.max(parseInt(numberMatch[0], 10), 5), 100)
const normalized = Math.round(num / 5)
const index = Math.max(0, Math.min(perDoseOptions.length - 1, normalized - 1))
return index
}
interface UsageSummaryCardProps {
prescription: ClinicPrescription;
doseCount: string;
onDoseChange?: (value: string) => void;
frequencyIndex: number;
onFrequencyChange?: (value: number) => void;
perDoseIndex: number;
onPerDoseChange?: (value: number) => void;
}
const UsageSummaryCard = ({
prescription,
doseCount,
onDoseChange,
frequencyIndex,
onFrequencyChange,
perDoseIndex,
onPerDoseChange
}: UsageSummaryCardProps) => {
const firstItem = prescription.items?.[0]
const [showFrequencyPicker, setShowFrequencyPicker] = useState(false)
const [showPerDosePicker, setShowPerDosePicker] = useState(false)
const frequencyPickerOptions = frequencyOptions.map((label, index) => ({
text: label,
value: String(index)
}))
const perDosePickerOptions = perDoseOptions.map((label, index) => ({
text: label,
value: String(index)
}))
// const formatFrequency = (frequency?: string) => {
// if (!frequency) {
// return {label: '每日', value: '3次'}
// }
// if (frequency.includes('每日')) {
// return {label: '每日', value: frequency.replace('每日', '') || '1次'}
// }
// if (frequency.includes('每天')) {
// return {label: '每天', value: frequency.replace('每天', '') || '1次'}
// }
// return {label: '频率', value: frequency}
// }
const formatDosage = () => {
const dosageText = firstItem?.dosage || firstItem?.specification
if (!dosageText) {
return '5g'
}
return dosageText
}
const formatDuration = () => {
const totalDoses = Number(doseCount) || 0
const perDay = frequencyIndex + 1
if (totalDoses <= 0) {
return '0天'
}
const days = Math.max(1, Math.ceil(totalDoses / perDay))
return `${days}`
}
// const summaryDesc = firstItem?.comments || prescription.comments || '请根据患者情况调整剂量,严格按照医嘱服用。'
// const decoctionHint = prescription.decoctionInstructions || '设置用药时间、用药禁忌、医嘱等'
const handleDoseInputChange = (value: string) => {
const sanitized = value.replace(/[^0-9]/g, '')
onDoseChange?.(sanitized)
}
return (
<View className="usage-card">
<View className="usage-card__header">
<View className="usage-card__icon">
<Text className="usage-card__icon-text"></Text>
</View>
<Text className="usage-card__title"></Text>
</View>
<View className="usage-card__dose-row">
<Text className="usage-card__label"></Text>
<View className="usage-card__dose-value">
<Input
className="usage-card__dose-input"
value={doseCount}
type="number"
placeholder="填写"
style={{
minWidth: '60px',
backgroundColor: '#f8f9fb',
borderRadius: '8px',
padding: '2px 6px'
}}
onChange={(value) => handleDoseInputChange(value)}
/>
<Text className="usage-card__dose-unit"></Text>
</View>
</View>
<View className="usage-card__grid">
<View
className="usage-card__grid-item usage-card__grid-item--picker"
onClick={() => setShowFrequencyPicker(true)}
>
<Text className="usage-card__grid-label"></Text>
<View className="usage-card__grid-value">
<Text className="usage-card__grid-text">{frequencyOptions[frequencyIndex] || '一次'}</Text>
<ArrowDown size={10} color="#C1C1C1"/>
</View>
</View>
<View
className="usage-card__grid-item usage-card__grid-item--picker"
onClick={() => setShowPerDosePicker(true)}
>
<Text className="usage-card__grid-label"></Text>
<View className="usage-card__grid-value">
<Text className="usage-card__grid-text">{perDoseOptions[perDoseIndex] || formatDosage()}</Text>
<ArrowDown size={10} color="#C1C1C1"/>
</View>
</View>
<View className="usage-card__grid-item">
<Text className="usage-card__grid-label"></Text>
<View className="usage-card__grid-value">
<Text className="usage-card__grid-text">{formatDuration()}</Text>
</View>
</View>
</View>
{/*<View className="usage-card__desc">*/}
{/* <Text>{summaryDesc}</Text>*/}
{/*</View>*/}
{/*<View className="usage-card__hint">*/}
{/* <Text>{decoctionHint}</Text>*/}
{/*</View>*/}
{/*<View className="usage-card__hint usage-card__hint--muted">*/}
{/* <Text>请输入制作要求,该内容患者不可见</Text>*/}
{/*</View>*/}
<NutPicker
visible={showFrequencyPicker}
title="请选择每日次数"
options={frequencyPickerOptions}
value={[String(frequencyIndex)]}
defaultValue={[String(frequencyIndex)]}
onConfirm={(_, value) => {
const selected = Array.isArray(value) && value.length > 0 ? Number(value[0]) : 0
onFrequencyChange?.(selected)
setShowFrequencyPicker(false)
}}
onClose={() => setShowFrequencyPicker(false)}
onCancel={() => setShowFrequencyPicker(false)}
/>
<NutPicker
visible={showPerDosePicker}
title="请选择每次用量"
options={perDosePickerOptions}
value={[String(perDoseIndex)]}
defaultValue={[String(perDoseIndex)]}
onConfirm={(_, value) => {
const selected = Array.isArray(value) && value.length > 0 ? Number(value[0]) : 0
onPerDoseChange?.(selected)
setShowPerDosePicker(false)
}}
onClose={() => setShowPerDosePicker(false)}
onCancel={() => setShowPerDosePicker(false)}
/>
</View>
)
}
const AddClinicOrder = () => { const AddClinicOrder = () => {
const {params} = useRouter(); const {params} = useRouter();
const [toUser, setToUser] = useState<ClinicPatientUser>() const [toUser, setToUser] = useState<ClinicPatientUser>()
@@ -54,13 +264,17 @@ const AddClinicOrder = () => {
image: '' // 添加image字段 image: '' // 添加image字段
}) })
const [doseCount, setDoseCount] = useState<string>('1')
const [frequencyIndex, setFrequencyIndex] = useState<number>(2)
const [perDoseIndex, setPerDoseIndex] = useState<number>(0)
// 判断是编辑还是新增模式 // 判断是编辑还是新增模式
const isEditMode = !!params.id const isEditMode = !!params.id
const toUserId = params.id ? Number(params.id) : undefined const toUserId = params.id ? Number(params.id) : undefined
const reload = async () => { const reload = async () => {
if (toUserId) { if (toUserId) {
getClinicPatientUser(Number(toUserId)).then(data => { clinicPatientUserByPatientUserId(Number(toUserId)).then(data => {
setToUser(data) setToUser(data)
}) })
} }
@@ -321,6 +535,24 @@ const AddClinicOrder = () => {
} }
}, [isEditMode]); }, [isEditMode]);
useEffect(() => {
if (selectedPrescription) {
const firstItem = selectedPrescription.items?.[0]
const inferredDose = firstItem?.quantity || firstItem?.amount || firstItem?.days || selectedPrescription.items?.length
if (inferredDose) {
setDoseCount(String(inferredDose))
} else {
setDoseCount('1')
}
setFrequencyIndex(getFrequencyIndexFromText(firstItem?.usageFrequency))
setPerDoseIndex(getPerDoseIndexFromText(firstItem?.dosage || firstItem?.specification))
} else {
setDoseCount('1')
setFrequencyIndex(2)
setPerDoseIndex(0)
}
}, [selectedPrescription])
if (loading) { if (loading) {
return <Loading className={'px-2'}></Loading> return <Loading className={'px-2'}></Loading>
} }
@@ -480,7 +712,7 @@ const AddClinicOrder = () => {
onClick={() => navTo(`/doctor/orders/selectPrescription`, true)} onClick={() => navTo(`/doctor/orders/selectPrescription`, true)}
/> />
{/* 药方信息 */} {/* 药方信息 */}
{selectedPrescription && ( {selectedPrescription ? (
<> <>
<Cell extra={'药方信息'}> <Cell extra={'药方信息'}>
<View className={'flex flex-col'}> <View className={'flex flex-col'}>
@@ -493,6 +725,16 @@ const AddClinicOrder = () => {
</View> </View>
</Cell> </Cell>
<UsageSummaryCard
prescription={selectedPrescription}
doseCount={doseCount}
onDoseChange={setDoseCount}
frequencyIndex={frequencyIndex}
onFrequencyChange={setFrequencyIndex}
perDoseIndex={perDoseIndex}
onPerDoseChange={setPerDoseIndex}
/>
{/* 煎药说明 */} {/* 煎药说明 */}
<TextArea <TextArea
value={formData.decoctionInstructions} value={formData.decoctionInstructions}
@@ -502,7 +744,7 @@ const AddClinicOrder = () => {
maxLength={200} maxLength={200}
/> />
</> </>
)} ) : null}
{/* 底部浮动按钮 */} {/* 底部浮动按钮 */}
<FixedButton text={'下一步:确认订单信息'} onClick={() => formRef.current?.submit()}/> <FixedButton text={'下一步:确认订单信息'} onClick={() => formRef.current?.submit()}/>

View File

@@ -14,8 +14,11 @@ import Taro from '@tarojs/taro'
import FixedButton from "@/components/FixedButton"; import FixedButton from "@/components/FixedButton";
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model"; import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {addClinicOrder} from "@/api/clinic/clinicOrder"; import {addClinicPrescription} from "@/api/clinic/clinicPrescription";
import {batchAddClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem";
import './confirm.scss' import './confirm.scss'
import request from "@/utils/request";
import {ApiResult} from "@/api";
// 订单数据接口 // 订单数据接口
interface OrderData { interface OrderData {
@@ -84,51 +87,105 @@ const DoctorOrderConfirm = () => {
try { try {
setSubmitLoading(true) setSubmitLoading(true)
// 构建诊所订单数据 const doctorId = Taro.getStorageSync('UserId') // 当前医生ID
const clinicOrderData = {
userId: orderData.patient.userId, // 第一步:创建处方主表记录
doctorId: Taro.getStorageSync('UserId'), // 当前医生ID console.log('开始创建处方记录...')
type: 0, // 订单类型:诊所订单 const prescriptionData: ClinicPrescription = {
title: `${orderData.patient.realName}的处方订单`, userId: orderData.patient.patientUserId,
totalPrice: getTotalPrice(), doctorId: doctorId,
payPrice: getTotalPrice(), prescriptionType: orderData.prescription?.prescriptionType || 0, // 处方类型
buyerRemarks: orderData.diagnosis, diagnosis: orderData.diagnosis, // 诊断结果
merchantRemarks: orderData.treatmentPlan, treatmentPlan: orderData.treatmentPlan, // 治疗方案
comments: JSON.stringify({ decoctionInstructions: orderData.decoctionInstructions, // 煎药说明
diagnosis: orderData.diagnosis, image: orderData.images ? JSON.stringify(orderData.images) : '', // 病例图片
treatmentPlan: orderData.treatmentPlan, orderPrice: getTotalPrice(), // 订单总金额
decoctionInstructions: orderData.decoctionInstructions, price: getMedicinePrice(), // 药品单价
prescriptionId: orderData.prescription?.id, payPrice: getTotalPrice(), // 实付金额
images: orderData.images status: 0, // 状态0正常
}), isInvalid: 0, // 未失效
payStatus: '0', // 未付款 isSettled: 0, // 未结算
orderStatus: 0, // 待支付 comments: `患者:${orderData.patient.realName},年龄:${orderData.patient.age}`
deliveryStatus: 10, // 未发货
} }
console.log('提交订单数据:', clinicOrderData) const createdPrescription = await addClinicPrescription(prescriptionData)
console.log('处方创建成功:', createdPrescription)
// 调用API创建订单 if (!createdPrescription || !createdPrescription.id) {
await addClinicOrder(clinicOrderData) throw new Error('处方创建失败未返回处方ID')
}
const prescriptionId = createdPrescription.id
// 第二步:创建处方明细记录(药品列表)
if (orderData.prescription?.items && orderData.prescription.items.length > 0) {
console.log('开始创建处方明细...')
const list = []
for (const item of orderData.prescription.items) {
const prescriptionItemData = {
prescriptionId: prescriptionId, // 关联处方ID
prescriptionNo: createdPrescription.orderNo, // 处方编号
medicineId: item.medicineId,
medicineName: item.medicineName,
specification: item.specification,
dosage: item.dosage,
usageFrequency: item.usageFrequency,
days: item.days,
amount: item.amount,
unitPrice: item.unitPrice,
quantity: item.quantity,
userId: orderData.patient.userId,
comments: item.comments
}
list.push(prescriptionItemData)
}
await batchAddClinicPrescriptionItem(list)
console.log('处方明细创建成功')
const order: any = {
userId: orderData.patient.userId,
orderNo: createdPrescription.orderNo,
type: 3,
title: "药方",
totalPrice: getTotalPrice(),
goodsItems: []
}
// @ts-ignore
const orderGoodsList = []
for (const item of orderData.prescription.items) {
const orderGoods = {
goodsId: item.medicineId,
quantity: item.quantity,
}
orderGoodsList.push(orderGoods)
}
order.goodsItems = orderGoodsList
const res = await request.post<ApiResult<unknown>>('/shop/shop-order', order)
if (res.code !== 0) {
throw new Error(res.message || '创建商城订单失败')
}
}
console.log('处方创建完成处方ID:', prescriptionId)
// 清除临时数据 // 清除临时数据
Taro.removeStorageSync('tempOrderData') Taro.removeStorageSync('tempOrderData')
Taro.showToast({ Taro.showToast({
title: '处方已发送给患者', title: '处方已发送给患者',
icon: 'success', icon: 'none',
duration: 2000 duration: 2000
}) })
setTimeout(() => { setTimeout(() => {
// 跳转到订单列表 // 跳转到订单列表
Taro.redirectTo({ Taro.redirectTo({
url: '/doctor/orders/index' url: '/clinic/clinicPatientUser/prescription'
}) })
}, 2000) }, 2000)
} catch (error: any) { } catch (error: any) {
console.error('创建订单失败:', error) console.error('创建处方/订单失败:', error)
Taro.showToast({ Taro.showToast({
title: error.message || '发送失败,请重试', title: error.message || '发送失败,请重试',
icon: 'error' icon: 'error'
@@ -342,7 +399,7 @@ const DoctorOrderConfirm = () => {
{/* 底部操作按钮 */} {/* 底部操作按钮 */}
<FixedButton <FixedButton
text={submitLoading ? '发送中...' : '确认并发送给患者'} text={submitLoading ? '发送中...' : '确认并发送给患者'}
icon={<Edit />} icon={<Edit/>}
onClick={handleConfirmOrder} onClick={handleConfirmOrder}
/> />
<View className="fixed-bottom-bar"> <View className="fixed-bottom-bar">

View File

@@ -2,21 +2,15 @@ import React, {useState, useEffect, useCallback} from 'react'
import {View, Text, ScrollView} from '@tarojs/components' import {View, Text, ScrollView} from '@tarojs/components'
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro' import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
import {useDealerUser} from '@/hooks/useDealerUser' import {useDealerUser} from '@/hooks/useDealerUser'
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model' import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {pageClinicPrescription} from "@/api/clinic/clinicPrescription";
interface OrderWithDetails extends ShopDealerOrder {
orderNo?: string
customerName?: string
userCommission?: string
}
const DealerOrders: React.FC = () => { const DealerOrders: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false) const [refreshing, setRefreshing] = useState<boolean>(false)
const [loadingMore, setLoadingMore] = useState<boolean>(false) const [loadingMore, setLoadingMore] = useState<boolean>(false)
const [orders, setOrders] = useState<OrderWithDetails[]>([]) const [orders, setOrders] = useState<ClinicPrescription[]>([])
const [currentPage, setCurrentPage] = useState<number>(1) const [currentPage, setCurrentPage] = useState<number>(1)
const [hasMore, setHasMore] = useState<boolean>(true) const [hasMore, setHasMore] = useState<boolean>(true)
@@ -24,8 +18,7 @@ const DealerOrders: React.FC = () => {
// 获取订单数据 // 获取订单数据
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => { const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
if (!dealerUser?.userId) return console.log('获取订单数据')
try { try {
if (isRefresh) { if (isRefresh) {
setRefreshing(true) setRefreshing(true)
@@ -35,17 +28,14 @@ const DealerOrders: React.FC = () => {
setLoadingMore(true) setLoadingMore(true)
} }
const result = await pageShopDealerOrder({ const result = await pageClinicPrescription({
page, page,
limit: 10 limit: 10
}) })
if (result?.list) { if (result?.list) {
const newOrders = result.list.map(order => ({ const newOrders = result.list.map(order => ({
...order, ...order
orderNo: `${order.orderId}`,
customerName: `用户${order.userId}`,
userCommission: order.firstMoney || '0.00'
})) }))
if (page === 1) { if (page === 1) {
@@ -102,7 +92,7 @@ const DealerOrders: React.FC = () => {
return 'warning' return 'warning'
} }
const renderOrderItem = (order: OrderWithDetails) => ( const renderOrderItem = (order: ClinicPrescription) => (
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm"> <View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-1"> <View className="flex justify-between items-start mb-1">
<Text className="font-semibold text-gray-800"> <Text className="font-semibold text-gray-800">
@@ -117,15 +107,9 @@ const DealerOrders: React.FC = () => {
<Text className="text-sm text-gray-400"> <Text className="text-sm text-gray-400">
¥{order.orderPrice || '0.00'} ¥{order.orderPrice || '0.00'}
</Text> </Text>
<Text className="text-sm text-orange-500 font-semibold">
¥{order.userCommission}
</Text>
</View> </View>
<View className="flex justify-between items-center"> <View className="flex justify-between items-center">
<Text className="text-sm text-gray-400">
{order.customerName}
</Text>
<Text className="text-sm text-gray-400"> <Text className="text-sm text-gray-400">
{order.createTime} {order.createTime}
</Text> </Text>

View File

@@ -172,7 +172,7 @@ const CustomerIndex = () => {
</View> </View>
</View> </View>
</View> </View>
<Button type="warning" onClick={() => navTo(`/chat/doctor/index?id=${item.userId}`)}></Button> <Button type="warning" onClick={() => navTo(`/chat/doctor/index?userId=${item.userId}`)}></Button>
</View> </View>
</View> </View>
</View> </View>
@@ -216,7 +216,8 @@ const CustomerIndex = () => {
</View> </View>
</View> </View>
</View> </View>
<Button type="warning" onClick={() => navTo(`/doctor/orders/add?id=${item.userId}`)}></Button> <Button type="warning" onClick={() => navTo(`/doctor/orders/add?id=${item.patientUserId}`)}></Button>
<Button type="primary" onClick={() => navTo(`/chat/doctor/index?userId=${item.patientUserId}`)}></Button>
</View> </View>
</View> </View>
</View> </View>
@@ -350,7 +351,7 @@ const CustomerIndex = () => {
<View className="bg-white pt-2 border-b border-gray-100"> <View className="bg-white pt-2 border-b border-gray-100">
<SearchBar <SearchBar
value={searchValue} value={searchValue}
placeholder="搜索患者名称、手机号" placeholder="搜索医师名称、手机号"
onChange={(value) => setSearchValue(value)} onChange={(value) => setSearchValue(value)}
onClear={() => { onClear={() => {
setSearchValue(''); setSearchValue('');

17
src/pages/index/Role.scss Normal file
View File

@@ -0,0 +1,17 @@
.doctor-user{
background: url("https://oss.wsdns.cn/20251102/f893dba3f4da479f93e87c6def563d60.png");
background-repeat: no-repeat;
background-size: 100%;
width: 100%;
height: 140px;
border-radius: 25px;
}
.patient-user{
background: url("https://oss.wsdns.cn/20251102/d42d50e672d844bcaf7265aee03f3606.png");
background-repeat: no-repeat;
background-size: 100%;
width: 100%;
height: 140px;
border-radius: 25px;
}

45
src/pages/index/Role.tsx Normal file
View File

@@ -0,0 +1,45 @@
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {useShopInfo} from "@/hooks/useShopInfo"
import './Role.scss'
import navTo from "@/utils/common";
import {useEffect, useState} from "react";
const Page = () => {
const [isDoctor, setIsDoctor] = useState<boolean>(false)
// 使用 useShopInfo hooks 获取导航数据
const {
error,
} = useShopInfo()
// 获取顶部导航菜单
useEffect(() => {
setIsDoctor(Taro.getStorageSync('Doctor') || Taro.getStorageSync('Doctor') == 'true')
}, []);
// 处理错误状态
if (error) {
return (
<View className={'p-2 text-center text-red-500'}>
</View>
)
}
return (
<View className={'p-2 z-50 mt-1 gap-2 flex justify-between'}>
{isDoctor && (
<View className={'doctor-user rounded-lg w-full block'} onClick={() => navTo(`/clinic/index`)}></View>
)}
{!isDoctor && (
<View className={'doctor-user rounded-lg w-full block'} onClick={() => Taro.switchTab({
url: `/pages/chat/chat`
})}></View>
)}
<View className={'patient-user rounded-lg bg-white w-full'}
onClick={() => navTo(`/clinic/clinicPatientUser/prescription`, true)}></View>
</View>
)
}
export default Page

View File

@@ -3,11 +3,11 @@ import Taro from '@tarojs/taro';
import {useShareAppMessage} from "@tarojs/taro" import {useShareAppMessage} from "@tarojs/taro"
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {getShopInfo} from "@/api/layout"; import {getShopInfo} from "@/api/layout";
import Menu from "./Menu";
import Banner from "./Banner"; import Banner from "./Banner";
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite"; import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
import './index.scss' import './index.scss'
import GoodsList from './GoodsList'; import GoodsList from './GoodsList';
import Role from "@/pages/index/Role";
function Home() { function Home() {
// 吸顶状态 // 吸顶状态
@@ -158,7 +158,8 @@ function Home() {
<Header /> <Header />
<div className={'flex flex-col mt-12'}> <div className={'flex flex-col mt-12'}>
<Menu/> {/*<Menu/>*/}
<Role />
<Banner/> <Banner/>
<GoodsList onStickyChange={handleTabsStickyChange}/> <GoodsList onStickyChange={handleTabsStickyChange}/>
</div> </div>

View File

@@ -188,6 +188,7 @@ function OrderList(props: OrderListProps) {
// 合并搜索条件tab的statusFilter优先级更高 // 合并搜索条件tab的statusFilter优先级更高
const searchConditions: any = { const searchConditions: any = {
page: currentPage, page: currentPage,
type: 0,
userId: statusParams.userId, // 用户ID userId: statusParams.userId, // 用户ID
...props.searchParams, // 搜索关键词等其他条件 ...props.searchParams, // 搜索关键词等其他条件
}; };

View File

@@ -53,6 +53,7 @@ function Index() {
const submitSucceed = (values: any) => { const submitSucceed = (values: any) => {
console.log('提交表单', values); console.log('提交表单', values);
if (FormData.status != 2 && FormData.status != undefined) return false; if (FormData.status != 2 && FormData.status != undefined) return false;
console.log(FormData.type)
if (FormData.type == 0) { if (FormData.type == 0) {
if (!FormData.sfz1 || !FormData.sfz2) { if (!FormData.sfz1 || !FormData.sfz2) {
Taro.showToast({ Taro.showToast({
@@ -249,6 +250,21 @@ function Index() {
// 企业类型 // 企业类型
FormData.type == 1 && ( FormData.type == 1 && (
<> <>
<Form.Item
label={'真实姓名'}
name="realName"
required
initialValue={FormData.realName}
rules={[{message: '请输入真实姓名'}]}
>
<Input
placeholder={'请输入真实姓名'}
type="text"
disabled={FormData.status != 2 && FormData.status != undefined}
value={FormData?.realName}
onChange={(value) => setFormData({...FormData, realName: value})}
/>
</Form.Item>
<Form.Item <Form.Item
label={'主体名称'} label={'主体名称'}
name="name" name="name"