完善功能

This commit is contained in:
2025-12-01 10:14:44 +08:00
parent d8011065d9
commit 2681ccc94b
31 changed files with 1281 additions and 302 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import type { PageParam } from '@/api/index'; import type { PageParam } from '@/api/index';
import {ClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem/model"; import {ClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem/model";
import {ShopOrder} from "@/api/shop/shopOrder/model";
/** /**
* 处方主表 * 处方主表
@@ -66,6 +67,7 @@ export interface ClinicPrescription {
payStatus?: boolean; payStatus?: boolean;
// 订单状态 // 订单状态
orderStatus?: number; orderStatus?: number;
shopOrder?: ShopOrder
} }
/** /**
@@ -77,4 +79,5 @@ export interface ClinicPrescriptionParam extends PageParam {
doctorId?: number; doctorId?: number;
userId?: number; userId?: number;
keywords?: string; keywords?: string;
withDoctor?: boolean;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -147,6 +147,7 @@ export interface ShopOrder {
hasTakeGift?: string; hasTakeGift?: string;
// 订单商品项 // 订单商品项
orderGoods?: OrderGoods[]; orderGoods?: OrderGoods[];
makePay?: boolean
} }
/** /**

View File

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

View File

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

View File

@@ -108,6 +108,7 @@ export default {
"clinicPatientUser/add", "clinicPatientUser/add",
"clinicPatientUser/selectPatient", "clinicPatientUser/selectPatient",
"clinicPatientUser/prescription", "clinicPatientUser/prescription",
"clinicPatientUser/detail",
"clinicDoctorUser/index", "clinicDoctorUser/index",
"clinicDoctorUser/add", "clinicDoctorUser/add",
"clinicPrescription/index", "clinicPrescription/index",

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,12 @@ import {
getStatusText, getStatusText,
getStatusTagType, getStatusTagType,
getStatusOptions, getStatusOptions,
mapApplyStatusToCustomerStatus,
mapCustomerStatusToApplyStatus mapCustomerStatusToApplyStatus
} from '@/utils/customerStatus'; } from '@/utils/customerStatus';
import FixedButton from "@/components/FixedButton"; import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common"; import navTo from "@/utils/common";
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply"; import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
import {userPageClinicPatientUser} from "@/api/clinic/clinicPatientUser";
// 扩展User类型添加客户状态和保护天数 // 扩展User类型添加客户状态和保护天数
interface CustomerUser extends UserType { interface CustomerUser extends UserType {
@@ -102,69 +102,6 @@ const CustomerIndex = () => {
}); });
}; };
// 计算剩余保护天数(基于过期时间)
const calculateProtectDays = (expirationTime?: string, applyTime?: string): number => {
try {
// 优先使用过期时间字段
if (expirationTime) {
const expDate = new Date(expirationTime.replace(' ', 'T'));
const now = new Date();
// 计算剩余毫秒数
const remainingMs = expDate.getTime() - now.getTime();
// 转换为天数,向上取整
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
console.log('=== 基于过期时间计算 ===');
console.log('过期时间:', expirationTime);
console.log('当前时间:', now.toLocaleString());
console.log('剩余天数:', remainingDays);
console.log('======================');
return Math.max(0, remainingDays);
}
// 如果没有过期时间,回退到基于申请时间计算
if (!applyTime) return 0;
const protectionPeriod = 7; // 保护期7天
// 解析申请时间
let applyDate: Date;
if (applyTime.includes('T')) {
applyDate = new Date(applyTime);
} else {
applyDate = new Date(applyTime.replace(' ', 'T'));
}
// 获取当前时间
const now = new Date();
// 只比较日期部分,忽略时间
const applyDateOnly = new Date(applyDate.getFullYear(), applyDate.getMonth(), applyDate.getDate());
const currentDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// 计算已经过去的天数
const timeDiff = currentDateOnly.getTime() - applyDateOnly.getTime();
const daysPassed = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
// 计算剩余保护天数
const remainingDays = protectionPeriod - daysPassed;
console.log('=== 基于申请时间计算 ===');
console.log('申请时间:', applyTime);
console.log('已过去天数:', daysPassed);
console.log('剩余保护天数:', remainingDays);
console.log('======================');
return Math.max(0, remainingDays);
} catch (error) {
console.error('日期计算错误:', error);
return 0;
}
};
// 获取客户数据 // 获取客户数据
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => { const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
@@ -174,22 +111,22 @@ const CustomerIndex = () => {
// 构建API参数根据状态筛选 // 构建API参数根据状态筛选
const params: any = { const params: any = {
type: 4, // type: 4,
page: currentPage page: currentPage,
}; };
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab); const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
if (applyStatus !== undefined) { if (applyStatus !== undefined) {
params.applyStatus = applyStatus; params.applyStatus = applyStatus;
} }
const res = await pageShopDealerApply(params); const res = await userPageClinicPatientUser(params);
if (res?.list && res.list.length > 0) { if (res?.list && res.list.length > 0) {
// 正确映射状态并计算保护天数 // 正确映射状态并计算保护天数
const mappedList = res.list.map(customer => ({ const mappedList = res.list.map(customer => ({
...customer, ...customer,
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10), // customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '') // protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
})); }));
// 如果是重置页面或第一页,直接设置新数据;否则追加数据 // 如果是重置页面或第一页,直接设置新数据;否则追加数据

View File

@@ -1,24 +1,32 @@
import {useState} from "react"; import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro' import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag} from '@nutui/nutui-react-taro' import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag, Popup} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import { import {
pageClinicPrescription, updateClinicPrescription, pageClinicPrescription,
WxPayResult WxPayResult
} from "@/api/clinic/clinicPrescription"; } from "@/api/clinic/clinicPrescription";
import {copyText} from "@/utils/common"; import {copyText} from "@/utils/common";
import {createOrder} from "@/api/shop/shopOrder"; import {updateShopOrder} from "@/api/shop/shopOrder";
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
const ClinicPrescriptionList = () => { const ClinicPrescriptionList = () => {
const [list, setList] = useState<ClinicPrescription[]>([]) const [list, setList] = useState<ClinicPrescription[]>([])
const [loading, setLoading] = useState<boolean>(false) 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 = () => { const reload = () => {
setLoading(true) setLoading(true)
pageClinicPrescription({ pageClinicPrescription({
// 添加查询条件
userId: Taro.getStorageSync('UserId'), userId: Taro.getStorageSync('UserId'),
withDoctor: true,
}) })
.then(data => { .then(data => {
setList(data?.list || []) setList(data?.list || [])
@@ -35,6 +43,112 @@ const ClinicPrescriptionList = () => {
} }
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)
}
}
/** /**
* 处理微信支付 * 处理微信支付
*/ */
@@ -45,11 +159,6 @@ const ClinicPrescriptionList = () => {
throw new Error('微信支付参数错误'); throw new Error('微信支付参数错误');
} }
// 验证微信支付必要参数
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
throw new Error('微信支付参数不完整');
}
try { try {
await Taro.requestPayment({ await Taro.requestPayment({
timeStamp: result.timeStamp, timeStamp: result.timeStamp,
@@ -79,7 +188,14 @@ const ClinicPrescriptionList = () => {
/** /**
* 统一支付入口 * 统一支付入口
*/ */
const onPay = async (item: ClinicPrescription) => { const onPay = async (item: ClinicPrescription | null, skipAddressCheck: boolean = false) => {
if (!item) {
Taro.showToast({
title: '处方信息缺失',
icon: 'error'
});
return;
}
if (!item.id) { if (!item.id) {
Taro.showToast({ Taro.showToast({
title: '处方信息缺失', title: '处方信息缺失',
@@ -88,48 +204,39 @@ const ClinicPrescriptionList = () => {
return; return;
} }
Taro.showLoading({title: '支付中...'}); const shopOrder = (item as any).shopOrder
if (!skipAddressCheck) {
if (shopOrder && (!shopOrder.addressId || shopOrder.addressId === 0)) {
await ensureAddressBeforePay(item)
return
}
}
await Taro.showLoading({title: '支付中...'});
try { try {
// 调用统一支付接口 // 调用统一支付接口
const result = await createOrder( // @ts-ignore
const {data} = await updateShopOrder(
{ {
type: 1, orderId: shopOrder.orderId,
addressId: 10951, makePay: true
comments: '开方',
deliveryType: 0,
payType: 1,
goodsItems: [
{
goodsId: 10056,
quantity: 1
}
]
} }
); );
console.log(result, 'resultresultresultresultresultresult') const result = data as WxPayResult;
console.log('订单创建结果:', result); console.log('订单创建结果:', result);
if (!result) {
throw new Error('创建订单失败');
}
if (!result.orderNo) {
throw new Error('订单号获取失败');
}
// 调用微信支付 // 调用微信支付
await handleWechatPay(result); await handleWechatPay(result);
// 支付成功 // 支付成功
console.log('支付成功,订单号:', result.orderNo); // console.log('支付成功,订单号:', result.orderNo);
await updateClinicPrescription({ // await updateClinicPrescription({
id: item.id, // id: item.id,
orderNo: result.orderNo, // orderNo: result.orderNo,
status: 2 // status: 2
}) // })
Taro.showToast({ Taro.showToast({
title: '支付成功', title: '支付成功',
@@ -185,7 +292,7 @@ const ClinicPrescriptionList = () => {
<Cell <Cell
title={`${item.id}`} title={`${item.id}`}
extra={ extra={
<Tag type={'warning'} className="font-medium">{item.status == 2 ? '已支付' : '待支付'}</Tag> <Tag type={'warning'} className="font-medium">{item?.shopOrder.payStatus == 1 ? '已支付' : '待支付'}</Tag>
} }
onClick={() => copyText(`${item.orderNo}`)} onClick={() => copyText(`${item.orderNo}`)}
/> />
@@ -226,21 +333,81 @@ const ClinicPrescriptionList = () => {
</Text> </Text>
} }
/> />
{item.status == 0 && ( <Cell>
<Cell> <Space className="w-full justify-end">
<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 <Button
type={'danger'} type={'danger'}
size="small"
onClick={() => onPay(item)} onClick={() => onPay(item)}
> >
</Button> </Button>
</Space> )}
</Cell> </Space>
)} </Cell>
</CellGroup> </CellGroup>
))} ))}
</View> </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>
</> </>
) )
} }

View File

@@ -5,7 +5,7 @@ import {Loading, InfiniteLoading, Empty, Space, SearchBar, Button} from '@nutui/
import {Phone} from '@nutui/icons-react-taro' import {Phone} from '@nutui/icons-react-taro'
import type {ClinicPatientUser as PatientUserType} from "@/api/clinic/clinicPatientUser/model"; import type {ClinicPatientUser as PatientUserType} from "@/api/clinic/clinicPatientUser/model";
import { import {
pageClinicPatientUser userPageClinicPatientUser
} from "@/api/clinic/clinicPatientUser"; } from "@/api/clinic/clinicPatientUser";
// 患者类型 // 患者类型
@@ -63,7 +63,7 @@ const SelectPatient = () => {
params.keywords = displaySearchValue.trim(); params.keywords = displaySearchValue.trim();
} }
const res = await pageClinicPatientUser(params); const res = await userPageClinicPatientUser(params);
if (res?.list && res.list.length > 0) { if (res?.list && res.list.length > 0) {
// 如果是重置页面或第一页,直接设置新数据;否则追加数据 // 如果是重置页面或第一页,直接设置新数据;否则追加数据

View File

@@ -76,6 +76,7 @@ const AddClinicOrder = () => {
// 设置选中的处方(供其他页面调用) // 设置选中的处方(供其他页面调用)
// @ts-ignore // @ts-ignore
const setSelectedPrescriptionFunc = (prescription: ClinicPrescription) => { const setSelectedPrescriptionFunc = (prescription: ClinicPrescription) => {
console.log('设置选中的处方:', prescription)
setSelectedPrescription(prescription) setSelectedPrescription(prescription)
} }

View File

@@ -12,6 +12,7 @@ import PaymentCountdown from "@/components/PaymentCountdown";
import {PaymentType} from "@/utils/payment"; import {PaymentType} from "@/utils/payment";
import {goTo} from "@/utils/navigation"; import {goTo} from "@/utils/navigation";
import {pageClinicPrescription} from "@/api/clinic/clinicPrescription"; import {pageClinicPrescription} from "@/api/clinic/clinicPrescription";
import Prescription from "@/clinic/clinicPatientUser/prescription";
// 判断订单是否支付已过期 // 判断订单是否支付已过期
const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => { const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => {
@@ -389,9 +390,10 @@ function OrderList(props: OrderListProps) {
}; };
// 立即支付 // 立即支付
const payOrder = async (order: ShopOrder) => { // @ts-ignore
const payOrder = async (order: Prescription) => {
try { try {
if (!order.orderId || !order.orderNo) { if (!order.id || !order.orderNo) {
Taro.showToast({ Taro.showToast({
title: '订单信息错误', title: '订单信息错误',
icon: 'error' icon: 'error'
@@ -428,19 +430,20 @@ function OrderList(props: OrderListProps) {
Taro.showLoading({title: '发起支付...'}); Taro.showLoading({title: '发起支付...'});
// 构建商品数据 // 构建商品数据
const goodsItems = order.orderGoods?.map(goods => ({ const goodsItems = order.items?.map(goods => ({
goodsId: goods.goodsId, goodsId: goods.medicineId,
quantity: goods.totalNum || 1 quantity: goods.totalNum || 1
})) || []; })) || [];
// 对于已存在的订单,我们需要重新发起支付 // 对于已存在的订单,我们需要重新发起支付
// 构建支付请求数据,包含完整的商品信息 // 构建支付请求数据,包含完整的商品信息
const paymentData = { const paymentData = {
orderId: order.orderId, orderId: order.id,
orderNo: order.orderNo, orderNo: order.orderNo,
goodsItems: goodsItems, goodsItems: goodsItems,
addressId: order.addressId, // addressId: order.addressId,
payType: PaymentType.WECHAT payType: PaymentType.WECHAT,
type: 3
}; };
console.log('重新支付数据:', paymentData); console.log('重新支付数据:', paymentData);
@@ -701,10 +704,12 @@ function OrderList(props: OrderListProps) {
e.stopPropagation(); e.stopPropagation();
void cancelOrder(item); void cancelOrder(item);
}}></Button> }}></Button>
<Button size={'small'} type="primary" onClick={(e) => { {item.showPayButton && (
e.stopPropagation(); <Button size={'small'} type="primary" onClick={(e) => {
void payOrder(item); e.stopPropagation();
}}></Button> void payOrder(item);
}}></Button>
)}
</Space> </Space>
)} )}

View File

@@ -281,9 +281,11 @@ const DoctorOrderConfirm = () => {
<Space> <Space>
<Text className="medicine-name">{item.medicineName}</Text> <Text className="medicine-name">{item.medicineName}</Text>
<Text className="medicine-price">¥{item.unitPrice}</Text> <Text className="medicine-price">¥{item.unitPrice}</Text>
<Text className="medicine-spec"> {!!item.specification && (
{item.specification || '规格未知'} × {item.quantity || 1} <Text className="medicine-spec">
</Text> {item.specification || '规格未知'} × {item.quantity || 1}
</Text>
)}
</Space> </Space>
<View className="medicine-sub"> <View className="medicine-sub">
<Text className="medicine-subtotal"> <Text className="medicine-subtotal">
@@ -372,7 +374,7 @@ const DoctorOrderConfirm = () => {
{/* 底部操作按钮 */} {/* 底部操作按钮 */}
<FixedButton <FixedButton
text={submitLoading ? '发送中...' : '确认并发送给患者'} text={submitLoading ? '发送中...' : '确认并发送给患者'}
icon={<Edit />} icon={<Edit/>}
onClick={handleConfirmOrder} onClick={handleConfirmOrder}
/> />
<View className="fixed-bottom-bar"> <View className="fixed-bottom-bar">

View File

@@ -11,7 +11,7 @@ function ClinicPrescriptionList() {
const [searchParams, setSearchParams] = useState<ShopOrderParam>({ const [searchParams, setSearchParams] = useState<ShopOrderParam>({
statusFilter: params.statusFilter != undefined && params.statusFilter != '' ? parseInt(params.statusFilter) : -1 statusFilter: params.statusFilter != undefined && params.statusFilter != '' ? parseInt(params.statusFilter) : -1
}) })
const [showSearch, setShowSearch] = useState(false) const [showSearch] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('') const [searchKeyword, setSearchKeyword] = useState('')
const searchTimeoutRef = useRef<NodeJS.Timeout>() const searchTimeoutRef = useRef<NodeJS.Timeout>()

View File

@@ -128,11 +128,14 @@ const SelectPrescription = () => {
<View className="flex items-center mb-1"> <View className="flex items-center mb-1">
<Space direction="vertical"> <Space direction="vertical">
<Text className="text-xs text-gray-500"> <Text className="text-xs text-gray-500">
: {prescription.prescriptionType === 0 ? '中药' : prescription.prescriptionType === 1 ? '西药' : '未知'} : {prescription.treatmentPlan}
</Text> </Text>
<Text className="text-xs text-gray-500"> <Text className="text-xs text-gray-500">
: {prescription.diagnosis || ''} : {prescription.prescriptionType === 0 ? '中药' : prescription.prescriptionType === 1 ? '西药' : '未知'}
</Text> </Text>
{/*<Text className="text-xs text-gray-500">*/}
{/* 诊断结果: {prescription.diagnosis || '无'}*/}
{/*</Text>*/}
<Text className="text-xs text-gray-500"> <Text className="text-xs text-gray-500">
: {prescription.createTime || '未知'} : {prescription.createTime || '未知'}
</Text> </Text>

View File

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

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

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

View File

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

View File

@@ -15,8 +15,10 @@ import FixedButton from "@/components/FixedButton";
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model"; import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model"; import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
import {addClinicPrescription} from "@/api/clinic/clinicPrescription"; import {addClinicPrescription} from "@/api/clinic/clinicPrescription";
import {addClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem"; import {batchAddClinicPrescriptionItem} from "@/api/clinic/clinicPrescriptionItem";
import './confirm.scss' import './confirm.scss'
import request from "@/utils/request";
import {ApiResult} from "@/api";
// 订单数据接口 // 订单数据接口
interface OrderData { interface OrderData {
@@ -90,7 +92,7 @@ const DoctorOrderConfirm = () => {
// 第一步:创建处方主表记录 // 第一步:创建处方主表记录
console.log('开始创建处方记录...') console.log('开始创建处方记录...')
const prescriptionData: ClinicPrescription = { const prescriptionData: ClinicPrescription = {
userId: orderData.patient.userId, userId: orderData.patient.patientUserId,
doctorId: doctorId, doctorId: doctorId,
prescriptionType: orderData.prescription?.prescriptionType || 0, // 处方类型 prescriptionType: orderData.prescription?.prescriptionType || 0, // 处方类型
diagnosis: orderData.diagnosis, // 诊断结果 diagnosis: orderData.diagnosis, // 诊断结果
@@ -118,6 +120,7 @@ const DoctorOrderConfirm = () => {
// 第二步:创建处方明细记录(药品列表) // 第二步:创建处方明细记录(药品列表)
if (orderData.prescription?.items && orderData.prescription.items.length > 0) { if (orderData.prescription?.items && orderData.prescription.items.length > 0) {
console.log('开始创建处方明细...') console.log('开始创建处方明细...')
const list = []
for (const item of orderData.prescription.items) { for (const item of orderData.prescription.items) {
const prescriptionItemData = { const prescriptionItemData = {
prescriptionId: prescriptionId, // 关联处方ID prescriptionId: prescriptionId, // 关联处方ID
@@ -134,11 +137,35 @@ const DoctorOrderConfirm = () => {
userId: orderData.patient.userId, userId: orderData.patient.userId,
comments: item.comments comments: item.comments
} }
await addClinicPrescriptionItem(prescriptionItemData) list.push(prescriptionItemData)
} }
await batchAddClinicPrescriptionItem(list)
console.log('处方明细创建成功') 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) console.log('处方创建完成处方ID:', prescriptionId)
// 清除临时数据 // 清除临时数据
@@ -146,14 +173,14 @@ const DoctorOrderConfirm = () => {
Taro.showToast({ Taro.showToast({
title: '处方已发送给患者', title: '处方已发送给患者',
icon: 'success', icon: 'none',
duration: 2000 duration: 2000
}) })
setTimeout(() => { setTimeout(() => {
// 跳转到订单列表 // 跳转到订单列表
Taro.redirectTo({ Taro.redirectTo({
url: '/doctor/orders/index' url: '/clinic/clinicPatientUser/prescription'
}) })
}, 2000) }, 2000)
@@ -372,7 +399,7 @@ const DoctorOrderConfirm = () => {
{/* 底部操作按钮 */} {/* 底部操作按钮 */}
<FixedButton <FixedButton
text={submitLoading ? '发送中...' : '确认并发送给患者'} text={submitLoading ? '发送中...' : '确认并发送给患者'}
icon={<Edit />} icon={<Edit/>}
onClick={handleConfirmOrder} onClick={handleConfirmOrder}
/> />
<View className="fixed-bottom-bar"> <View className="fixed-bottom-bar">

View File

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

View File

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