feat(clinic): 添加患者管理和处方开立功能
- 新增患者管理页面,支持搜索、查看、编辑备注和删除患者 - 实现选择患者功能,用于处方开立时关联患者信息- 添加处方开立页面,支持诊断结果、治疗方案输入及图片上传- 新增用药订单页面,展示患者的历史处方订单并支持支付 - 在处方模型中增加医生姓名字段,优化处方数据显示- 扩展处方查询参数,支持按医生和用户ID筛选
This commit is contained in:
@@ -22,6 +22,8 @@ export interface ClinicPrescription {
|
||||
weight?: string;
|
||||
// 医生
|
||||
doctorId?: number;
|
||||
// 姓名
|
||||
doctorName?: string;
|
||||
// 订单编号
|
||||
orderNo?: string;
|
||||
// 关联就诊表
|
||||
@@ -68,5 +70,7 @@ export interface ClinicPrescription {
|
||||
*/
|
||||
export interface ClinicPrescriptionParam extends PageParam {
|
||||
id?: number;
|
||||
doctorId?: number;
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -104,10 +104,17 @@ export default {
|
||||
"root": "clinic",
|
||||
"pages": [
|
||||
"index",
|
||||
"clinicPatientUser/index",
|
||||
"clinicPatientUser/add",
|
||||
"clinicPatientUser/selectPatient",
|
||||
"clinicPatientUser/prescription",
|
||||
"clinicDoctorUser/index",
|
||||
"clinicDoctorUser/add",
|
||||
"clinicPrescription/index",
|
||||
"clinicPrescription/add",
|
||||
"clinicPrescription/selectPrescription",
|
||||
"clinicPrescription/confirm",
|
||||
"clinicPrescription/detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
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;
|
||||
4
src/clinic/clinicPatientUser/prescription.config.ts
Normal file
4
src/clinic/clinicPatientUser/prescription.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '用药订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
142
src/clinic/clinicPatientUser/prescription.tsx
Normal file
142
src/clinic/clinicPatientUser/prescription.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ClinicPrescription} from "@/api/clinic/clinicPrescription/model";
|
||||
import {
|
||||
pageClinicPrescription,
|
||||
removeClinicPrescription
|
||||
} from "@/api/clinic/clinicPrescription";
|
||||
import {copyText} from "@/utils/common";
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {buildSingleGoodsOrder, PaymentHandler} from "@/utils/payment";
|
||||
|
||||
const ClinicPrescriptionList = () => {
|
||||
const [list, setList] = useState<ClinicPrescription[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const reload = () => {
|
||||
setLoading(true)
|
||||
pageClinicPrescription({
|
||||
// 添加查询条件
|
||||
userId: Taro.getStorageSync('UserId'),
|
||||
})
|
||||
.then(data => {
|
||||
setList(data?.list || [])
|
||||
})
|
||||
.catch(() => {
|
||||
Taro.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'error'
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 统一支付入口
|
||||
*/
|
||||
const onPay = async (goods: ShopGoods) => {
|
||||
try {
|
||||
// 构建订单数据
|
||||
const orderData = buildSingleGoodsOrder(
|
||||
);
|
||||
|
||||
// 执行支付
|
||||
await PaymentHandler.pay(orderData, 1);
|
||||
|
||||
} catch (error: any) {
|
||||
}
|
||||
};
|
||||
|
||||
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.orderNo}
|
||||
extra={
|
||||
<Tag type={'warning'} className="font-medium">待支付</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
|
||||
type={'warning'}
|
||||
onClick={() => onPay(item)}
|
||||
>
|
||||
去支付
|
||||
</Button>
|
||||
</Space>
|
||||
</Cell>
|
||||
</CellGroup>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '新增处方主表',
|
||||
navigationBarTitleText: '开处方',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
@@ -1,51 +1,272 @@
|
||||
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) => {
|
||||
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 +280,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 +326,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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
408
src/clinic/clinicPrescription/confirm.tsx
Normal file
408
src/clinic/clinicPrescription/confirm.tsx
Normal file
@@ -0,0 +1,408 @@
|
||||
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>
|
||||
<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,6 +1,6 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag, Divider} from '@nutui/nutui-react-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";
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
removeClinicPrescription
|
||||
} from "@/api/clinic/clinicPrescription";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {copyText} from "@/utils/common";
|
||||
|
||||
const ClinicPrescriptionList = () => {
|
||||
const [list, setList] = useState<ClinicPrescription[]>([])
|
||||
@@ -18,6 +19,7 @@ const ClinicPrescriptionList = () => {
|
||||
setLoading(true)
|
||||
pageClinicPrescription({
|
||||
// 添加查询条件
|
||||
doctorId: Taro.getStorageSync('UserId'),
|
||||
})
|
||||
.then(data => {
|
||||
setList(data?.list || [])
|
||||
@@ -62,41 +64,10 @@ const ClinicPrescriptionList = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取处方类型文本
|
||||
const getPrescriptionTypeText = (type?: number) => {
|
||||
return type === 0 ? '中药' : type === 1 ? '西药' : '未知'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return '正常'
|
||||
case 1:
|
||||
return '已完成'
|
||||
case 2:
|
||||
return '已支付'
|
||||
case 3:
|
||||
return '已取消'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusType = (status?: number): 'primary' | 'success' | 'danger' | 'warning' | 'default' => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 'primary'
|
||||
case 1:
|
||||
return 'success'
|
||||
case 2:
|
||||
return 'success'
|
||||
case 3:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
const onDetail = (item: ClinicPrescription) => {
|
||||
Taro.navigateTo({
|
||||
url: `/clinic/clinicPrescription/detail?id=${item.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const getSexName = (sex?: number) => {
|
||||
@@ -138,28 +109,30 @@ const ClinicPrescriptionList = () => {
|
||||
{list.map((item) => (
|
||||
<CellGroup key={item.id} className="mb-3">
|
||||
<Cell
|
||||
title={
|
||||
<View className="flex items-center justify-between">
|
||||
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>
|
||||
|
||||
<Tag type={getStatusType(item.status)}>
|
||||
{getStatusText(item.status)}
|
||||
</Tag>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
<Cell
|
||||
title="处方类型"
|
||||
extra={
|
||||
<Tag type="info">
|
||||
{getPrescriptionTypeText(item.prescriptionType)}
|
||||
</Tag>
|
||||
}
|
||||
/>
|
||||
{/*<Cell*/}
|
||||
{/* title="处方类型"*/}
|
||||
{/* extra={*/}
|
||||
{/* <Tag type="info">*/}
|
||||
{/* {getPrescriptionTypeText(item.prescriptionType)}*/}
|
||||
{/* </Tag>*/}
|
||||
{/* }*/}
|
||||
{/*/>*/}
|
||||
{item.diagnosis && (
|
||||
<Cell
|
||||
title="诊断结果"
|
||||
@@ -172,18 +145,6 @@ const ClinicPrescriptionList = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{item.treatmentPlan && (
|
||||
<Cell
|
||||
title="治疗方案"
|
||||
extra={
|
||||
<Text className="text-gray-600 text-sm">
|
||||
{item.treatmentPlan.length > 20
|
||||
? `${item.treatmentPlan.substring(0, 20)}...`
|
||||
: item.treatmentPlan}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Cell
|
||||
title="订单金额"
|
||||
extra={
|
||||
@@ -192,16 +153,6 @@ const ClinicPrescriptionList = () => {
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
{item.items && item.items.length > 0 && (
|
||||
<Cell
|
||||
title="药品数量"
|
||||
extra={
|
||||
<Tag type="warning">
|
||||
共 {item.items.length} 味药
|
||||
</Tag>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Cell
|
||||
title="创建时间"
|
||||
extra={
|
||||
@@ -210,24 +161,17 @@ const ClinicPrescriptionList = () => {
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
{item.comments && (
|
||||
<Cell
|
||||
title="备注"
|
||||
extra={
|
||||
<Text className="text-gray-600 text-sm">
|
||||
{item.comments.length > 30
|
||||
? `${item.comments.substring(0, 30)}...`
|
||||
: item.comments}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<Cell>
|
||||
<Space className="w-full justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Edit/>}
|
||||
onClick={() => onDetail(item)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<Edit/>}
|
||||
onClick={() => onEdit(item)}
|
||||
>
|
||||
@@ -235,7 +179,6 @@ const ClinicPrescriptionList = () => {
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="danger"
|
||||
icon={<Del/>}
|
||||
onClick={() => onDel(item)}
|
||||
>
|
||||
|
||||
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'
|
||||
})
|
||||
234
src/clinic/clinicPrescription/selectPrescription.tsx
Normal file
234
src/clinic/clinicPrescription/selectPrescription.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
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.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"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '在线开方',
|
||||
navigationBarTitleText: '开处方',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
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;
|
||||
}
|
||||
60
src/pages/index/Role.tsx
Normal file
60
src/pages/index/Role.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {Space, Image} from '@nutui/nutui-react-taro'
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {goTo} from "@/utils/navigation"
|
||||
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 {
|
||||
loading: shopLoading,
|
||||
error,
|
||||
getNavigation
|
||||
} = useShopInfo()
|
||||
|
||||
// 获取顶部导航菜单
|
||||
const navigation = getNavigation()
|
||||
const home = navigation.topNavs.find(item => item.model == 'index')
|
||||
const navItems = navigation.topNavs.filter(item => item.parentId == home?.navigationId) || []
|
||||
|
||||
const onNav = (item: any) => {
|
||||
if (item.path) {
|
||||
return goTo(`${item.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 +159,8 @@ function Home() {
|
||||
<Header />
|
||||
|
||||
<div className={'flex flex-col mt-12'}>
|
||||
<Menu/>
|
||||
{/*<Menu/>*/}
|
||||
<Role />
|
||||
<Banner/>
|
||||
<GoodsList onStickyChange={handleTabsStickyChange}/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user