This commit is contained in:
xx
2026-02-28 10:05:36 +08:00
parent debed766d5
commit e37d605e38
12 changed files with 970 additions and 887 deletions

1697
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,10 @@ const API_CONFIG = {
// 内网地址HTTP
INTERNAL_URL: 'http://10.1.5.118:8077/platform-api',
// 外网地址HTTPS
EXTERNAL_URL: 'https://apihis.circleharmonyhospital.cn:8982/platform-api',
// 默认使用外网地址,可根据环境变量切换
// 开发环境使用内网,生产环境使用外网
EXTERNAL_URL: 'http://apihis.circleharmonyhospital.cn:8982/platform-api',
BASE_URL: import.meta.env.NODE_ENV === 'development'
? 'http://10.1.5.118:8077/platform-api'
: '/platform-api',
// 请求超时时间120秒
: 'http://apihis.circleharmonyhospital.cn:8982/platform-api',
TIMEOUT: 120000,
};
@@ -63,7 +60,10 @@ request.interceptors.response.use(
localStorage.removeItem('operatorName');
localStorage.removeItem('operatorUsername');
// 跳转到首页并添加登录参数
window.location.href = '/home?login=true';
const baseUrl = import.meta.env.NODE_ENV === 'development' ? '/' : '/tijian-zongkong/';
window.location.href = `${baseUrl}#/home?login=true`;
// navigate('/home?login=true');
// return;
}
break;
case 403:

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import type { ExamClient } from '../../data/mockData';
import { searchPhysicalExamAddItem, getAddItemCustomerInfo, getChannelCompanyList, createNativePaymentQrcode, checkNativePaymentStatus, getAddItemBillPdf, getCustomSettlementApproveStatus, customSettlementApply, customSettlementApplyCancel } from '../../api';
@@ -30,6 +31,7 @@ interface ExamAddonPanelProps {
}
export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
const navigate = useNavigate();
const [addonList, setAddonList] = useState<AddonItem[]>([]);
const allAddons = useMemo(() => addonList, [addonList]);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -589,7 +591,8 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
// 等待计时器:当审核中时开始计时
useEffect(() => {
if (customSettlementStatus?.apply_status === 1) {
const isWaiting = customSettlementStatus?.apply_status === 1 || (paymentLoading && !showQrcodeModal);
if (isWaiting) {
// 开始计时
setWaitingSeconds(0);
waitingTimerRef.current = window.setInterval(() => {
@@ -610,7 +613,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
waitingTimerRef.current = null;
}
};
}, [customSettlementStatus?.apply_status]);
}, [customSettlementStatus?.apply_status, paymentLoading, showQrcodeModal]);
// 提交自定义结算申请
const handleSubmitCustomSettlement = async () => {
@@ -678,7 +681,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
const apply_user = localStorage.getItem('operatorName');
if (!apply_user) {
alert('请先登录');
window.location.href = '/home';
navigate('/home');
return;
}
const res = await customSettlementApply({
@@ -813,6 +816,8 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
clearInterval(pollingTimerRef.current);
}
setPaymentMessage('等待支付确认...');
pollingTimerRef.current = window.setInterval(async () => {
try {
const res = await checkNativePaymentStatus({
@@ -844,10 +849,14 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
fetchAddItemBillPdf(physical_exam_id, combinationItemCodes).then((success) => {
if (success) {
setPaymentMessage('支付成功,加项单已生成,正在跳转签署...');
// 跳转到签署页面
onGoToSign?.();
// 延迟跳转,让用户看到消息
setTimeout(() => {
onGoToSign?.();
setPaymentLoading(false);
}, 1000);
} else {
setPaymentMessage('支付成功,但加项单生成失败');
setPaymentLoading(false);
}
setTimeout(() => {
setPaymentMessage(null);
@@ -859,6 +868,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPaymentLoading(false);
setPaymentMessage('支付失败');
setTimeout(() => {
setPaymentMessage(null);
@@ -888,7 +898,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
}
setPaymentLoading(true);
setPaymentMessage(null);
setPaymentMessage('正在发起支付...');
try {
const selectedItems = allAddons.filter((item) => selectedIds.has(item.id || item.name));
@@ -985,6 +995,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
);
} else {
setPaymentMessage(res.Message || '生成支付二维码失败');
setPaymentLoading(false);
}
} else {
// 挂账模式:直接查询
@@ -1025,9 +1036,13 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
fetchAddItemBillPdf(physical_exam_id, combinationItemCodes).then((success) => {
if (success) {
setPaymentMessage('挂账成功,加项单已生成,正在跳转签署...');
onGoToSign?.();
setTimeout(() => {
onGoToSign?.();
setPaymentLoading(false);
}, 1000);
} else {
setPaymentMessage('挂账成功,但加项单生成失败');
setPaymentLoading(false);
}
setTimeout(() => {
setPaymentMessage(null);
@@ -1035,12 +1050,14 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
});
} else {
setPaymentMessage('挂账失败');
setPaymentLoading(false);
setTimeout(() => {
setPaymentMessage(null);
}, 3000);
}
} else {
setPaymentMessage(res.Message || '挂账失败');
setPaymentLoading(false);
}
}
} catch (err) {
@@ -1392,6 +1409,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPaymentLoading(false);
setShowQrcodeModal(false);
setQrcodeUrl(null);
}}
@@ -1435,15 +1453,15 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
</button>
</div>
<div className='px-4 py-6 space-y-4'>
<div className='px-4 py-1 space-y-4'>
{/* 选中项目信息 */}
<div className='text-sm text-gray-700'>
<div className='font-medium mb-2'> ({selectedItems.length}):</div>
<div className='max-h-32 overflow-y-auto space-y-1'>
<div className='max-h-[44px] overflow-y-auto space-y-0.5 pr-1'>
{selectedItems.map((item, idx) => (
<div key={idx} className='text-xs text-gray-600 flex justify-between'>
<span>{item.name}</span>
<span>¥{parseFloat(item.currentPrice || item.originalPrice || '0').toFixed(2)}</span>
<div key={idx} className='text-[11px] text-gray-500 flex justify-between'>
<span className='truncate mr-2'>{item.name}</span>
<span className='flex-shrink-0 font-mono'>¥{parseFloat(item.currentPrice || item.originalPrice || '0').toFixed(2)}</span>
</div>
))}
</div>
@@ -1577,7 +1595,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
<div className='flex gap-3 pt-2'>
<Button
onClick={() => setShowCustomSettlementModal(false)}
className='flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700'
className='flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 text-center items-center justify-center'
>
{isApprovedOrRejected ? '关闭' : '取消'}
</Button>
@@ -1586,7 +1604,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
onClick={handleSubmitCustomSettlement}
disabled={customSettlementLoading || !customApplyReason.trim()}
className={cls(
'flex-1 px-4 py-2 text-white font-medium',
'flex-1 px-4 py-2 text-white font-medium text-center items-center justify-center',
customSettlementLoading || !customApplyReason.trim()
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
@@ -1602,25 +1620,33 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
)
}
{/* 审核中全屏遮罩 */}
{/* 审核中/支付中全屏遮罩 */}
{
customSettlementStatus?.apply_status === 1 && (
(customSettlementStatus?.apply_status === 1 || (paymentLoading && !showQrcodeModal)) && (
<div className='fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50'>
<div className='bg-white rounded-2xl p-6 max-w-md w-full mx-4 shadow-xl'>
<div className='text-center mb-6'>
<div className='text-lg font-semibold text-blue-600 mb-2'>...</div>
<div className='text-sm text-gray-500 mb-1'></div>
<div className='text-lg font-semibold text-blue-600 mb-2'>
{customSettlementStatus?.apply_status === 1 ? '审核中...' : '支付处理中...'}
</div>
<div className='text-sm text-gray-500 mb-1'>
{customSettlementStatus?.apply_status === 1
? '正在等待审核结果,请稍候'
: (paymentMessage || '正在处理,请稍候...')}
</div>
<div className='text-xs text-gray-400'> {waitingSeconds} </div>
</div>
<div className='flex justify-center'>
<Button
onClick={handleCancelCustomSettlement}
disabled={customSettlementLoading}
className='px-6 py-2 bg-gray-500 hover:bg-gray-600 text-white disabled:opacity-50'
>
{customSettlementLoading ? '取消中...' : '取消申请'}
</Button>
</div>
{customSettlementStatus?.apply_status === 1 && (
<div className='flex justify-center'>
<Button
onClick={handleCancelCustomSettlement}
disabled={customSettlementLoading}
className='px-6 py-2 bg-gray-500 hover:bg-gray-600 text-white disabled:opacity-50'
>
{customSettlementLoading ? '取消中...' : '取消申请'}
</Button>
</div>
)}
</div>
</div>
)

View File

@@ -117,66 +117,66 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
{infoLoading && (
<div className='mb-3 text-xs text-gray-500'>...</div>
)}
<div className='grid grid-cols-2 gap-3 mb-3'>
<div className='grid grid-cols-2 gap-x-3 gap-y-1.5 mb-2'>
<div>
<span className='text-[11px] text-gray-500'></span>
<Input
placeholder='请输入收件人姓名'
className='mt-1'
className='mt-0.5 h-8 text-xs'
value={addressContact}
onChange={(e) => setAddressContact(e.target.value)}
/>
</div>
<div>
<span className='text-[11px] text-gray-500'></span>
<Input
placeholder='用于快递联系'
className='mt-1'
className='mt-0.5 h-8 text-xs'
value={addressMobile}
onChange={(e) => setAddressMobile(e.target.value)}
/>
</div>
<div>
<span className='text-[11px] text-gray-500'></span>
<Input
placeholder='例如:上海市'
className='mt-1'
className='mt-0.5 h-8 text-xs'
value={provinceName}
onChange={(e) => setProvinceName(e.target.value)}
/>
</div>
<div>
<span className='text-[11px] text-gray-500'></span>
<Input
placeholder='例如:上海市'
className='mt-1'
className='mt-0.5 h-8 text-xs'
value={cityName}
onChange={(e) => setCityName(e.target.value)}
/>
</div>
<div>
<span className='text-[11px] text-gray-500'></span>
<Input
placeholder='例如:浦东新区'
className='mt-1'
className='mt-0.5 h-8 text-xs'
value={countryName}
onChange={(e) => setCountryName(e.target.value)}
/>
</div>
<div className='col-span-2'>
<span className='text-[11px] text-gray-500'></span>
<Input
placeholder='请输入详细寄送地址'
className='mt-1'
className='mt-0.5 h-8 text-xs'
value={addressContent}
onChange={(e) => setAddressContent(e.target.value)}
/>
</div>
</div>
<div className='space-y-2'>
<div></div>
<div className='space-y-1'>
<div className='text-[11px] text-gray-500'></div>
<textarea
className='w-full rounded-2xl border px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-gray-200 min-h-[80px]'
className='w-full rounded-xl border px-3 py-1.5 text-xs outline-none focus:ring-2 focus:ring-gray-200 min-h-[60px] resize-none'
placeholder='如需多份报告、加急寄送等,请在此备注'
value={addressRemark}
onChange={(e) => setAddressRemark(e.target.value)}
@@ -190,12 +190,12 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
{saveMessage}
</div>
)}
<div className='mt-4 flex items-center justify-between text-[11px] text-gray-500'>
<div>
<span className='font-medium text-gray-800'>{client.name}</span>{client.id}
<div className='mt-2 flex items-center justify-between text-[10px] text-gray-500'>
<div className='truncate mr-2'>
<span className='font-medium text-gray-800'>{client.name}</span> ({client.id})
</div>
<Button
className='px-4 py-1.5 text-xs'
className='px-4 py-1.5 h-8 text-xs flex-shrink-0'
onClick={async () => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id) {

View File

@@ -127,7 +127,7 @@ export const ExamSection = ({
const hasMore = displayCount < filteredClients.length;
return (
<div className='space-y-4'>
<div className='space-y-4 h-full overflow-y-auto p-4 pb-10'>
<Card>
<CardHeader></CardHeader>
<CardContent>

View File

@@ -418,7 +418,6 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
}, [examId, optionalConfirmed]);
const handlePickFile = () => {
// 有可选项目但尚未确认时,禁止拍照并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
@@ -465,24 +464,25 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file) {
return;
}
setMessage(null);
try {
const jpgFile = await convertToJpg(file);
setIdCardFile(jpgFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(jpgFile);
} catch (err) {
alert('err: ' + String(err));
console.error('图片转换失败', err);
setMessage('图片处理失败,请重试');
}
e.target.value = '';
};

View File

@@ -180,7 +180,7 @@ export const HomeSection = () => {
</Card>
)}
<div className='grid grid-cols-2 gap-4'>
<div className='grid grid-cols-2 gap-4 pb-4'>
<Card>
<CardHeader>B1 </CardHeader>
<CardContent>

View File

@@ -10,7 +10,7 @@ interface TopBarProps {
onLogout?: () => void;
}
export const TopBar = ({ enableSearch = true, operatorName, onLoginClick, onLogout }: TopBarProps) => {
export const TopBar = ({ operatorName, onLoginClick, onLogout }: TopBarProps) => {
const [showMenu, setShowMenu] = useState(false);
const [displayName, setDisplayName] = useState(operatorName || '');

View File

@@ -1,4 +1,4 @@
import { Card, CardContent, CardHeader } from '../ui';
import { Card, CardContent } from '../ui';
export const SupportSection = () => (
<Card>

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { ExamClient, ExamModalTab } from '../data/mockData';
import { EXAM_TAGS } from '../data/mockData';
@@ -17,13 +18,14 @@ export const ExamPage = () => {
const [examFilterTags, setExamFilterTags] = useState<Set<(typeof EXAM_TAGS)[number]>>(new Set(['全部']));
const [loading, setLoading] = useState(false);
const [refreshSeq, setRefreshSeq] = useState(0);
const navigate = useNavigate();
// 进入页面时获取用户菜单权限
useEffect(() => {
const token = localStorage.getItem('accessToken');
if (!token) {
// 跳转登录
window.location.href = '/home';
navigate('/home');
return;
}
getUserOwnedMenus({ app_id: APP_ID })

View File

@@ -1,4 +1,4 @@
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { createHashRouter, Navigate } from 'react-router-dom';
import { MainLayout } from './layouts/MainLayout';
import { HomePage } from './pages/HomePage';
@@ -6,7 +6,7 @@ import { ExamPage } from './pages/ExamPage';
import { BookingPage } from './pages/BookingPage';
import { SupportPage } from './pages/SupportPage';
export const router = createBrowserRouter([
export const router = createHashRouter([
{
path: '/',
element: <MainLayout />,

View File

@@ -3,8 +3,12 @@ import react from '@vitejs/plugin-react-swc'
// https://vite.dev/config/
export default defineConfig({
base: "./",
plugins: [react()],
server: {
allowedHosts: ['ipad.shenynet.com'],
},
resolve: {
dedupe: ['react', 'react-dom']
},
})