231 lines
9.7 KiB
TypeScript
231 lines
9.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { User, Template, SystemSettings, FormField, DEFAULT_FORM_FIELDS } from '../types';
|
|
import { defaultReportContent } from '../utils/defaultContent';
|
|
import { storage } from '../utils/storage';
|
|
import { User as UserIcon, Lock } from 'lucide-react';
|
|
|
|
export default function Login() {
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
const initData = () => {
|
|
const existingUsers = storage.get<User[]>('users', []);
|
|
const hasAdmin = existingUsers.some((u) => u.username === 'admin' && u.password === '123456');
|
|
|
|
let savedTemplates = storage.get<Template[]>('templates', []);
|
|
if (savedTemplates.length === 0) {
|
|
const initialTemplate: Template = {
|
|
id: 'surgery',
|
|
name: '腹腔镜胆囊切除术报告',
|
|
desc: '标准手术记录模板',
|
|
content: defaultReportContent,
|
|
createdAt: new Date().toISOString(),
|
|
author: 'admin'
|
|
};
|
|
savedTemplates = [initialTemplate];
|
|
storage.set('templates', savedTemplates);
|
|
}
|
|
|
|
if (!hasAdmin) {
|
|
const allTplIds = savedTemplates.map(t => t.id);
|
|
const defaultUsers: User[] = [
|
|
{ username: 'admin', password: '123456', role: 'super', name: '超级管理员', status: 'active', createdAt: '2024-01-01', visibleTemplates: allTplIds, manageableTemplates: allTplIds },
|
|
{ username: 'manager', password: '123456', role: 'admin', name: '管理员', status: 'active', createdAt: '2024-01-01', department: '外科', visibleTemplates: allTplIds, manageableTemplates: allTplIds },
|
|
{ username: '0001', password: '123456', role: 'user', name: '张医生', status: 'active', createdAt: '2024-01-01', department: '外科', visibleTemplates: allTplIds, manageableTemplates: [] }
|
|
];
|
|
storage.set('users', defaultUsers);
|
|
console.log('Default users initialized');
|
|
}
|
|
|
|
const fieldsConfig = storage.get<FormField[]>('formFieldsConfig', []);
|
|
if (fieldsConfig.length === 0) {
|
|
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
|
|
}
|
|
|
|
const savedAssets = storage.get<{id: string; name: string; dataUrl: string}[]>('imageAssets', []);
|
|
if (savedAssets.length === 0) {
|
|
fetch('/logo_square.png')
|
|
.then(res => res.blob())
|
|
.then(blob => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
const dataUrl = reader.result as string;
|
|
storage.set('imageAssets', [{ id: 'asset_logo', name: '医院Logo', dataUrl }]);
|
|
};
|
|
reader.readAsDataURL(blob);
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
const settingsRaw = storage.get<SystemSettings>('systemSettings', {} as SystemSettings);
|
|
if (!settingsRaw.frameCount) {
|
|
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
const positions: number[] = [];
|
|
for (let i = 1; i <= 12; i++) {
|
|
positions.push(round1((100 / 13) * i));
|
|
}
|
|
const defaultSettings = {
|
|
frameCount: 12,
|
|
framePositions: positions,
|
|
apiEndpoint: '',
|
|
apiKey: '',
|
|
defaultTemplate: savedTemplates[0]?.id || '',
|
|
frameMode: 'uniform',
|
|
autoInsertFrames: true,
|
|
autoInsertDelay: 1,
|
|
autoInsertFrameIndices: [0, 1, 2, 3, 4, 5]
|
|
};
|
|
storage.set('systemSettings', defaultSettings);
|
|
}
|
|
};
|
|
|
|
initData();
|
|
}, []);
|
|
|
|
const handleLogin = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const u = username.trim();
|
|
const p = password.trim();
|
|
|
|
const users = storage.get<User[]>('users', []);
|
|
let user = users.find(user => user.username === u && user.password === p);
|
|
|
|
// Fallback for default accounts if localStorage is messed up
|
|
if (!user) {
|
|
const defaults = [
|
|
{ u: 'admin', p: '123456', r: 'super', n: '超级管理员' },
|
|
{ u: 'manager', p: '123456', r: 'admin', n: '管理员' },
|
|
{ u: '0001', p: '123456', r: 'user', n: '张医生' }
|
|
];
|
|
const d = defaults.find(item => item.u === u && item.p === p);
|
|
if (d) {
|
|
const allTemplates = storage.get<Template[]>('templates', []);
|
|
const allTplIds = allTemplates.map(t => t.id);
|
|
user = { username: d.u, password: d.p, role: d.r as any, name: d.n, status: 'active', createdAt: '2024-01-01', visibleTemplates: allTplIds, manageableTemplates: d.r === 'user' ? [] : allTplIds, department: d.r === 'super' ? '' : '外科' };
|
|
// Sync back to localStorage
|
|
const updatedUsers = [...users.filter(item => item.username !== u), user];
|
|
storage.set('users', updatedUsers);
|
|
}
|
|
}
|
|
|
|
if (user) {
|
|
if (user.status === 'inactive') {
|
|
setError('该账号已被禁用');
|
|
return;
|
|
}
|
|
storage.set('currentUser', user);
|
|
navigate('/dashboard');
|
|
} else {
|
|
setError('用户ID或密码错误');
|
|
console.log('Login failed for:', u);
|
|
}
|
|
};
|
|
|
|
const fillLogin = (u: string, p: string) => {
|
|
setUsername(u);
|
|
setPassword(p);
|
|
setTimeout(() => {
|
|
// Trigger the robust login logic manually
|
|
const users = storage.get<User[]>('users', []);
|
|
let user = users.find(user => user.username === u && user.password === p);
|
|
|
|
if (!user) {
|
|
const defaults = [
|
|
{ u: 'admin', p: '123456', r: 'super', n: '超级管理员' },
|
|
{ u: 'manager', p: '123456', r: 'admin', n: '管理员' },
|
|
{ u: '0001', p: '123456', r: 'user', n: '张医生' }
|
|
];
|
|
const d = defaults.find(item => item.u === u && item.p === p);
|
|
if (d) {
|
|
user = { username: d.u, password: d.p, role: d.r as any, name: d.n, status: 'active', createdAt: '2024-01-01' };
|
|
const updatedUsers = [...users.filter(item => item.username !== u), user];
|
|
storage.set('users', updatedUsers);
|
|
}
|
|
}
|
|
|
|
if (user) {
|
|
storage.set('currentUser', user);
|
|
navigate('/dashboard');
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-bg p-6">
|
|
<div className="bg-white rounded-3xl shadow-[0_20px_50px_-12px_rgba(0,0,0,0.08)] p-12 w-full max-w-[460px] border border-border">
|
|
<div className="text-center mb-10">
|
|
<div className="flex flex-col items-center">
|
|
<img src="/logo_square.png" alt="Logo" className="w-16 h-16 object-contain mb-6" />
|
|
<h1 className="text-2xl font-bold text-text-main tracking-tight mb-1">手术图文病历报告生成终端</h1>
|
|
<p className="text-xs text-text-muted uppercase tracking-widest font-bold">智能图文报告管理系统</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleLogin} className="space-y-6">
|
|
<div className="space-y-1.5">
|
|
<label className="block text-[10px] font-bold text-text-main uppercase tracking-wider">用户ID</label>
|
|
<div className="relative">
|
|
<UserIcon className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-muted" size={18} />
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="请输入您的用户ID"
|
|
required
|
|
className="input-minimal pl-11"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="block text-[10px] font-bold text-text-main uppercase tracking-wider">密码</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-muted" size={18} />
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="请输入您的登录密码"
|
|
required
|
|
className="input-minimal pl-11"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="btn-accent w-full py-4 text-base shadow-[0_8px_20px_-4px_rgba(37,99,235,0.2)]"
|
|
>
|
|
进入系统
|
|
</button>
|
|
{error && <div className="text-red-500 text-xs text-center font-bold animate-pulse">{error}</div>}
|
|
</form>
|
|
|
|
<div className="mt-10 pt-8 border-t border-border">
|
|
<h3 className="text-[10px] text-text-muted mb-4 uppercase tracking-widest font-bold text-center">快捷登录测试账号</h3>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{[
|
|
{ u: 'admin', p: '123456', r: '超级管理员', c: 'bg-amber-100 text-amber-700' },
|
|
{ u: 'manager', p: '123456', r: '管理员', c: 'bg-blue-100 text-blue-700' },
|
|
{ u: '0001', p: '123456', r: '医生', c: 'bg-green-100 text-green-700' }
|
|
].map(test => (
|
|
<div
|
|
key={test.u}
|
|
onClick={() => fillLogin(test.u, test.p)}
|
|
className="flex justify-between items-center p-3 bg-slate-50 rounded-xl cursor-pointer transition-all hover:bg-white hover:shadow-md border border-transparent hover:border-border group"
|
|
>
|
|
<span className="text-xs font-bold text-text-main">{test.u} / {test.p}</span>
|
|
<span className={`text-[9px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wider ${test.c}`}>
|
|
{test.r}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|