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
`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.
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",
"type": "module",
"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",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.2",
"pdfjs-dist": "^5.4.449",
"pdfjs-dist": "^4.4.168",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"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';
function App() {
return <RouterProvider router={router} />;
return (
<div className='h-screen max-h-screen overflow-hidden'>
<RouterProvider router={router} />
</div>
);
}
export default App;

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',
// 默认使用外网地址,可根据环境变量切换
// 开发环境使用内网,生产环境使用外网
BASE_URL: import.meta.env.NODE_ENV === 'development'
? 'http://10.1.5.118:8077/platform-api'
: 'http://apihis.circleharmonyhospital.cn:8982/platform-api',
// 请求超时时间120秒
EXTERNAL_URL: 'http://apihis.circleharmonyhospital.cn:8982/platform-api',
BASE_URL: import.meta.env.MODE === 'development'
? '/platform-api'
: '/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.MODE === 'development' ? '/' : '/tijian-zongkong/';
window.location.href = `${baseUrl}#/home?login=true`;
// navigate('/home?login=true');
// return;
}
break;
case 403:

View File

@@ -548,6 +548,8 @@ export interface InputPhysicalExamAddItem {
scrm_account_name?: string | null;
/** 项目名称(默认空值,传入项目名称过滤数据) */
item_name?: string;
/** 折扣率 */
discount_rate?: number | null;
}
/**
@@ -720,11 +722,11 @@ export type PhysicalExamQrcodeCreateResponse = CommonActionResult<string>;
*/
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;
/** 挂账公司ID挂账公司传对应的ID其他传0 */
company_id: number;
/** 订单总金额 */
orderAmount: number;
}
/**
@@ -1396,6 +1400,15 @@ export interface InputCustomSettlementApplyApprove {
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;
/** 最终结算金额 */
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;
/** 结算金额 */
settlement_price: number;
/** 折扣率 */
discount_ratio?: number | null;
}
/**
@@ -1447,6 +1468,8 @@ export interface InputCustomSettlementApply {
final_settlement_price?: number;
/** 申请理由 */
apply_reason?: string;
/** 申请人 */
apply_user: string;
}
/**
@@ -1470,6 +1493,8 @@ export interface InputCustomSettlementApplyCancel {
physical_exam_id: number;
/** 体检加项组合ID多个逗号分隔例如123,456 */
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>
{viewMode === 'form' ? (
<>
<div className='overflow-y-auto overflow-x-hidden max-h-[clamp(260px,calc(100vh-520px),560px)] pr-1'>
{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) {
@@ -261,7 +261,7 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
{saveLoading ? '保存中...' : '保存寄送信息'}
</Button>
</div>
</>
</div>
) : (
<div className='flex flex-col items-center justify-center py-8'>
{qrcodeLoading ? (
@@ -271,7 +271,7 @@ export const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => {
<img
src={qrcodeUrl.startsWith('data:') ? qrcodeUrl : `data:image/png;base64,${qrcodeUrl}`}
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>

View File

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

View File

@@ -76,6 +76,16 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
.finally(() => setDetailLoading(false));
}, [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 (
<div
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}
progressList={progressList}
loading={detailLoading}
onCustomerUpdated={refetchDetail}
/>
)}
{tab === 'sign' && <ExamSignPanel examId={Number(client.id)} onBusyChange={setSignBusy} />}

View File

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

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

@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import type { OutputTongyishuFileInfo, OutputTijianPdfFileInfo, OutputPhysicalExamItemInfo } from '../../api';
import {
@@ -20,7 +19,6 @@ import {
import type { SignaturePadHandle } from '../ui';
import { Button, SignaturePad } from '../ui';
// Polyfill for Promise.withResolvers
if (typeof (Promise as any).withResolvers === 'undefined') {
(Promise as any).withResolvers = function <T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
@@ -33,8 +31,13 @@ if (typeof (Promise as any).withResolvers === 'undefined') {
};
}
// 配置 PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
if (typeof (Promise as any).try === 'undefined') {
(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 {
examId?: number;
@@ -418,7 +421,6 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
}, [examId, optionalConfirmed]);
const handlePickFile = () => {
// 有可选项目但尚未确认时,禁止拍照并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
@@ -465,24 +467,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 = '';
};
@@ -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>
{item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && (
<img
src='/sign.png'
src={import.meta.env.MODE === 'development' ? '/sign.png' : '/tijian-zongkong/sign.png'}
alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy'
@@ -2105,7 +2108,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
<span className='truncate'></span>
{isDaojiandanSigned && (
<img
src='/sign.png'
src={import.meta.env.MODE === 'development' ? '/sign.png' : '/tijian-zongkong/sign.png'}
alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy'
@@ -2228,7 +2231,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
)}
{isSigned && (
<img
src='/sign.png'
src={import.meta.env.MODE === 'development' ? '/sign.png' : '/tijian-zongkong/sign.png'}
alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy'

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-10'>
<Card>
<CardHeader>B1 </CardHeader>
<CardContent>

View File

@@ -22,10 +22,18 @@ const NAV_ITEMS = [
{ key: 'support', icon: IconSupport, label: '客服咨询' },
] as const;
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
};
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 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>
@@ -73,13 +81,13 @@ export const Sidebar = ({ active, onNavigate, onQuickAction }: SidebarProps) =>
</div>
</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'>
<span>💻</span>
<span>客服 / IT 支持</span>
</div>
<div className='text-gray-600'>遇到系统问题可一键联系 IT / 运营支持。</div>
</section>
</section> */}
</aside>
);

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

View File

@@ -95,9 +95,9 @@ export const NoteModal = ({ noteText, onNoteChange, onClose }: NoteModalProps) =
{saving ? '保存中...' : '保存'}
</button>
</div>
<div className='text-right text-[11px] text-gray-500'>
{/* <div className='text-right text-[11px] text-gray-500'>
备注内容会同步至客户详情页,供前台和导检护士查看。
</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 = () => (
<Card>
<CardHeader> · </CardHeader>
{/* <CardHeader>客服咨询 · 圆圆客服台卡</CardHeader> */}
<CardContent>
<div className='grid grid-cols-[1.2fr_1fr] gap-6 items-center'>
<div className='space-y-3 text-sm text-gray-700'>
<div className='items-center'>
{/* <div className='space-y-3 text-sm text-gray-700'>
<p>通过「圆圆客服」二维码,客户可获得一站式健康服务:包含体检预约、报告查询、报告解读等。</p>
<ul className='list-disc ml-5 space-y-1 text-xs text-gray-600'>
<li>支持体检当天现场扫码添加,绑定客户信息</li>
@@ -13,19 +13,19 @@ export const SupportSection = () => (
<li>提供一对一健康咨询与报告解读服务</li>
</ul>
<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='flex flex-col items-center gap-3 text-white'>
<div className='text-[11px] tracking-[0.2em] opacity-80'>CIRCLE HARMONY · </div>
<div className='text-sm font-medium'> · </div>
<div className='w-28 h-28 bg-none flex items-center justify-center'>
<div className='w-30 h-30 rounded-md overflow-hidden'>
<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-5 text-white'>
<div className='text-base tracking-[0.2em] opacity-80'>CIRCLE HARMONY · </div>
<div className='text-lg font-medium'> · </div>
<div className='w-72 h-72 bg-none flex items-center justify-center'>
<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' />
</div>
</div>
<div className='text-sm font-semibold'></div>
<div className='px-4 py-1.5 rounded-full border border-white/70 text-[11px] flex gap-2'>
<div className='text-lg font-semibold'></div>
<div className='px-6 py-2 rounded-full border border-white/70 text-sm flex gap-2'>
<span></span>
<span>/</span>
<span></span>

View File

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

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,11 +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) {
// 跳转登录
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 />,

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/
export default defineConfig({
base: "./",
plugins: [react()],
server: {
allowedHosts: ['ipad.shenynet.com'],
},
resolve: {
dedupe: ['react', 'react-dom']
},
})