1.0
This commit is contained in:
1697
package-lock.json
generated
1697
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -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('支付成功,加项单已生成,正在跳转签署...');
|
||||
// 跳转到签署页面
|
||||
// 延迟跳转,让用户看到消息
|
||||
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('挂账成功,加项单已生成,正在跳转签署...');
|
||||
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,16 +1620,23 @@ 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>
|
||||
{customSettlementStatus?.apply_status === 1 && (
|
||||
<div className='flex justify-center'>
|
||||
<Button
|
||||
onClick={handleCancelCustomSettlement}
|
||||
@@ -1621,6 +1646,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
|
||||
{customSettlementLoading ? '取消中...' : '取消申请'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = '';
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || '');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, CardContent, CardHeader } from '../ui';
|
||||
import { Card, CardContent } from '../ui';
|
||||
|
||||
export const SupportSection = () => (
|
||||
<Card>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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']
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user