init
This commit is contained in:
169
src/components/modals/LoginModal.tsx
Normal file
169
src/components/modals/LoginModal.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button, Input } from '../ui';
|
||||
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
onLoginSuccess?: (phone: string) => void;
|
||||
}
|
||||
|
||||
export const LoginModal = ({ onClose, onLoginSuccess }: LoginModalProps) => {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 验证码倒计时
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
// 验证手机号格式
|
||||
const validatePhone = (phoneNumber: string): boolean => {
|
||||
return /^1[3-9]\d{9}$/.test(phoneNumber);
|
||||
};
|
||||
|
||||
// 发送验证码
|
||||
const handleSendCode = async () => {
|
||||
if (!phone) {
|
||||
setError('请输入手机号');
|
||||
return;
|
||||
}
|
||||
if (!validatePhone(phone)) {
|
||||
setError('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
// 模拟发送验证码 API 调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
setLoading(false);
|
||||
setCountdown(60); // 60秒倒计时
|
||||
// 实际项目中,这里应该调用后端 API 发送验证码
|
||||
// 开发环境可以显示验证码(例如:123456)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('验证码已发送(开发环境:123456)');
|
||||
};
|
||||
|
||||
// 登录
|
||||
const handleLogin = async () => {
|
||||
if (!phone) {
|
||||
setError('请输入手机号');
|
||||
return;
|
||||
}
|
||||
if (!validatePhone(phone)) {
|
||||
setError('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
if (!code) {
|
||||
setError('请输入验证码');
|
||||
return;
|
||||
}
|
||||
if (code.length !== 6) {
|
||||
setError('验证码应为6位数字');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
// 模拟登录 API 调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// 开发环境:验证码为 123456 时通过
|
||||
if (code === '123456') {
|
||||
setLoading(false);
|
||||
onLoginSuccess?.(phone);
|
||||
onClose();
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('验证码错误,请重新输入');
|
||||
}
|
||||
};
|
||||
|
||||
const canSendCode = countdown === 0 && !loading && phone.length === 11;
|
||||
const canLogin = phone && code && !loading;
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/30' onClick={onClose}>
|
||||
<div
|
||||
className='w-[480px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='px-4 py-3 border-b flex items-center justify-between'>
|
||||
<div className='font-semibold'>操作员登录</div>
|
||||
<button className='text-xs text-gray-500 hover:text-gray-700' onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='px-4 py-6 bg-gray-50/60 space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<label className='text-xs text-gray-700 font-medium'>手机号</label>
|
||||
<Input
|
||||
type='tel'
|
||||
placeholder='请输入手机号'
|
||||
value={phone}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '').slice(0, 11);
|
||||
setPhone(value);
|
||||
setError('');
|
||||
}}
|
||||
maxLength={11}
|
||||
className='text-base'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<label className='text-xs text-gray-700 font-medium'>验证码</label>
|
||||
<div className='flex gap-2'>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='请输入6位验证码'
|
||||
value={code}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '').slice(0, 6);
|
||||
setCode(value);
|
||||
setError('');
|
||||
}}
|
||||
maxLength={6}
|
||||
className='text-base flex-1'
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSendCode}
|
||||
disabled={!canSendCode}
|
||||
className={!canSendCode ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒` : loading ? '发送中...' : '发送验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-500'>
|
||||
开发环境:验证码为 <span className='font-mono font-semibold'>123456</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className='px-3 py-2 rounded-xl bg-red-50 border border-red-200 text-xs text-red-600'>{error}</div>
|
||||
)}
|
||||
|
||||
<div className='pt-2'>
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
disabled={!canLogin}
|
||||
className={`w-full justify-center ${!canLogin ? 'opacity-50 cursor-not-allowed' : 'bg-gray-900 hover:bg-gray-800'}`}
|
||||
>
|
||||
{loading ? '· · ·' : '登录'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
144
src/components/modals/QuickActionModal.tsx
Normal file
144
src/components/modals/QuickActionModal.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { ExamClient, QuickActionType } from '../../data/mockData';
|
||||
import { EXAM_CLIENTS } from '../../data/mockData';
|
||||
import { Button, InfoCard, Input } from '../ui';
|
||||
|
||||
interface QuickActionModalProps {
|
||||
action: QuickActionType;
|
||||
noteText: string;
|
||||
onNoteChange: (v: string) => void;
|
||||
onClose: () => void;
|
||||
totalExamCount: number;
|
||||
mealCount: number;
|
||||
notMealCount: number;
|
||||
mealDoneIds: string[];
|
||||
onMealDone: (id: string) => void;
|
||||
}
|
||||
|
||||
export const QuickActionModal = ({
|
||||
action,
|
||||
noteText,
|
||||
onNoteChange,
|
||||
onClose,
|
||||
totalExamCount,
|
||||
mealCount,
|
||||
notMealCount,
|
||||
mealDoneIds,
|
||||
onMealDone,
|
||||
}: QuickActionModalProps) => {
|
||||
const titleMap: Record<Exclude<QuickActionType, 'none'>, string> = {
|
||||
meal: '用餐登记',
|
||||
vip: '太平 VIP 认证说明',
|
||||
delivery: '报告寄送登记',
|
||||
note: '备注窗',
|
||||
};
|
||||
|
||||
if (action === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'>
|
||||
<div className='w-[560px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'>
|
||||
<div className='px-4 py-3 border-b flex items-center justify-between'>
|
||||
<div className='font-semibold'>{titleMap[action]}</div>
|
||||
<button className='text-xs text-gray-500' onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<div className='px-4 py-4 bg-gray-50/60'>
|
||||
{action === 'meal' && (
|
||||
<div className='space-y-3'>
|
||||
<div className='grid grid-cols-3 gap-3 text-xs'>
|
||||
<InfoCard label='今日体检人数' value={totalExamCount} />
|
||||
<InfoCard label='已用餐人数' value={mealCount} />
|
||||
<InfoCard label='未用餐人数' value={notMealCount} />
|
||||
</div>
|
||||
<div className='text-xs text-gray-600 mt-2 mb-1'>选择已用餐客户进行登记:</div>
|
||||
<div className='max-h-60 overflow-auto border rounded-2xl bg-white p-2 text-xs'>
|
||||
{EXAM_CLIENTS.map((c: ExamClient) => {
|
||||
const checked = mealDoneIds.includes(c.id);
|
||||
return (
|
||||
<label
|
||||
key={c.id}
|
||||
className='flex items-center justify-between px-3 py-1.5 rounded-2xl hover:bg-gray-50 cursor-pointer'
|
||||
>
|
||||
<span>
|
||||
{c.name} <span className='text-gray-400 text-[11px]'>({c.id})</span>
|
||||
</span>
|
||||
<span className='flex items-center gap-2'>
|
||||
<span className='text-gray-400'>{c.status}</span>
|
||||
<input type='checkbox' checked={checked} onChange={() => onMealDone(c.id)} />
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action === 'vip' && (
|
||||
<div className='flex gap-4 items-center'>
|
||||
<div className='flex-1 text-xs text-gray-700 space-y-2'>
|
||||
<p>通过「太平 VIP 认证」二维码,可完成太平渠道客户的身份绑定与权益确认。</p>
|
||||
<ul className='list-disc ml-5 space-y-1'>
|
||||
<li>客户出示太平 APP 内会员二维码,由工作人员扫码完成认证。</li>
|
||||
<li>认证成功后,系统会自动标记为「太平 VIP 客户」,并记录在体检档案中。</li>
|
||||
<li>支持后续报告寄送、复查预约等专属服务。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='w-40 h-40 rounded-3xl bg-white border flex items-center justify-center text-xs text-gray-500'>
|
||||
太平认证二维码占位
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action === 'delivery' && (
|
||||
<div className='space-y-3 text-xs text-gray-700'>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div>
|
||||
收件人姓名
|
||||
<Input placeholder='请输入收件人姓名' className='mt-1' />
|
||||
</div>
|
||||
<div>
|
||||
联系电话
|
||||
<Input placeholder='用于快递联系' className='mt-1' />
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
寄送地址
|
||||
<Input placeholder='请输入详细寄送地址' className='mt-1' />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
备注说明
|
||||
<textarea
|
||||
className='w-full mt-1 rounded-2xl border px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-gray-200 min-h-[72px]'
|
||||
placeholder='如需多份报告、加急寄送等,请在此备注'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-right'>
|
||||
<Button>保存寄送信息</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action === 'note' && (
|
||||
<div className='space-y-3 text-xs text-gray-700'>
|
||||
<div>体检客户服务备注(仅内部可见)</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-[96px]'
|
||||
placeholder='例如:客户有既往疾病史、沟通偏好、特殊关怀需求等,可在此记录。'
|
||||
value={noteText}
|
||||
onChange={(e) => onNoteChange(e.target.value)}
|
||||
/>
|
||||
<div className='text-right text-[11px] text-gray-500'>
|
||||
备注内容会同步至客户详情页,供前台和导检护士查看。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user