2026-05-20-15-20-15 项目库复用逆向视图与用户弹窗调整
This commit is contained in:
@@ -9,7 +9,6 @@ import {
|
||||
Box,
|
||||
Image as ImageIcon,
|
||||
Info,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
@@ -22,8 +21,14 @@ import {
|
||||
Upload
|
||||
} from 'lucide-react';
|
||||
import * as THREE from 'three';
|
||||
import { DicomInfo, DicomPreview, ModuleStyle, Project, SegmentationExportScope } from '../types';
|
||||
import { DicomFusionVolume, DicomInfo, DicomPreview, ModuleStyle, Project, SegmentationExportScope } from '../types';
|
||||
import { api, downloadDicomArchive, downloadProjectExportBundle, ProjectExportTarget } from '../lib/api';
|
||||
import {
|
||||
FusionThreeView,
|
||||
VoxelizationMappingView,
|
||||
dicomOpacityOptions as reverseDicomOpacityOptions,
|
||||
displayOptions as reverseDisplayOptions,
|
||||
} from './ReverseWorkspace';
|
||||
|
||||
type Plane = 'axial' | 'sagittal' | 'coronal';
|
||||
type DisplayMode = DicomPreview['mode'];
|
||||
@@ -660,7 +665,8 @@ export default function ProjectLibrary({
|
||||
const [resultRotation, setResultRotation] = useState(0);
|
||||
const [moduleStyles, setModuleStyles] = useState<Record<string, ModuleStyle>>({});
|
||||
const [dicomPreview, setDicomPreview] = useState<DicomPreview | null>(null);
|
||||
const [resultDicomPreview, setResultDicomPreview] = useState<DicomPreview | null>(null);
|
||||
const [resultFusionVolume, setResultFusionVolume] = useState<DicomFusionVolume | null>(null);
|
||||
const [resultFusionError, setResultFusionError] = useState('');
|
||||
const [dicomInfo, setDicomInfo] = useState<DicomInfo | null>(null);
|
||||
const [dicomInfoError, setDicomInfoError] = useState('');
|
||||
const [isDicomInfoOpen, setIsDicomInfoOpen] = useState(false);
|
||||
@@ -756,7 +762,10 @@ export default function ProjectLibrary({
|
||||
const latestResultStyles = latestSegmentationResult?.moduleStyles ?? moduleStyles;
|
||||
const resultMaxSlice = Math.max((selectedProject?.dicomCount ?? 1) - 1, 0);
|
||||
const resultMappingSlice = Math.max(0, Math.min(resultMaxSlice, resultPreviewSlice));
|
||||
const resultFineDetailLimit = solidityOptions.find((option) => option.id === 'fine')?.limit ?? 36000;
|
||||
const resultDisplayOption = reverseDisplayOptions.find((option) => option.id === 'fine') ?? reverseDisplayOptions[0];
|
||||
const resultDicomOpacity = reverseDicomOpacityOptions.find((option) => option.id === 'high') ?? reverseDicomOpacityOptions[reverseDicomOpacityOptions.length - 1];
|
||||
const resultCutStart = Math.max(0, Math.min(resultMaxSlice, latestSegmentationResult?.sliceStart ?? 0));
|
||||
const resultCutEnd = Math.max(0, Math.min(resultMaxSlice, latestSegmentationResult?.sliceEnd ?? resultMaxSlice));
|
||||
const resultVisibleModules = stlFiles
|
||||
.map((fileName, index) => ({
|
||||
fileName,
|
||||
@@ -872,30 +881,33 @@ export default function ProjectLibrary({
|
||||
}, [selectedProject?.id, selectedProject?.dicomCount, sliceIndex, plane, displayMode, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProject || viewMode !== 'mask' || !selectedProject.dicomCount) {
|
||||
setResultDicomPreview(null);
|
||||
if (!selectedProject || viewMode !== 'mask' || !latestSegmentationResult || !selectedProject.dicomCount) {
|
||||
setResultFusionVolume(null);
|
||||
setResultFusionError('');
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const maxSlice = Math.max(selectedProject.dicomCount - 1, 0);
|
||||
const previewSlice = Math.max(0, Math.min(maxSlice, resultPreviewSlice));
|
||||
api.getDicomPreview(selectedProject.id, previewSlice, 'axial', resultDisplayMode)
|
||||
.then((preview) => {
|
||||
const start = Math.min(resultCutStart, resultCutEnd);
|
||||
const end = Math.max(resultCutStart, resultCutEnd);
|
||||
setResultFusionError('');
|
||||
api.getDicomFusionVolume(selectedProject.id, start, end, 'soft')
|
||||
.then((volume) => {
|
||||
if (!cancelled) {
|
||||
setResultDicomPreview(preview);
|
||||
setResultFusionVolume(volume);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
setResultDicomPreview(null);
|
||||
setResultFusionVolume(null);
|
||||
setResultFusionError(error instanceof Error ? error.message : 'DICOM 三维融合体载入失败');
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedProject?.id, selectedProject?.dicomCount, viewMode, latestSegmentationResult?.id, resultPreviewSlice, resultDisplayMode]);
|
||||
}, [selectedProject?.id, selectedProject?.dicomCount, viewMode, latestSegmentationResult?.id, resultCutStart, resultCutEnd]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (sliceRepeatRef.current !== null) {
|
||||
@@ -1554,136 +1566,87 @@ export default function ProjectLibrary({
|
||||
)}
|
||||
|
||||
{viewMode === 'mask' && (
|
||||
<div className="h-full grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div className="grid min-h-0 grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div className="relative min-h-[520px] overflow-hidden rounded-2xl border border-slate-900 bg-slate-950 text-white shadow-sm">
|
||||
<div className="absolute left-4 top-4 z-10 rounded-xl border border-white/10 bg-black/45 px-3 py-2 backdrop-blur">
|
||||
<p className="text-sm font-bold">影像与模型融合视角</p>
|
||||
<p className="mt-1 font-mono text-[10px] text-white/45">
|
||||
{latestSegmentationResult ? `模型显示 精细 · 融合显示 DICOM 高 · Z ${resultMappingSlice + 1}/${selectedProject.dicomCount}` : '等待保存结果'}
|
||||
</p>
|
||||
<div className="h-full grid grid-cols-1 gap-6 2xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_340px]">
|
||||
<div className="min-h-[560px]">
|
||||
{latestSegmentationResult ? (
|
||||
<FusionThreeView
|
||||
project={selectedProject}
|
||||
volume={resultFusionVolume}
|
||||
modelPose={latestResultPose}
|
||||
moduleStyles={latestResultStyles}
|
||||
detailLimit={resultDisplayOption.limit}
|
||||
solidMode={false}
|
||||
dicomOpacity={resultDicomOpacity}
|
||||
showBounds={latestSegmentationResult.showBounds ?? true}
|
||||
cutEnabled={latestSegmentationResult.cutEnabled ?? false}
|
||||
cutStart={resultCutStart}
|
||||
cutEnd={resultCutEnd}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[560px] items-center justify-center rounded-3xl border border-dashed border-slate-200 bg-slate-950 px-8 text-center text-sm font-bold text-white/35">
|
||||
暂无保存结果,请在逆向工作区保存当前映射。
|
||||
</div>
|
||||
{latestSegmentationResult ? (
|
||||
<NativeStlViewer
|
||||
projectId={selectedProject.id}
|
||||
files={stlFiles}
|
||||
styles={latestResultStyles}
|
||||
detailLimit={resultFineDetailLimit}
|
||||
solidMode={false}
|
||||
pose={latestResultPose}
|
||||
onPoseChange={setResultPose}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center px-8 text-center text-sm font-bold text-white/35">
|
||||
暂无保存结果,请在逆向工作区保存当前映射。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{resultFusionError && (
|
||||
<p className="mt-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-bold text-amber-700">
|
||||
{resultFusionError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative flex min-h-[520px] flex-col overflow-hidden rounded-2xl border border-slate-900 bg-black text-white shadow-sm">
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<div className="absolute left-4 top-4 z-10 flex flex-wrap gap-2">
|
||||
<span className="rounded-lg border border-white/10 bg-black/50 px-2.5 py-1 font-mono text-[10px] font-bold text-white/70 backdrop-blur">
|
||||
BASE DICOM
|
||||
</span>
|
||||
<span className="rounded-lg border border-cyan-300/40 bg-cyan-400/15 px-2.5 py-1 font-mono text-[10px] font-bold text-cyan-100 backdrop-blur">
|
||||
OVERLAY LABEL MAP
|
||||
</span>
|
||||
<span className="rounded-lg border border-white/10 bg-black/50 px-2.5 py-1 font-mono text-[10px] font-bold text-white/70 backdrop-blur">
|
||||
Z {resultMappingSlice + 1}/{selectedProject.dicomCount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute right-4 top-4 z-10 flex flex-wrap justify-end gap-1.5">
|
||||
<div className="flex rounded-lg border border-white/10 bg-black/50 p-1 backdrop-blur">
|
||||
{displayModes.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
onClick={() => setResultDisplayMode(mode.id)}
|
||||
className={`rounded-md px-2 py-1 text-[9px] font-bold transition ${resultDisplayMode === mode.id ? 'bg-cyan-400 text-slate-950' : 'text-white/55 hover:text-white'}`}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setResultRotation((value) => (value + 270) % 360)}
|
||||
className="rounded-lg border border-white/10 bg-black/50 px-2 py-1 text-[9px] font-bold text-white/70 backdrop-blur hover:text-white"
|
||||
title="左转 90°"
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResultRotation((value) => (value + 90) % 360)}
|
||||
className="rounded-lg border border-white/10 bg-black/50 px-2 py-1 text-[9px] font-bold text-white/70 backdrop-blur hover:text-white"
|
||||
title="右转 90°"
|
||||
>
|
||||
<RotateCw size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center p-8">
|
||||
{latestSegmentationResult && resultDicomPreview ? (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<DicomCanvas preview={resultDicomPreview} rotation={resultRotation} />
|
||||
<div className="pointer-events-none absolute inset-x-10 bottom-10 rounded-2xl border border-white/10 bg-black/55 p-3 backdrop-blur">
|
||||
<div className="mb-2 flex items-center justify-between gap-3 text-[11px] font-bold text-white/70">
|
||||
<span>当前切片构件</span>
|
||||
<span className="font-mono text-cyan-100">{resultVisibleModules.length} 个构件</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{resultVisibleModules.slice(0, 8).map(({ fileName, name, style }) => (
|
||||
<div key={fileName} className="flex min-w-0 items-center gap-2 rounded-lg bg-white/5 px-2 py-1">
|
||||
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: style.color }} />
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] font-bold text-white/75">{name}</span>
|
||||
<span className="font-mono text-[9px] text-white/45">ID {style.partId}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-sm font-bold text-white/30">
|
||||
{latestSegmentationResult ? '正在载入逆向分割映射视图...' : '暂无逆向分割映射视图'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-white/10 bg-slate-950 px-4 py-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Slice Navigator</p>
|
||||
<span className="font-mono text-[10px] font-bold text-cyan-100">
|
||||
{resultMappingSlice + 1} / {Math.max(selectedProject.dicomCount, 1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[28px_1fr_28px] items-center gap-3">
|
||||
<button
|
||||
onClick={() => setResultPreviewSlice((value) => Math.max(0, value - 1))}
|
||||
disabled={!latestSegmentationResult || resultMappingSlice <= 0}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35"
|
||||
title="上一层"
|
||||
>
|
||||
<ChevronLeft size={15} />
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={resultMaxSlice}
|
||||
value={resultMappingSlice}
|
||||
disabled={!latestSegmentationResult}
|
||||
onChange={(event) => setResultPreviewSlice(Number(event.target.value))}
|
||||
className="h-2 w-full accent-cyan-400 disabled:opacity-35"
|
||||
aria-label="项目库逆向分割结果切片导航"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setResultPreviewSlice((value) => Math.min(resultMaxSlice, value + 1))}
|
||||
disabled={!latestSegmentationResult || resultMappingSlice >= resultMaxSlice}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35"
|
||||
title="下一层"
|
||||
>
|
||||
<ChevronRight size={15} />
|
||||
</button>
|
||||
<div className="min-h-[560px]">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="flex items-center gap-2 font-bold text-slate-700">
|
||||
<Layers size={18} className="text-cyan-500" />
|
||||
逆向分割映射视图
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-center justify-end gap-1.5">
|
||||
<div className="flex rounded-xl bg-slate-100 p-1">
|
||||
{displayModes.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
onClick={() => setResultDisplayMode(mode.id)}
|
||||
className={`rounded-lg px-2 py-1 text-[10px] font-bold transition ${
|
||||
resultDisplayMode === mode.id ? 'bg-white text-cyan-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setResultRotation((value) => (value + 270) % 360)}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 hover:text-cyan-600"
|
||||
title="左转 90°"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResultRotation((value) => (value + 90) % 360)}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 hover:text-cyan-600"
|
||||
title="右转 90°"
|
||||
>
|
||||
<RotateCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{latestSegmentationResult ? (
|
||||
<VoxelizationMappingView
|
||||
project={selectedProject}
|
||||
moduleStyles={latestResultStyles}
|
||||
modelPose={latestResultPose}
|
||||
detailLimit={resultDisplayOption.limit}
|
||||
slice={resultMappingSlice}
|
||||
totalSlices={selectedProject.dicomCount}
|
||||
onSliceChange={setResultPreviewSlice}
|
||||
displayMode={resultDisplayMode}
|
||||
rotation={resultRotation}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[520px] items-center justify-center rounded-3xl border border-dashed border-slate-200 bg-slate-950 px-8 text-center text-sm font-bold text-white/35">
|
||||
暂无逆向分割映射视图。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -28,9 +28,9 @@ interface ModelPreviewPayload {
|
||||
};
|
||||
}
|
||||
|
||||
type DisplayLevel = 'standard' | 'fine' | 'ultra' | 'solid';
|
||||
type DicomOpacityLevel = 'low' | 'medium' | 'high';
|
||||
type MappingDisplayMode = DicomPreview['mode'];
|
||||
export type DisplayLevel = 'standard' | 'fine' | 'ultra' | 'solid';
|
||||
export type DicomOpacityLevel = 'low' | 'medium' | 'high';
|
||||
export type MappingDisplayMode = DicomPreview['mode'];
|
||||
type ModelPoseKey = keyof ModelPose;
|
||||
type PoseDraftValues = Record<ModelPoseKey, string>;
|
||||
type AxisKey = 'x' | 'y' | 'z';
|
||||
@@ -46,13 +46,13 @@ type WorkspaceLeaveGuard = () => Promise<boolean>;
|
||||
|
||||
const modelPoseKeys: ModelPoseKey[] = ['rotateX', 'rotateY', 'rotateZ', 'translateX', 'translateY', 'translateZ', 'scale'];
|
||||
|
||||
const displayOptions: Array<{ id: DisplayLevel; label: string; limit: number }> = [
|
||||
export const displayOptions: Array<{ id: DisplayLevel; label: string; limit: number }> = [
|
||||
{ id: 'standard', label: '标准', limit: 16000 },
|
||||
{ id: 'fine', label: '精细', limit: 36000 },
|
||||
{ id: 'ultra', label: '超精细', limit: 72000 },
|
||||
{ id: 'solid', label: '实体', limit: 200000 },
|
||||
];
|
||||
const dicomOpacityOptions: Array<{ id: DicomOpacityLevel; label: string; sliceOpacity: number; volumeOpacity: number; boxOpacity: number }> = [
|
||||
export const dicomOpacityOptions: Array<{ id: DicomOpacityLevel; label: string; sliceOpacity: number; volumeOpacity: number; boxOpacity: number }> = [
|
||||
{ id: 'low', label: '低', sliceOpacity: 0.82, volumeOpacity: 0.12, boxOpacity: 0.32 },
|
||||
{ id: 'medium', label: '中', sliceOpacity: 0.92, volumeOpacity: 0.2, boxOpacity: 0.42 },
|
||||
{ id: 'high', label: '高', sliceOpacity: 1, volumeOpacity: 0.32, boxOpacity: 0.54 },
|
||||
@@ -393,7 +393,7 @@ function CoordinateAxesInset({ projection }: { projection: AxisProjection }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FusionThreeView({
|
||||
export function FusionThreeView({
|
||||
project,
|
||||
volume,
|
||||
modelPose,
|
||||
@@ -1737,7 +1737,7 @@ function drawVoxelOverlayLayer(
|
||||
return { activeModules, filledPixels, segmentCount, modules };
|
||||
}
|
||||
|
||||
function VoxelizationMappingView({
|
||||
export function VoxelizationMappingView({
|
||||
project,
|
||||
moduleStyles,
|
||||
modelPose,
|
||||
|
||||
@@ -23,6 +23,7 @@ interface UserFormState {
|
||||
account: string;
|
||||
department: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const emptyUserForm: UserFormState = {
|
||||
@@ -30,6 +31,7 @@ const emptyUserForm: UserFormState = {
|
||||
account: '',
|
||||
department: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
export default function UserManagement() {
|
||||
@@ -83,6 +85,7 @@ export default function UserManagement() {
|
||||
account: user.account,
|
||||
department: user.department,
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
setFormMode('edit');
|
||||
setMessage(`正在编辑用户:${user.name}`);
|
||||
@@ -95,6 +98,7 @@ export default function UserManagement() {
|
||||
account: user.account,
|
||||
department: user.department,
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
setFormMode('password');
|
||||
setMessage(`正在修改密码:${user.name}`);
|
||||
@@ -114,13 +118,18 @@ export default function UserManagement() {
|
||||
const account = form.account.trim();
|
||||
const department = form.department.trim();
|
||||
const password = form.password.trim();
|
||||
const confirmPassword = form.confirmPassword.trim();
|
||||
|
||||
if (!name || !account || !department || (formMode === 'create' && !password)) {
|
||||
setMessage('姓名、账号、科室不能为空;新增用户必须填写密码');
|
||||
if (!name || !account || !department) {
|
||||
setMessage('姓名、账号、科室不能为空');
|
||||
return;
|
||||
}
|
||||
if (formMode === 'password' && !password) {
|
||||
setMessage('请输入新密码');
|
||||
if ((formMode === 'create' || formMode === 'password') && (!password || !confirmPassword)) {
|
||||
setMessage('请输入两遍密码');
|
||||
return;
|
||||
}
|
||||
if ((formMode === 'create' || formMode === 'password') && password !== confirmPassword) {
|
||||
setMessage('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,7 +143,7 @@ export default function UserManagement() {
|
||||
name,
|
||||
account,
|
||||
department,
|
||||
...(password ? { password } : {}),
|
||||
...(formMode === 'password' ? { password } : {}),
|
||||
});
|
||||
setMessage(formMode === 'password' ? `已更新 ${name} 的密码` : `已更新用户:${name}`);
|
||||
}
|
||||
@@ -326,34 +335,92 @@ export default function UserManagement() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
value={form.name}
|
||||
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
|
||||
disabled={formMode === 'password'}
|
||||
placeholder="姓名"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
|
||||
/>
|
||||
<input
|
||||
value={form.account}
|
||||
onChange={(event) => setForm((current) => ({ ...current, account: event.target.value }))}
|
||||
disabled={formMode === 'password'}
|
||||
placeholder="账号"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
|
||||
/>
|
||||
<input
|
||||
value={form.department}
|
||||
onChange={(event) => setForm((current) => ({ ...current, department: event.target.value }))}
|
||||
disabled={formMode === 'password'}
|
||||
placeholder="科室"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => setForm((current) => ({ ...current, password: event.target.value }))}
|
||||
placeholder={formMode === 'edit' ? '新密码,留空则不修改' : '密码'}
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
{formMode !== 'password' ? (
|
||||
<>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">姓名</span>
|
||||
<input
|
||||
value={form.name}
|
||||
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
|
||||
placeholder="请输入用户姓名"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">账号</span>
|
||||
<input
|
||||
value={form.account}
|
||||
onChange={(event) => setForm((current) => ({ ...current, account: event.target.value }))}
|
||||
placeholder="请输入登录账号"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">科室</span>
|
||||
<input
|
||||
value={form.department}
|
||||
onChange={(event) => setForm((current) => ({ ...current, department: event.target.value }))}
|
||||
placeholder="请输入所属科室"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
</label>
|
||||
{formMode === 'create' && (
|
||||
<>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">密码</span>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => setForm((current) => ({ ...current, password: event.target.value }))}
|
||||
placeholder="请输入初始密码"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">确认密码</span>
|
||||
<input
|
||||
type="password"
|
||||
value={form.confirmPassword}
|
||||
onChange={(event) => setForm((current) => ({ ...current, confirmPassword: event.target.value }))}
|
||||
placeholder="请再次输入初始密码"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-2xl border border-slate-100 bg-slate-50 p-4">
|
||||
<p className="text-xs font-bold text-slate-400">当前修改对象</p>
|
||||
<div className="mt-2 grid gap-2 text-sm font-semibold text-slate-700">
|
||||
<span>{form.name}</span>
|
||||
<span className="font-mono text-slate-500">{form.account}</span>
|
||||
<span className="text-slate-500">{form.department}</span>
|
||||
</div>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">新密码</span>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => setForm((current) => ({ ...current, password: event.target.value }))}
|
||||
placeholder="请输入新密码"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-amber-400 focus:ring-2 focus:ring-amber-500/20"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-bold text-slate-500">确认新密码</span>
|
||||
<input
|
||||
type="password"
|
||||
value={form.confirmPassword}
|
||||
onChange={(event) => setForm((current) => ({ ...current, confirmPassword: event.target.value }))}
|
||||
placeholder="请再次输入新密码"
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-amber-400 focus:ring-2 focus:ring-amber-500/20"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button onClick={closeForm} className="rounded-xl px-4 py-2 text-sm font-bold text-slate-600 hover:bg-slate-100">
|
||||
|
||||
Reference in New Issue
Block a user