Compare commits
10 Commits
25eb1366c5
...
2681ccc94b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2681ccc94b | |||
| d8011065d9 | |||
| e70eb5de69 | |||
| a1f9167a42 | |||
| 4098f2a7e2 | |||
| 5749fab9e8 | |||
| 894b4bf7ce | |||
| 32811faf54 | |||
| a5efb6250f | |||
| ca3ff9dc9e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ yarn-error.log*
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
/java/
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
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: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
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: '通源堂健康生态平台',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
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: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
@@ -37,6 +41,7 @@ export function getEnvConfig() {
|
||||
// 导出环境变量
|
||||
export const {
|
||||
API_BASE_URL,
|
||||
WS_URL,
|
||||
APP_NAME,
|
||||
DEBUG
|
||||
} = getEnvConfig()
|
||||
|
||||
@@ -99,3 +99,13 @@ export async function getClinicDoctorUser(id: number) {
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -16,6 +16,20 @@ export async function pageClinicPatientUser(params: ClinicPatientUserParam) {
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ClinicPatientUser {
|
||||
type?: number;
|
||||
// 自增ID
|
||||
userId?: number;
|
||||
patientUserId?: number;
|
||||
// 姓名
|
||||
realName?: string;
|
||||
// 头像
|
||||
|
||||
@@ -37,12 +37,12 @@ export async function listClinicPrescription(params?: ClinicPrescriptionParam) {
|
||||
|
||||
*/
|
||||
export async function addClinicPrescription(data: ClinicPrescription) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
const res = await request.post<ApiResult<ClinicPrescription>>(
|
||||
'/clinic/clinic-prescription',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
return res.data; // 返回处方数据,包含处方ID
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
@@ -106,3 +106,40 @@ export async function getClinicPrescription(id: number) {
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import {ClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem/model";
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
|
||||
/**
|
||||
* 处方主表
|
||||
@@ -10,8 +11,20 @@ export interface ClinicPrescription {
|
||||
id?: number;
|
||||
// 患者
|
||||
userId?: number;
|
||||
// 姓名
|
||||
realName?: string;
|
||||
// 性别 0男 1女
|
||||
sex?: number;
|
||||
// 年龄
|
||||
age?: string;
|
||||
// 身高
|
||||
height?: string;
|
||||
// 体重
|
||||
weight?: string;
|
||||
// 医生
|
||||
doctorId?: number;
|
||||
// 姓名
|
||||
doctorName?: string;
|
||||
// 订单编号
|
||||
orderNo?: string;
|
||||
// 关联就诊表
|
||||
@@ -50,6 +63,11 @@ export interface ClinicPrescription {
|
||||
updateTime?: string;
|
||||
// 药方信息
|
||||
items?: ClinicPrescriptionItem[];
|
||||
// 支付状态
|
||||
payStatus?: boolean;
|
||||
// 订单状态
|
||||
orderStatus?: number;
|
||||
shopOrder?: ShopOrder
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,5 +76,8 @@ export interface ClinicPrescription {
|
||||
*/
|
||||
export interface ClinicPrescriptionParam extends PageParam {
|
||||
id?: number;
|
||||
doctorId?: number;
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
withDoctor?: boolean;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,16 @@ export async function addClinicPrescriptionItem(data: ClinicPrescriptionItem) {
|
||||
}
|
||||
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
13
src/api/payment/index.ts
Normal 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));
|
||||
}
|
||||
29
src/api/payment/model/index.ts
Normal file
29
src/api/payment/model/index.ts
Normal 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;
|
||||
}
|
||||
@@ -34,12 +34,12 @@ export async function listShopChatConversation(params?: ShopChatConversationPara
|
||||
* 添加聊天会话表
|
||||
*/
|
||||
export async function addShopChatConversation(data: ShopChatConversation) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
const res = await request.post<ApiResult<ShopChatConversation>>(
|
||||
'/shop/shop-chat-conversation',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
@@ -99,3 +99,13 @@ export async function getShopChatConversation(id: number) {
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface ShopChatMessage {
|
||||
type?: string;
|
||||
// 消息内容
|
||||
content?: string;
|
||||
// 会话ID
|
||||
conversationId?: number;
|
||||
// 屏蔽接收方
|
||||
sideTo?: number;
|
||||
// 屏蔽发送方
|
||||
@@ -52,6 +54,8 @@ export interface ShopChatMessage {
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 是否本人消息
|
||||
isMine?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,5 +63,6 @@ export interface ShopChatMessage {
|
||||
*/
|
||||
export interface ShopChatMessageParam extends PageParam {
|
||||
id?: number;
|
||||
conversationId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function updateShopOrder(data: ShopOrder) {
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
return res;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -147,14 +147,15 @@ export interface ShopOrder {
|
||||
hasTakeGift?: string;
|
||||
// 订单商品项
|
||||
orderGoods?: OrderGoods[];
|
||||
makePay?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单商品项
|
||||
*/
|
||||
export interface OrderGoodsItem {
|
||||
goodsId: number;
|
||||
quantity: number;
|
||||
goodsId?: number;
|
||||
quantity?: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}
|
||||
@@ -164,7 +165,7 @@ export interface OrderGoodsItem {
|
||||
*/
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
goodsItems?: OrderGoodsItem[];
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -185,8 +186,8 @@ export interface OrderCreateRequest {
|
||||
* 订单商品项
|
||||
*/
|
||||
export interface OrderGoodsItem {
|
||||
goodsId: number;
|
||||
quantity: number;
|
||||
goodsId?: number;
|
||||
quantity?: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}
|
||||
@@ -195,8 +196,10 @@ export interface OrderGoodsItem {
|
||||
* 创建订单请求
|
||||
*/
|
||||
export interface OrderCreateRequest {
|
||||
// 订单编号
|
||||
orderNo?: string;
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
goodsItems?: OrderGoodsItem[];
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -209,6 +212,8 @@ export interface OrderCreateRequest {
|
||||
deliveryType?: number;
|
||||
// 自提店铺ID
|
||||
selfTakeMerchantId?: number;
|
||||
// 订单类型
|
||||
type?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,17 @@ export async function addShopOrderGoods(data: ShopOrderGoods) {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改商品信息
|
||||
*/
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface Order {
|
||||
updateTime?: string;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
makePay?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -104,8 +104,18 @@ export default {
|
||||
"root": "clinic",
|
||||
"pages": [
|
||||
"index",
|
||||
"clinicPatientUser/index",
|
||||
"clinicPatientUser/add",
|
||||
"clinicDoctorUser/add"
|
||||
"clinicPatientUser/selectPatient",
|
||||
"clinicPatientUser/prescription",
|
||||
"clinicPatientUser/detail",
|
||||
"clinicDoctorUser/index",
|
||||
"clinicDoctorUser/add",
|
||||
"clinicPrescription/index",
|
||||
"clinicPrescription/add",
|
||||
"clinicPrescription/selectPrescription",
|
||||
"clinicPrescription/confirm",
|
||||
"clinicPrescription/detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,158 +1,284 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import {useState, useEffect, useRef} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {
|
||||
Loading,
|
||||
InfiniteLoading,
|
||||
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 Taro, {useDidShow, useRouter, useLoad} from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {Voice, FaceMild} from '@nutui/icons-react-taro'
|
||||
import {getClinicDoctorUserByUserId} from "@/api/clinic/clinicDoctorUser";
|
||||
import {addShopChatMessage, listShopChatMessage} from "@/api/shop/shopChatMessage";
|
||||
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 {params} = useRouter();
|
||||
const [doctor, setDoctor] = useState<ClinicDoctorUser>()
|
||||
const [list, setList] = useState<ShopChatMessage[]>([])
|
||||
|
||||
const {params} = useRouter()
|
||||
const [messages, setMessages] = 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 [doctors, setDoctors] = useState<ClinicDoctorUser[]>([])
|
||||
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 socketRef = useRef<Taro.SocketTask | null>(null)
|
||||
|
||||
// 获取列表数据
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const quickActions = [
|
||||
{label: '开方', type: 'prescription'},
|
||||
{label: '快捷回复', type: 'quickReply'},
|
||||
{label: '发问诊单', type: 'sendInquiry'}
|
||||
]
|
||||
|
||||
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 doctorUser = await getClinicDoctorUser(Number(params.id))
|
||||
if (doctorUser) {
|
||||
setDoctor(doctorUser)
|
||||
Taro.setNavigationBarTitle({title: `${doctorUser.realName}`});
|
||||
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}`})
|
||||
}
|
||||
const messages = await pageShopChatMessage({})
|
||||
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
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(() => {
|
||||
fetchData().then()
|
||||
}, []);
|
||||
|
||||
// 监听页面显示,当从其他页面返回时刷新数据
|
||||
useDidShow(() => {
|
||||
// 刷新当前tab的数据和统计信息
|
||||
fetchData().then();
|
||||
});
|
||||
|
||||
// 渲染医师项
|
||||
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>
|
||||
if (conversationId) {
|
||||
connectWebSocket(conversationId).catch(err => {
|
||||
console.error('WebSocket连接失败:', err)
|
||||
})
|
||||
}
|
||||
{
|
||||
item.age && (
|
||||
<>
|
||||
<Divider direction="vertical"/>
|
||||
<Text className={'text-gray-400 text-xs'}>{item.age}岁</Text>
|
||||
</>
|
||||
)
|
||||
return () => {
|
||||
socketRef.current?.close()
|
||||
socketRef.current = null
|
||||
}
|
||||
{
|
||||
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>
|
||||
);
|
||||
|
||||
}, [conversationId])
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50 w-full">
|
||||
<View className={'p-4'}>
|
||||
{list?.map(renderPatientUserItem)}
|
||||
<View className="min-h-screen bg-gray-50 w-full pb-24">
|
||||
<View className="px-4 pt-4 pb-24">
|
||||
{messages.length === 0 ? (
|
||||
<View className="mt-10 text-center text-gray-400 text-sm">
|
||||
<Text>暂时还没有聊天记录</Text>
|
||||
</View>
|
||||
<View className={'fixed bottom-0 w-full bg-orange-50 pt-2 pb-8'}>
|
||||
<View className={'flex flex-1 items-center justify-between'}>
|
||||
<Voice className={'mx-2'} />
|
||||
<Input className={'w-full'} style={{
|
||||
) : (
|
||||
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 className="fixed bottom-0 left-0 right-0 bg-orange-50 pt-2 pb-8 px-3 border-t border-orange-100">
|
||||
<View className="flex gap-3 mb-2 overflow-x-auto">
|
||||
{quickActions.map(action => (
|
||||
<View
|
||||
key={action.type}
|
||||
onClick={() => handleQuickAction(action.type)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
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'
|
||||
}} />
|
||||
<FaceMild size={26} className={'ml-2'} />
|
||||
<AddCircle size={26} className={'mx-2'} />
|
||||
}}
|
||||
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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerIndex;
|
||||
|
||||
3
src/clinic/clinicDoctorUser/index.config.ts
Normal file
3
src/clinic/clinicDoctorUser/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '患者管理'
|
||||
})
|
||||
363
src/clinic/clinicDoctorUser/index.tsx
Normal file
363
src/clinic/clinicDoctorUser/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '处方订单管理',
|
||||
navigationBarTitleText: '开方详情',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
72
src/clinic/clinicPatientUser/detail.scss
Normal file
72
src/clinic/clinicPatientUser/detail.scss
Normal 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;
|
||||
}
|
||||
159
src/clinic/clinicPatientUser/detail.tsx
Normal file
159
src/clinic/clinicPatientUser/detail.tsx
Normal 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;
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
getStatusText,
|
||||
getStatusTagType,
|
||||
getStatusOptions,
|
||||
mapApplyStatusToCustomerStatus,
|
||||
mapCustomerStatusToApplyStatus
|
||||
} from '@/utils/customerStatus';
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
|
||||
import {userPageClinicPatientUser} from "@/api/clinic/clinicPatientUser";
|
||||
|
||||
// 扩展User类型,添加客户状态和保护天数
|
||||
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) => {
|
||||
@@ -174,22 +111,22 @@ const CustomerIndex = () => {
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 4,
|
||||
page: currentPage
|
||||
// type: 4,
|
||||
page: currentPage,
|
||||
};
|
||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||
if (applyStatus !== undefined) {
|
||||
params.applyStatus = applyStatus;
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
const res = await userPageClinicPatientUser(params);
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态并计算保护天数
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
|
||||
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
|
||||
// customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
|
||||
// protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
|
||||
}));
|
||||
|
||||
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '新增处方订单',
|
||||
navigationBarTitleText: '用药订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
415
src/clinic/clinicPatientUser/prescription.tsx
Normal file
415
src/clinic/clinicPatientUser/prescription.tsx
Normal 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;
|
||||
4
src/clinic/clinicPatientUser/selectPatient.config.ts
Normal file
4
src/clinic/clinicPatientUser/selectPatient.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '选择患者',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
285
src/clinic/clinicPatientUser/selectPatient.tsx
Normal file
285
src/clinic/clinicPatientUser/selectPatient.tsx
Normal 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;
|
||||
201
src/clinic/clinicPrescription/_index.tsx
Normal file
201
src/clinic/clinicPrescription/_index.tsx
Normal 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;
|
||||
@@ -1,5 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '新增处方主表
|
||||
',
|
||||
navigationBarTitleText: '开处方',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
@@ -1,51 +1,273 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
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 {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 {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 [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ClinicPrescription>({})
|
||||
const formRef = useRef<any>(null)
|
||||
const [toUser, setToUser] = useState<ClinicPatientUser>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
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 () => {
|
||||
if (params.id) {
|
||||
const data = await getClinicPrescription(Number(params.id))
|
||||
setFormData(data)
|
||||
} else {
|
||||
setFormData({})
|
||||
if (toUserId) {
|
||||
getClinicPatientUser(Number(toUserId)).then(data => {
|
||||
setToUser(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
if (params.id) {
|
||||
// 编辑模式
|
||||
await updateClinicPrescription({
|
||||
...values,
|
||||
id: Number(params.id)
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
await addClinicPrescription(values)
|
||||
// 设置选中的患者(供其他页面调用)
|
||||
// @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: `操作成功`,
|
||||
title: '上传成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
return Taro.navigateBack()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: `操作失败`,
|
||||
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) => {
|
||||
try {
|
||||
console.log('提交数据:', values)
|
||||
|
||||
// 参数校验
|
||||
if (!toUser && !selectedPatient) {
|
||||
Taro.showToast({
|
||||
title: `请选择发送对象或患者`,
|
||||
icon: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!values.diagnosis) {
|
||||
Taro.showToast({
|
||||
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'
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('数据处理失败:', error);
|
||||
Taro.showToast({
|
||||
title: `数据处理失败: ${error.message || error || '未知错误'}`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
@@ -59,7 +281,45 @@ const AddClinicPrescription = () => {
|
||||
reload().then(() => {
|
||||
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) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
@@ -67,32 +327,188 @@ const AddClinicPrescription = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 显示已选择的用户(如果有的话) */}
|
||||
<Cell title={(
|
||||
<View className={'flex items-center'}>
|
||||
<Avatar src={toUser?.avatar}/>
|
||||
<View className={'ml-2 flex flex-col'}>
|
||||
<Text>{toUser?.realName || '请选择'}</Text>
|
||||
<Text className={'text-gray-300'}>{toUser?.phone}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)} extra={(
|
||||
<ArrowRight color="#cccccc" className={'mt-2'} size={20}/>
|
||||
)} onClick={() => navTo(`/clinic/clinicPatientUser/selectPatient`, true)}
|
||||
/>
|
||||
|
||||
{toUser && (
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
initialValues={formData}
|
||||
labelPosition={'top'}
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
footer={
|
||||
<div
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<Form.Item
|
||||
name="diagnosis"
|
||||
label={'诊断结果'}
|
||||
required
|
||||
rules={[{required: true, message: '请填写诊断结果'}]}
|
||||
initialValue={formData.diagnosis}
|
||||
>
|
||||
<Input placeholder="请输入诊断结果" style={{
|
||||
backgroundColor: '#f9f9f9',
|
||||
padding: '4px',
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="treatmentPlan"
|
||||
label={'治疗方案'}
|
||||
required
|
||||
rules={[{required: true, message: '请填写治疗方案'}]}
|
||||
initialValue={formData.treatmentPlan}
|
||||
>
|
||||
<TextArea
|
||||
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',
|
||||
width: '100%'
|
||||
border: '2px dashed #d9d9d9',
|
||||
backgroundColor: '#fafafa',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
nativeType="submit"
|
||||
type="success"
|
||||
size="large"
|
||||
className={'w-full'}
|
||||
block
|
||||
>
|
||||
{params.id ? '更新' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="userId" label="患者" initialValue={FormData.userId} required>
|
||||
<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;
|
||||
|
||||
809
src/clinic/clinicPrescription/components/OrderList.tsx
Normal file
809
src/clinic/clinicPrescription/components/OrderList.tsx
Normal 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
|
||||
6
src/clinic/clinicPrescription/confirm.config.ts
Normal file
6
src/clinic/clinicPrescription/confirm.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
navigationBarTitleText: '确认处方订单',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}
|
||||
286
src/clinic/clinicPrescription/confirm.scss
Normal file
286
src/clinic/clinicPrescription/confirm.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
410
src/clinic/clinicPrescription/confirm.tsx
Normal file
410
src/clinic/clinicPrescription/confirm.tsx
Normal 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
|
||||
6
src/clinic/clinicPrescription/detail.config.ts
Normal file
6
src/clinic/clinicPrescription/detail.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
navigationBarTitleText: '开方详情',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}
|
||||
190
src/clinic/clinicPrescription/detail.tsx
Normal file
190
src/clinic/clinicPrescription/detail.tsx
Normal 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;
|
||||
@@ -1,5 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '处方主表
|
||||
管理',
|
||||
navigationBarTitleText: '处方管理',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
72
src/clinic/clinicPrescription/index.scss
Normal file
72
src/clinic/clinicPrescription/index.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,113 @@
|
||||
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 {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
|
||||
import {listClinicPrescription, removeClinicPrescription, updateClinicPrescription} from "@/api/clinic/clinicPrescription";
|
||||
import {useState, useCallback, useRef, useEffect} from "react";
|
||||
import {Space, Button, Input} from '@nutui/nutui-react-taro'
|
||||
import {View} from '@tarojs/components';
|
||||
import OrderList from "./components/OrderList";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {ShopOrderParam} from "@/api/shop/shopOrder/model";
|
||||
import './index.scss'
|
||||
|
||||
const ClinicPrescriptionList = () => {
|
||||
const [list, setList] = useState<ClinicPrescription[]>([])
|
||||
function ClinicPrescriptionList() {
|
||||
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 = () => {
|
||||
listClinicPrescription({
|
||||
// 添加查询条件
|
||||
})
|
||||
.then(data => {
|
||||
setList(data || [])
|
||||
})
|
||||
.catch(() => {
|
||||
Taro.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'error'
|
||||
});
|
||||
})
|
||||
const reload = async (where?: ShopOrderParam) => {
|
||||
console.log(where,'where...')
|
||||
setSearchParams(prev => ({ ...prev, ...where }))
|
||||
}
|
||||
|
||||
|
||||
const onDel = async (id?: number) => {
|
||||
await removeClinicPrescription(id)
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload();
|
||||
// 防抖搜索函数
|
||||
const debouncedSearch = useCallback((keyword: string) => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload()
|
||||
});
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
if (keyword.trim()) {
|
||||
handleSearch({keywords: keyword.trim()});
|
||||
} else {
|
||||
// 如果搜索关键词为空,清除keywords参数
|
||||
const newSearchParams = { ...searchParams };
|
||||
delete newSearchParams.keywords;
|
||||
setSearchParams(newSearchParams);
|
||||
reload(newSearchParams).then();
|
||||
}
|
||||
}, 500); // 500ms防抖延迟
|
||||
}, [searchParams]);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (where: ShopOrderParam) => {
|
||||
// 合并搜索参数,保留当前的statusFilter
|
||||
const newSearchParams = {
|
||||
...searchParams, // 保留当前的所有参数(包括statusFilter)
|
||||
...where // 应用新的搜索条件
|
||||
};
|
||||
setSearchParams(newSearchParams)
|
||||
reload(newSearchParams).then()
|
||||
}
|
||||
useEffect(() => {
|
||||
reload().then()
|
||||
}, []);
|
||||
|
||||
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'
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 搜索组件 */}
|
||||
{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'
|
||||
}}
|
||||
description="暂无数据"
|
||||
/>
|
||||
</View>
|
||||
<Space>
|
||||
<Button onClick={() => Taro.navigateTo({url: '/clinic/clinicPrescription/add'})}>新增处方主表
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (searchKeyword.trim()) {
|
||||
handleSearch({keywords: searchKeyword.trim()});
|
||||
}
|
||||
}}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
)
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/*订单列表*/}
|
||||
<OrderList
|
||||
onReload={() => reload(searchParams)}
|
||||
searchParams={searchParams}
|
||||
showSearch={showSearch}
|
||||
onSearchParamsChange={(newParams) => {
|
||||
console.log('父组件接收到searchParams变化:', newParams);
|
||||
setSearchParams(newParams);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.map((item, _) => (
|
||||
<Cell.Group key={item.
|
||||
export default ClinicPrescriptionList;
|
||||
|
||||
4
src/clinic/clinicPrescription/selectPatient.config.ts
Normal file
4
src/clinic/clinicPrescription/selectPatient.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '选择患者',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
285
src/clinic/clinicPrescription/selectPatient.tsx
Normal file
285
src/clinic/clinicPrescription/selectPatient.tsx
Normal 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;
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '选择处方',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
237
src/clinic/clinicPrescription/selectPrescription.tsx
Normal file
237
src/clinic/clinicPrescription/selectPrescription.tsx
Normal 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;
|
||||
@@ -147,7 +147,7 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'在线开方'} onClick={() => navigateToPage('/doctor/orders/add')}>
|
||||
<Grid.Item text={'在线开方'} onClick={() => navigateToPage('/clinic/clinicPrescription/add')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Edit color="#10b981" size="20"/>
|
||||
@@ -163,7 +163,7 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'处方管理'} onClick={() => navigateToPage('/doctor/orders/index')}>
|
||||
<Grid.Item text={'处方管理'} onClick={() => navigateToPage('/clinic/clinicPrescription/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Orderlist color="#f59e0b" size="20"/>
|
||||
|
||||
@@ -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)) {
|
||||
Taro.showToast({
|
||||
title: '请填写正确的手机号',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '在线开方',
|
||||
navigationBarTitleText: '开处方',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
134
src/doctor/orders/add.scss
Normal file
134
src/doctor/orders/add.scss
Normal 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;
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
Avatar,
|
||||
Input,
|
||||
Space,
|
||||
TextArea
|
||||
TextArea,
|
||||
Picker as NutPicker
|
||||
} 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 Taro from '@tarojs/taro'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
@@ -18,8 +19,8 @@ import navTo from "@/utils/common";
|
||||
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
|
||||
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {getClinicPatientUser} from "@/api/clinic/clinicPatientUser";
|
||||
import {addClinicOrder} from "@/api/clinic/clinicOrder";
|
||||
import {clinicPatientUserByPatientUserId} from "@/api/clinic/clinicPatientUser";
|
||||
import './add.scss'
|
||||
|
||||
// 图片数据接口
|
||||
interface UploadedImageData {
|
||||
@@ -31,6 +32,215 @@ interface UploadedImageData {
|
||||
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 {params} = useRouter();
|
||||
const [toUser, setToUser] = useState<ClinicPatientUser>()
|
||||
@@ -54,13 +264,17 @@ const AddClinicOrder = () => {
|
||||
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 toUserId = params.id ? Number(params.id) : undefined
|
||||
|
||||
const reload = async () => {
|
||||
if (toUserId) {
|
||||
getClinicPatientUser(Number(toUserId)).then(data => {
|
||||
clinicPatientUserByPatientUserId(Number(toUserId)).then(data => {
|
||||
setToUser(data)
|
||||
})
|
||||
}
|
||||
@@ -321,6 +535,24 @@ const AddClinicOrder = () => {
|
||||
}
|
||||
}, [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) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
@@ -480,7 +712,7 @@ const AddClinicOrder = () => {
|
||||
onClick={() => navTo(`/doctor/orders/selectPrescription`, true)}
|
||||
/>
|
||||
{/* 药方信息 */}
|
||||
{selectedPrescription && (
|
||||
{selectedPrescription ? (
|
||||
<>
|
||||
<Cell extra={'药方信息'}>
|
||||
<View className={'flex flex-col'}>
|
||||
@@ -493,6 +725,16 @@ const AddClinicOrder = () => {
|
||||
</View>
|
||||
</Cell>
|
||||
|
||||
<UsageSummaryCard
|
||||
prescription={selectedPrescription}
|
||||
doseCount={doseCount}
|
||||
onDoseChange={setDoseCount}
|
||||
frequencyIndex={frequencyIndex}
|
||||
onFrequencyChange={setFrequencyIndex}
|
||||
perDoseIndex={perDoseIndex}
|
||||
onPerDoseChange={setPerDoseIndex}
|
||||
/>
|
||||
|
||||
{/* 煎药说明 */}
|
||||
<TextArea
|
||||
value={formData.decoctionInstructions}
|
||||
@@ -502,7 +744,7 @@ const AddClinicOrder = () => {
|
||||
maxLength={200}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={'下一步:确认订单信息'} onClick={() => formRef.current?.submit()}/>
|
||||
|
||||
@@ -14,8 +14,11 @@ 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 {addClinicOrder} from "@/api/clinic/clinicOrder";
|
||||
import {addClinicPrescription} from "@/api/clinic/clinicPrescription";
|
||||
import {batchAddClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem";
|
||||
import './confirm.scss'
|
||||
import request from "@/utils/request";
|
||||
import {ApiResult} from "@/api";
|
||||
|
||||
// 订单数据接口
|
||||
interface OrderData {
|
||||
@@ -84,51 +87,105 @@ const DoctorOrderConfirm = () => {
|
||||
try {
|
||||
setSubmitLoading(true)
|
||||
|
||||
// 构建诊所订单数据
|
||||
const clinicOrderData = {
|
||||
userId: orderData.patient.userId,
|
||||
doctorId: Taro.getStorageSync('UserId'), // 当前医生ID
|
||||
type: 0, // 订单类型:诊所订单
|
||||
title: `${orderData.patient.realName}的处方订单`,
|
||||
totalPrice: getTotalPrice(),
|
||||
payPrice: getTotalPrice(),
|
||||
buyerRemarks: orderData.diagnosis,
|
||||
merchantRemarks: orderData.treatmentPlan,
|
||||
comments: JSON.stringify({
|
||||
diagnosis: orderData.diagnosis,
|
||||
treatmentPlan: orderData.treatmentPlan,
|
||||
decoctionInstructions: orderData.decoctionInstructions,
|
||||
prescriptionId: orderData.prescription?.id,
|
||||
images: orderData.images
|
||||
}),
|
||||
payStatus: '0', // 未付款
|
||||
orderStatus: 0, // 待支付
|
||||
deliveryStatus: 10, // 未发货
|
||||
const doctorId = Taro.getStorageSync('UserId') // 当前医生ID
|
||||
|
||||
// 第一步:创建处方主表记录
|
||||
console.log('开始创建处方记录...')
|
||||
const prescriptionData: ClinicPrescription = {
|
||||
userId: orderData.patient.patientUserId,
|
||||
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}岁`
|
||||
}
|
||||
|
||||
console.log('提交订单数据:', clinicOrderData)
|
||||
const createdPrescription = await addClinicPrescription(prescriptionData)
|
||||
console.log('处方创建成功:', createdPrescription)
|
||||
|
||||
// 调用API创建订单
|
||||
await addClinicOrder(clinicOrderData)
|
||||
if (!createdPrescription || !createdPrescription.id) {
|
||||
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.showToast({
|
||||
title: '处方已发送给患者',
|
||||
icon: 'success',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// 跳转到订单列表
|
||||
Taro.redirectTo({
|
||||
url: '/doctor/orders/index'
|
||||
url: '/clinic/clinicPatientUser/prescription'
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('创建订单失败:', error)
|
||||
console.error('创建处方/订单失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '发送失败,请重试',
|
||||
icon: 'error'
|
||||
|
||||
@@ -2,21 +2,15 @@ import React, {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text, ScrollView} from '@tarojs/components'
|
||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
|
||||
|
||||
interface OrderWithDetails extends ShopDealerOrder {
|
||||
orderNo?: string
|
||||
customerName?: string
|
||||
userCommission?: string
|
||||
}
|
||||
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
|
||||
import {pageClinicPrescription} from "@/api/clinic/clinicPrescription";
|
||||
|
||||
const DealerOrders: React.FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = 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 [hasMore, setHasMore] = useState<boolean>(true)
|
||||
|
||||
@@ -24,8 +18,7 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
// 获取订单数据
|
||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
console.log('获取订单数据')
|
||||
try {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true)
|
||||
@@ -35,17 +28,14 @@ const DealerOrders: React.FC = () => {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
|
||||
const result = await pageShopDealerOrder({
|
||||
const result = await pageClinicPrescription({
|
||||
page,
|
||||
limit: 10
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const newOrders = result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userCommission: order.firstMoney || '0.00'
|
||||
...order
|
||||
}))
|
||||
|
||||
if (page === 1) {
|
||||
@@ -102,7 +92,7 @@ const DealerOrders: React.FC = () => {
|
||||
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 className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
@@ -117,15 +107,9 @@ const DealerOrders: React.FC = () => {
|
||||
<Text className="text-sm text-gray-400">
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<Text className="text-sm text-orange-500 font-semibold">
|
||||
我的佣金:¥{order.userCommission}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
客户:{order.customerName}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{order.createTime}
|
||||
</Text>
|
||||
|
||||
@@ -172,7 +172,7 @@ const CustomerIndex = () => {
|
||||
</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>
|
||||
@@ -216,7 +216,8 @@ const CustomerIndex = () => {
|
||||
</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>
|
||||
@@ -350,7 +351,7 @@ const CustomerIndex = () => {
|
||||
<View className="bg-white pt-2 border-b border-gray-100">
|
||||
<SearchBar
|
||||
value={searchValue}
|
||||
placeholder="搜索患者名称、手机号"
|
||||
placeholder="搜索医师名称、手机号"
|
||||
onChange={(value) => setSearchValue(value)}
|
||||
onClear={() => {
|
||||
setSearchValue('');
|
||||
|
||||
17
src/pages/index/Role.scss
Normal file
17
src/pages/index/Role.scss
Normal 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
45
src/pages/index/Role.tsx
Normal 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
|
||||
|
||||
@@ -3,11 +3,11 @@ import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import Menu from "./Menu";
|
||||
import Banner from "./Banner";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
||||
import './index.scss'
|
||||
import GoodsList from './GoodsList';
|
||||
import Role from "@/pages/index/Role";
|
||||
|
||||
function Home() {
|
||||
// 吸顶状态
|
||||
@@ -158,7 +158,8 @@ function Home() {
|
||||
<Header />
|
||||
|
||||
<div className={'flex flex-col mt-12'}>
|
||||
<Menu/>
|
||||
{/*<Menu/>*/}
|
||||
<Role />
|
||||
<Banner/>
|
||||
<GoodsList onStickyChange={handleTabsStickyChange}/>
|
||||
</div>
|
||||
|
||||
@@ -188,6 +188,7 @@ function OrderList(props: OrderListProps) {
|
||||
// 合并搜索条件,tab的statusFilter优先级更高
|
||||
const searchConditions: any = {
|
||||
page: currentPage,
|
||||
type: 0,
|
||||
userId: statusParams.userId, // 用户ID
|
||||
...props.searchParams, // 搜索关键词等其他条件
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ function Index() {
|
||||
const submitSucceed = (values: any) => {
|
||||
console.log('提交表单', values);
|
||||
if (FormData.status != 2 && FormData.status != undefined) return false;
|
||||
console.log(FormData.type)
|
||||
if (FormData.type == 0) {
|
||||
if (!FormData.sfz1 || !FormData.sfz2) {
|
||||
Taro.showToast({
|
||||
@@ -249,6 +250,21 @@ function Index() {
|
||||
// 企业类型
|
||||
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
|
||||
label={'主体名称'}
|
||||
name="name"
|
||||
|
||||
Reference in New Issue
Block a user