Compare commits

..

13 Commits

Author SHA1 Message Date
xx
e420897878 1.0.2 2026-02-28 16:23:03 +08:00
xx
d451cb1172 1.0.1 2026-02-28 15:09:53 +08:00
xx
e37d605e38 1.0 2026-02-28 10:05:36 +08:00
xx
debed766d5 优化比例 2026-02-05 14:42:26 +08:00
xx
abc0a6051d 更新需求 2026-02-05 11:25:03 +08:00
xianyi
513e113ea9 更新结算价 2026-02-04 09:22:36 +08:00
xianyi
07c74c956e InputAddItemCombinationInfo 2026-02-02 16:37:59 +08:00
xianyi
29f6a6e696 添加orderAmount 2026-02-02 16:14:55 +08:00
xianyi
e2158286be 修复体检中心登录与表单 2026-02-02 10:06:20 +08:00
xianyi
db6a8bc97f 修复回显 2026-01-28 15:46:31 +08:00
xianyi
41ce02512a 申请人 2026-01-28 13:56:09 +08:00
xianyi
d09bfc4ec6 修复 2026-01-28 11:41:34 +08:00
xianyi
f75e19cf85 调整样式 2026-01-27 16:35:56 +08:00
28 changed files with 2251 additions and 1466 deletions

View File

@@ -1,5 +1,7 @@
# React + TypeScript + Vite # React + TypeScript + Vite
`cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs`
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available: Currently, two official plugins are available:

2540
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,14 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "export NODE_ENV=development && vite --host 0.0.0.0", "dev": "export NODE_ENV=development && vite --host 0.0.0.0 --port 5777",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "^1.13.2", "axios": "^1.13.2",
"pdfjs-dist": "^5.4.449", "pdfjs-dist": "^4.4.168",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.9.6" "react-router-dom": "^7.9.6"

21
public/pdf.worker.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,11 @@ import { RouterProvider } from 'react-router-dom';
import { router } from './router'; import { router } from './router';
function App() { function App() {
return <RouterProvider router={router} />; return (
<div className='h-screen max-h-screen overflow-hidden'>
<RouterProvider router={router} />
</div>
);
} }
export default App; export default App;

View File

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

View File

@@ -548,6 +548,8 @@ export interface InputPhysicalExamAddItem {
scrm_account_name?: string | null; scrm_account_name?: string | null;
/** 项目名称(默认空值,传入项目名称过滤数据) */ /** 项目名称(默认空值,传入项目名称过滤数据) */
item_name?: string; item_name?: string;
/** 折扣率 */
discount_rate?: number | null;
} }
/** /**
@@ -720,11 +722,11 @@ export type PhysicalExamQrcodeCreateResponse = CommonActionResult<string>;
*/ */
export interface InputAddItemCombinationInfo { export interface InputAddItemCombinationInfo {
/** 体检组合项目代码 */ /** 体检组合项目代码 */
combination_item_code: string; combination_item_code?: string | null;
/** 体检组合项目价格 */ /** 体检组合项目价格 */
combination_item_price: number; combination_item_price?: number | null;
/** 折扣比例 */ /** 折扣比例 */
discount_rate: number; discount_rate?: number | null;
} }
/** /**
@@ -741,6 +743,8 @@ export interface InputOrderPaymentInfo {
pay_type: number; pay_type: number;
/** 挂账公司ID挂账公司传对应的ID其他传0 */ /** 挂账公司ID挂账公司传对应的ID其他传0 */
company_id: number; company_id: number;
/** 订单总金额 */
orderAmount: number;
} }
/** /**
@@ -1396,6 +1400,15 @@ export interface InputCustomSettlementApplyApprove {
add_item_id?: string; add_item_id?: string;
} }
/**
* 体检加项自定义结算申请-加项组合项
*/
export interface OutputCustomSettlementApplyApproveItem {
combination_item_code?: string | null;
combination_item_price?: number | null;
discount_rate?: number | null;
}
/** /**
* 体检加项自定义结算申请状态编辑出参 * 体检加项自定义结算申请状态编辑出参
*/ */
@@ -1408,6 +1421,12 @@ export interface OutputCustomSettlementApplyApprove {
apply_status_name?: string | null; apply_status_name?: string | null;
/** 最终结算金额 */ /** 最终结算金额 */
final_settlement_price?: number | null; final_settlement_price?: number | null;
/** 申请理由 */
apply_reason?: string;
/** 折扣 */
discount_ratio?: number | null;
/** 加项组合列表 */
listAddItemCombination?: OutputCustomSettlementApplyApproveItem[] | null;
} }
/** /**
@@ -1427,6 +1446,8 @@ export interface InputAddItemCustomSettlementDetail {
original_price: number; original_price: number;
/** 结算金额 */ /** 结算金额 */
settlement_price: number; settlement_price: number;
/** 折扣率 */
discount_ratio?: number | null;
} }
/** /**
@@ -1447,6 +1468,8 @@ export interface InputCustomSettlementApply {
final_settlement_price?: number; final_settlement_price?: number;
/** 申请理由 */ /** 申请理由 */
apply_reason?: string; apply_reason?: string;
/** 申请人 */
apply_user: string;
} }
/** /**
@@ -1470,6 +1493,8 @@ export interface InputCustomSettlementApplyCancel {
physical_exam_id: number; physical_exam_id: number;
/** 体检加项组合ID多个逗号分隔例如123,456 */ /** 体检加项组合ID多个逗号分隔例如123,456 */
add_item_id: string; add_item_id: string;
/** 申请人 */
cancel_user: string;
} }
/** /**

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because one or more lines are too long

BIN
src/assets/sign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -113,70 +113,70 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
</div> </div>
{viewMode === 'form' ? ( {viewMode === 'form' ? (
<> <div className='overflow-y-auto overflow-x-hidden max-h-[clamp(260px,calc(100vh-520px),560px)] pr-1'>
{infoLoading && ( {infoLoading && (
<div className='mb-3 text-xs text-gray-500'>...</div> <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> <div>
<span className='text-[11px] text-gray-500'></span>
<Input <Input
placeholder='请输入收件人姓名' placeholder='请输入收件人姓名'
className='mt-1' className='mt-0.5 h-8 text-xs'
value={addressContact} value={addressContact}
onChange={(e) => setAddressContact(e.target.value)} onChange={(e) => setAddressContact(e.target.value)}
/> />
</div> </div>
<div> <div>
<span className='text-[11px] text-gray-500'></span>
<Input <Input
placeholder='用于快递联系' placeholder='用于快递联系'
className='mt-1' className='mt-0.5 h-8 text-xs'
value={addressMobile} value={addressMobile}
onChange={(e) => setAddressMobile(e.target.value)} onChange={(e) => setAddressMobile(e.target.value)}
/> />
</div> </div>
<div> <div>
<span className='text-[11px] text-gray-500'></span>
<Input <Input
placeholder='例如:上海市' placeholder='例如:上海市'
className='mt-1' className='mt-0.5 h-8 text-xs'
value={provinceName} value={provinceName}
onChange={(e) => setProvinceName(e.target.value)} onChange={(e) => setProvinceName(e.target.value)}
/> />
</div> </div>
<div> <div>
<span className='text-[11px] text-gray-500'></span>
<Input <Input
placeholder='例如:上海市' placeholder='例如:上海市'
className='mt-1' className='mt-0.5 h-8 text-xs'
value={cityName} value={cityName}
onChange={(e) => setCityName(e.target.value)} onChange={(e) => setCityName(e.target.value)}
/> />
</div> </div>
<div> <div>
<span className='text-[11px] text-gray-500'></span>
<Input <Input
placeholder='例如:浦东新区' placeholder='例如:浦东新区'
className='mt-1' className='mt-0.5 h-8 text-xs'
value={countryName} value={countryName}
onChange={(e) => setCountryName(e.target.value)} onChange={(e) => setCountryName(e.target.value)}
/> />
</div> </div>
<div className='col-span-2'> <div className='col-span-2'>
<span className='text-[11px] text-gray-500'></span>
<Input <Input
placeholder='请输入详细寄送地址' placeholder='请输入详细寄送地址'
className='mt-1' className='mt-0.5 h-8 text-xs'
value={addressContent} value={addressContent}
onChange={(e) => setAddressContent(e.target.value)} onChange={(e) => setAddressContent(e.target.value)}
/> />
</div> </div>
</div> </div>
<div className='space-y-2'> <div className='space-y-1'>
<div></div> <div className='text-[11px] text-gray-500'></div>
<textarea <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='如需多份报告、加急寄送等,请在此备注' placeholder='如需多份报告、加急寄送等,请在此备注'
value={addressRemark} value={addressRemark}
onChange={(e) => setAddressRemark(e.target.value)} onChange={(e) => setAddressRemark(e.target.value)}
@@ -190,12 +190,12 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
{saveMessage} {saveMessage}
</div> </div>
)} )}
<div className='mt-4 flex items-center justify-between text-[11px] text-gray-500'> <div className='mt-2 flex items-center justify-between text-[10px] text-gray-500'>
<div> <div className='truncate mr-2'>
<span className='font-medium text-gray-800'>{client.name}</span>{client.id} <span className='font-medium text-gray-800'>{client.name}</span> ({client.id})
</div> </div>
<Button <Button
className='px-4 py-1.5 text-xs' className='px-4 py-1.5 h-8 text-xs flex-shrink-0'
onClick={async () => { onClick={async () => {
const physical_exam_id = Number(client.id); const physical_exam_id = Number(client.id);
if (!physical_exam_id) { if (!physical_exam_id) {
@@ -261,7 +261,7 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
{saveLoading ? '保存中...' : '保存寄送信息'} {saveLoading ? '保存中...' : '保存寄送信息'}
</Button> </Button>
</div> </div>
</> </div>
) : ( ) : (
<div className='flex flex-col items-center justify-center py-8'> <div className='flex flex-col items-center justify-center py-8'>
{qrcodeLoading ? ( {qrcodeLoading ? (
@@ -271,7 +271,7 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
<img <img
src={qrcodeUrl.startsWith('data:') ? qrcodeUrl : `data:image/png;base64,${qrcodeUrl}`} src={qrcodeUrl.startsWith('data:') ? qrcodeUrl : `data:image/png;base64,${qrcodeUrl}`}
alt='报告寄送登记二维码' alt='报告寄送登记二维码'
className='max-w-full max-h-[400px] object-contain' className='max-w-full max-h-[clamp(240px,calc(100vh-520px),400px)] object-contain'
/> />
<div className='text-xs text-gray-500'></div> <div className='text-xs text-gray-500'></div>
</div> </div>

View File

@@ -16,6 +16,7 @@ interface ExamDetailPanelProps {
addItemInfoList: CustomerExamAddItem[] | null; addItemInfoList: CustomerExamAddItem[] | null;
progressList: PhysicalExamProgressItem[] | null; progressList: PhysicalExamProgressItem[] | null;
loading: boolean; loading: boolean;
onCustomerUpdated?: () => void;
} }
const getMaritalCodeFromText = (text: string): number => { const getMaritalCodeFromText = (text: string): number => {
@@ -33,6 +34,7 @@ export const ExamDetailPanel = ({
addItemInfoList, addItemInfoList,
progressList, progressList,
loading, loading,
onCustomerUpdated,
}: ExamDetailPanelProps) => { }: ExamDetailPanelProps) => {
const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || ''; const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || '';
const baseMaritalText = const baseMaritalText =
@@ -108,6 +110,7 @@ export const ExamDetailPanel = ({
if (res.Status === 200) { if (res.Status === 200) {
setEditMessage('保存成功'); setEditMessage('保存成功');
setPhoneEditing(false); setPhoneEditing(false);
onCustomerUpdated?.();
setTimeout(() => setEditMessage(null), 2000); setTimeout(() => setEditMessage(null), 2000);
} else { } else {
setEditMessage(res.Message || '保存失败'); setEditMessage(res.Message || '保存失败');
@@ -140,6 +143,7 @@ export const ExamDetailPanel = ({
if (res.Status === 200) { if (res.Status === 200) {
setEditMessage('保存成功'); setEditMessage('保存成功');
setMaritalEditing(false); setMaritalEditing(false);
onCustomerUpdated?.();
setTimeout(() => setEditMessage(null), 2000); setTimeout(() => setEditMessage(null), 2000);
} else { } else {
setEditMessage(res.Message || '保存失败'); setEditMessage(res.Message || '保存失败');

View File

@@ -76,6 +76,16 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
.finally(() => setDetailLoading(false)); .finally(() => setDetailLoading(false));
}, [client.id]); }, [client.id]);
const refetchDetail = () => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id) return;
getCustomerDetail({ physical_exam_id }).then((detailRes) => {
setCustomerInfo(detailRes.Data?.customerInfo ?? null);
setAppointmentInfo(detailRes.Data?.appointmentInfo ?? null);
setAddItemInfoList(detailRes.Data?.addItemInfoList ?? null);
});
};
return ( return (
<div <div
className='fixed inset-0 z-40 flex items-center justify-center bg-black/50' className='fixed inset-0 z-40 flex items-center justify-center bg-black/50'
@@ -153,6 +163,7 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
addItemInfoList={addItemInfoList} addItemInfoList={addItemInfoList}
progressList={progressList} progressList={progressList}
loading={detailLoading} loading={detailLoading}
onCustomerUpdated={refetchDetail}
/> />
)} )}
{tab === 'sign' && <ExamSignPanel examId={Number(client.id)} onBusyChange={setSignBusy} />} {tab === 'sign' && <ExamSignPanel examId={Number(client.id)} onBusyChange={setSignBusy} />}

View File

@@ -1,13 +1,11 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import { getDaojiandanPdf as getDaojiandanPdfApi, submitDaojiandanSign, editDaojiandanPrintStatus } from '../../api'; import { getDaojiandanPdf as getDaojiandanPdfApi, submitDaojiandanSign, editDaojiandanPrintStatus } from '../../api';
import type { ExamClient } from '../../data/mockData'; import type { ExamClient } from '../../data/mockData';
import type { SignaturePadHandle } from '../ui'; import type { SignaturePadHandle } from '../ui';
import { Button, SignaturePad } from '../ui'; import { Button, SignaturePad } from '../ui';
// Polyfill for Promise.withResolvers
if (typeof (Promise as any).withResolvers === 'undefined') { if (typeof (Promise as any).withResolvers === 'undefined') {
(Promise as any).withResolvers = function <T>() { (Promise as any).withResolvers = function <T>() {
let resolve!: (value: T | PromiseLike<T>) => void; let resolve!: (value: T | PromiseLike<T>) => void;
@@ -20,8 +18,14 @@ if (typeof (Promise as any).withResolvers === 'undefined') {
}; };
} }
// 配置 PDF.js worker if (typeof (Promise as any).try === 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; (Promise as any).try = function (fn: () => any) {
return new Promise((resolve) => resolve(fn()));
};
}
pdfjsLib.GlobalWorkerOptions.workerSrc = import.meta.env.MODE === 'development' ? '/pdf.worker.min.js' : '/tijian-zongkong/pdf.worker.min.js';
export const ExamPrintPanel = ({ client }: { client: ExamClient }) => { export const ExamPrintPanel = ({ client }: { client: ExamClient }) => {
const [pdfUrl, setPdfUrl] = useState<string | null>(null); const [pdfUrl, setPdfUrl] = useState<string | null>(null);

View File

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

View File

@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import type { OutputTongyishuFileInfo, OutputTijianPdfFileInfo, OutputPhysicalExamItemInfo } from '../../api'; import type { OutputTongyishuFileInfo, OutputTijianPdfFileInfo, OutputPhysicalExamItemInfo } from '../../api';
import { import {
@@ -20,7 +19,6 @@ import {
import type { SignaturePadHandle } from '../ui'; import type { SignaturePadHandle } from '../ui';
import { Button, SignaturePad } from '../ui'; import { Button, SignaturePad } from '../ui';
// Polyfill for Promise.withResolvers
if (typeof (Promise as any).withResolvers === 'undefined') { if (typeof (Promise as any).withResolvers === 'undefined') {
(Promise as any).withResolvers = function <T>() { (Promise as any).withResolvers = function <T>() {
let resolve!: (value: T | PromiseLike<T>) => void; let resolve!: (value: T | PromiseLike<T>) => void;
@@ -33,8 +31,13 @@ if (typeof (Promise as any).withResolvers === 'undefined') {
}; };
} }
// 配置 PDF.js worker if (typeof (Promise as any).try === 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; (Promise as any).try = function (fn: () => any) {
return new Promise((resolve) => resolve(fn()));
};
}
pdfjsLib.GlobalWorkerOptions.workerSrc = import.meta.env.MODE === 'development' ? '/pdf.worker.min.js' : '/tijian-zongkong/pdf.worker.min.js';
interface ExamSignPanelProps { interface ExamSignPanelProps {
examId?: number; examId?: number;
@@ -418,7 +421,6 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
}, [examId, optionalConfirmed]); }, [examId, optionalConfirmed]);
const handlePickFile = () => { const handlePickFile = () => {
// 有可选项目但尚未确认时,禁止拍照并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) { if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目'); setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true); setShowOptionalConfirmTip(true);
@@ -465,24 +467,25 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) {
return;
}
setMessage(null); setMessage(null);
try { try {
const jpgFile = await convertToJpg(file); const jpgFile = await convertToJpg(file);
setIdCardFile(jpgFile); setIdCardFile(jpgFile);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
setPreviewImage(event.target?.result as string); setPreviewImage(event.target?.result as string);
}; };
reader.readAsDataURL(jpgFile); reader.readAsDataURL(jpgFile);
} catch (err) { } catch (err) {
alert('err: ' + String(err));
console.error('图片转换失败', err); console.error('图片转换失败', err);
setMessage('图片处理失败,请重试'); setMessage('图片处理失败,请重试');
} }
e.target.value = ''; e.target.value = '';
}; };
@@ -2053,7 +2056,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
<span className='truncate'>{item.pdf_name.length > 10 ? item.pdf_name.slice(0, 10) + "..." : item.pdf_name}</span> <span className='truncate'>{item.pdf_name.length > 10 ? item.pdf_name.slice(0, 10) + "..." : item.pdf_name}</span>
{item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( {item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && (
<img <img
src='/sign.png' src={import.meta.env.MODE === 'development' ? '/sign.png' : '/tijian-zongkong/sign.png'}
alt='已签名' alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none' className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy' loading='lazy'
@@ -2105,7 +2108,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
<span className='truncate'></span> <span className='truncate'></span>
{isDaojiandanSigned && ( {isDaojiandanSigned && (
<img <img
src='/sign.png' src={import.meta.env.MODE === 'development' ? '/sign.png' : '/tijian-zongkong/sign.png'}
alt='已签名' alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none' className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy' loading='lazy'
@@ -2228,7 +2231,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
)} )}
{isSigned && ( {isSigned && (
<img <img
src='/sign.png' src={import.meta.env.MODE === 'development' ? '/sign.png' : '/tijian-zongkong/sign.png'}
alt='已签名' alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none' className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy' loading='lazy'

View File

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

View File

@@ -22,10 +22,18 @@ const NAV_ITEMS = [
{ key: 'support', icon: IconSupport, label: '客服咨询' }, { key: 'support', icon: IconSupport, label: '客服咨询' },
] as const; ] as const;
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
};
export const Sidebar = ({ active, onNavigate, onQuickAction }: SidebarProps) => ( export const Sidebar = ({ active, onNavigate, onQuickAction }: SidebarProps) => (
<aside className='bg-white border-r p-4 flex flex-col gap-4'> <aside className='h-screen max-h-screen bg-white border-r p-4 flex flex-col gap-4 overflow-hidden'>
<div> <div>
<div className='text-base font-semibold'> · </div> <button type='button' onClick={toggleFullscreen} className='text-base font-semibold cursor-pointer hover:opacity-80 text-left'> · </button>
<div className='text-xs text-gray-500 mt-1'></div> <div className='text-xs text-gray-500 mt-1'></div>
</div> </div>
@@ -73,13 +81,13 @@ export const Sidebar = ({ active, onNavigate, onQuickAction }: SidebarProps) =>
</div> </div>
</section> </section>
<section className='mt-auto p-3 rounded-2xl border bg-gray-50/70 text-xs flex flex-col gap-2'> {/* <section className='mt-auto p-3 rounded-2xl border bg-gray-50/70 text-xs flex flex-col gap-2'>
<div className='text-sm font-medium flex items-center gap-2'> <div className='text-sm font-medium flex items-center gap-2'>
<span>💻</span> <span>💻</span>
<span>客服 / IT 支持</span> <span>客服 / IT 支持</span>
</div> </div>
<div className='text-gray-600'>遇到系统问题可一键联系 IT / 运营支持。</div> <div className='text-gray-600'>遇到系统问题可一键联系 IT / 运营支持。</div>
</section> </section> */}
</aside> </aside>
); );

View File

@@ -10,7 +10,7 @@ interface TopBarProps {
onLogout?: () => void; onLogout?: () => void;
} }
export const TopBar = ({ enableSearch = true, operatorName, onLoginClick, onLogout }: TopBarProps) => { export const TopBar = ({ operatorName, onLoginClick, onLogout }: TopBarProps) => {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [displayName, setDisplayName] = useState(operatorName || ''); const [displayName, setDisplayName] = useState(operatorName || '');
@@ -28,18 +28,18 @@ export const TopBar = ({ enableSearch = true, operatorName, onLoginClick, onLogo
return ( return (
<header className='flex items-center gap-3 p-2 border-b bg-white'> <header className='flex items-center gap-3 p-2 border-b bg-white'>
<div className='flex-1 flex items-center gap-3'> <div className='flex-1 flex items-center gap-3'>
{enableSearch ? ( {/* {enableSearch ? (
<div className='w-[420px] max-w-[60vw] h-[36px] flex items-center'> <div className='w-[420px] max-w-[60vw] h-[36px] flex items-center'>
{/* <Input <Input
placeholder='搜索 姓名 / 证件号 / 手机号' placeholder='搜索 姓名 / 证件号 / 手机号'
value={search} value={search}
onChange={(e) => onSearch(e.target.value)} onChange={(e) => onSearch(e.target.value)}
className='text-sm' className='text-sm'
/> */} />
</div> </div>
) : ( ) : (
<div className='text-sm text-gray-500 flex items-center p-[9px] pl-[14px]'>圆和医疗 · 体检驾驶舱</div> <div className='text-sm text-gray-500 flex items-center p-[9px] pl-[14px]'>圆和医疗 · 体检驾驶舱</div>
)} )} */}
</div> </div>
<div className='flex items-center gap-3 text-xs relative'> <div className='flex items-center gap-3 text-xs relative'>
<button <button

View File

@@ -95,9 +95,9 @@ export const NoteModal = ({ noteText, onNoteChange, onClose }: NoteModalProps) =
{saving ? '保存中...' : '保存'} {saving ? '保存中...' : '保存'}
</button> </button>
</div> </div>
<div className='text-right text-[11px] text-gray-500'> {/* <div className='text-right text-[11px] text-gray-500'>
备注内容会同步至客户详情页,供前台和导检护士查看。 备注内容会同步至客户详情页,供前台和导检护士查看。
</div> </div> */}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,11 @@
import { Card, CardContent, CardHeader } from '../ui'; import { Card, CardContent } from '../ui';
export const SupportSection = () => ( export const SupportSection = () => (
<Card> <Card>
<CardHeader> · </CardHeader> {/* <CardHeader>客服咨询 · 圆圆客服台卡</CardHeader> */}
<CardContent> <CardContent>
<div className='grid grid-cols-[1.2fr_1fr] gap-6 items-center'> <div className='items-center'>
<div className='space-y-3 text-sm text-gray-700'> {/* <div className='space-y-3 text-sm text-gray-700'>
<p>通过「圆圆客服」二维码,客户可获得一站式健康服务:包含体检预约、报告查询、报告解读等。</p> <p>通过「圆圆客服」二维码,客户可获得一站式健康服务:包含体检预约、报告查询、报告解读等。</p>
<ul className='list-disc ml-5 space-y-1 text-xs text-gray-600'> <ul className='list-disc ml-5 space-y-1 text-xs text-gray-600'>
<li>支持体检当天现场扫码添加,绑定客户信息</li> <li>支持体检当天现场扫码添加,绑定客户信息</li>
@@ -13,19 +13,19 @@ export const SupportSection = () => (
<li>提供一对一健康咨询与报告解读服务</li> <li>提供一对一健康咨询与报告解读服务</li>
</ul> </ul>
<div className='text-xs text-gray-500'>注:实际系统中可上传设计好的「圆圆客服二维码台卡」图片,用于前台展示与打印。</div> <div className='text-xs text-gray-500'>注:实际系统中可上传设计好的「圆圆客服二维码台卡」图片,用于前台展示与打印。</div>
</div> </div> */}
<div className='h-64 rounded-3xl overflow-hidden shadow-inner flex items-center justify-center bg-gradient-to-b from-[#152749] to-[#c73545]'> <div className='h-[70vh] rounded-3xl overflow-hidden shadow-inner flex items-center justify-center bg-gradient-to-b from-[#152749] to-[#c73545]'>
<div className='flex flex-col items-center gap-3 text-white'> <div className='flex flex-col items-center gap-5 text-white'>
<div className='text-[11px] tracking-[0.2em] opacity-80'>CIRCLE HARMONY · </div> <div className='text-base tracking-[0.2em] opacity-80'>CIRCLE HARMONY · </div>
<div className='text-sm font-medium'> · </div> <div className='text-lg font-medium'> · </div>
<div className='w-28 h-28 bg-none flex items-center justify-center'> <div className='w-72 h-72 bg-none flex items-center justify-center'>
<div className='w-30 h-30 rounded-md overflow-hidden'> <div className='w-72 h-72 rounded-md overflow-hidden'>
<img src="https://datacenter-open.oss-cn-hangzhou.aliyuncs.com/his/kefu-zixun.png" alt='圆圆客服二维码' className='w-full h-full object-cover' /> <img src="https://datacenter-open.oss-cn-hangzhou.aliyuncs.com/his/kefu-zixun.png" alt='圆圆客服二维码' className='w-full h-full object-cover' />
</div> </div>
</div> </div>
<div className='text-sm font-semibold'></div> <div className='text-lg font-semibold'></div>
<div className='px-4 py-1.5 rounded-full border border-white/70 text-[11px] flex gap-2'> <div className='px-6 py-2 rounded-full border border-white/70 text-sm flex gap-2'>
<span></span> <span></span>
<span>/</span> <span>/</span>
<span></span> <span></span>

View File

@@ -7,17 +7,21 @@
scrollbar-width: none; scrollbar-width: none;
scrollbar-color: #cbd5e1 #f8fafc; scrollbar-color: #cbd5e1 #f8fafc;
} }
.custom-scroll::-webkit-scrollbar { .custom-scroll::-webkit-scrollbar {
width: 8px; width: 8px;
} }
.custom-scroll::-webkit-scrollbar-track { .custom-scroll::-webkit-scrollbar-track {
background: #f8fafc; background: #f8fafc;
border-radius: 9999px; border-radius: 9999px;
} }
.custom-scroll::-webkit-scrollbar-thumb { .custom-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1; background: #cbd5e1;
border-radius: 9999px; border-radius: 9999px;
} }
.custom-scroll::-webkit-scrollbar-thumb:hover { .custom-scroll::-webkit-scrollbar-thumb:hover {
background: #94a3b8; background: #94a3b8;
} }
@@ -30,9 +34,13 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
body { html,
body,
#root {
margin: 0; margin: 0;
min-height: 100vh; height: 100%;
max-height: 100%;
overflow: hidden;
background-color: #f8fafc; background-color: #f8fafc;
} }

View File

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

6
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*?url' {
const src: string;
export default src;
}

View File

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