Spaces:
Sleeping
Sleeping
Update web/src/components/Message.tsx
Browse files- web/src/components/Message.tsx +30 -19
web/src/components/Message.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from "react";
|
| 2 |
import { Button } from "./ui/button";
|
| 3 |
import {
|
|
@@ -77,7 +78,7 @@ export function FileUploadArea({
|
|
| 77 |
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
| 78 |
const [showTypeDialog, setShowTypeDialog] = useState(false);
|
| 79 |
|
| 80 |
-
//
|
| 81 |
const urlCacheRef = useRef<Map<string, string>>(new Map());
|
| 82 |
|
| 83 |
const fingerprint = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
|
|
@@ -89,7 +90,7 @@ export function FileUploadArea({
|
|
| 89 |
];
|
| 90 |
}, [uploadedFiles, pendingFiles]);
|
| 91 |
|
| 92 |
-
//
|
| 93 |
useEffect(() => {
|
| 94 |
const need = new Set<string>();
|
| 95 |
for (const f of allFiles) {
|
|
@@ -97,6 +98,7 @@ export function FileUploadArea({
|
|
| 97 |
need.add(fingerprint(f));
|
| 98 |
}
|
| 99 |
|
|
|
|
| 100 |
for (const [key, url] of urlCacheRef.current.entries()) {
|
| 101 |
if (!need.has(key)) {
|
| 102 |
try {
|
|
@@ -107,9 +109,18 @@ export function FileUploadArea({
|
|
| 107 |
urlCacheRef.current.delete(key);
|
| 108 |
}
|
| 109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
}, [allFiles]);
|
| 111 |
|
| 112 |
-
//
|
| 113 |
useEffect(() => {
|
| 114 |
return () => {
|
| 115 |
for (const url of urlCacheRef.current.values()) {
|
|
@@ -123,15 +134,9 @@ export function FileUploadArea({
|
|
| 123 |
};
|
| 124 |
}, []);
|
| 125 |
|
| 126 |
-
|
| 127 |
-
const getOrCreatePreviewUrl = (file: File) => {
|
| 128 |
const key = fingerprint(file);
|
| 129 |
-
|
| 130 |
-
if (existed) return existed;
|
| 131 |
-
|
| 132 |
-
const url = URL.createObjectURL(file);
|
| 133 |
-
urlCacheRef.current.set(key, url);
|
| 134 |
-
return url;
|
| 135 |
};
|
| 136 |
|
| 137 |
const filterSupportedFiles = (files: File[]) => {
|
|
@@ -195,15 +200,21 @@ export function FileUploadArea({
|
|
| 195 |
|
| 196 |
const renderLeading = (file: File) => {
|
| 197 |
if (isImageFile(file)) {
|
| 198 |
-
const src =
|
| 199 |
return (
|
| 200 |
<div className="h-12 w-12 rounded-md overflow-hidden bg-background border border-border flex-shrink-0">
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
</div>
|
| 208 |
);
|
| 209 |
}
|
|
@@ -359,4 +370,4 @@ export function FileUploadArea({
|
|
| 359 |
)}
|
| 360 |
</Card>
|
| 361 |
);
|
| 362 |
-
}
|
|
|
|
| 1 |
+
确实是看不到你看看这里web/src/components/Message.tsx呢:
|
| 2 |
import React, { useEffect, useMemo, useRef, useState } from "react";
|
| 3 |
import { Button } from "./ui/button";
|
| 4 |
import {
|
|
|
|
| 78 |
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
| 79 |
const [showTypeDialog, setShowTypeDialog] = useState(false);
|
| 80 |
|
| 81 |
+
// ===== objectURL cache(更稳:不用 state,避免时序问题)=====
|
| 82 |
const urlCacheRef = useRef<Map<string, string>>(new Map());
|
| 83 |
|
| 84 |
const fingerprint = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
|
|
|
|
| 90 |
];
|
| 91 |
}, [uploadedFiles, pendingFiles]);
|
| 92 |
|
| 93 |
+
// 维护 cache:只保留当前需要的 image url;移除的立即 revoke
|
| 94 |
useEffect(() => {
|
| 95 |
const need = new Set<string>();
|
| 96 |
for (const f of allFiles) {
|
|
|
|
| 98 |
need.add(fingerprint(f));
|
| 99 |
}
|
| 100 |
|
| 101 |
+
// revoke removed
|
| 102 |
for (const [key, url] of urlCacheRef.current.entries()) {
|
| 103 |
if (!need.has(key)) {
|
| 104 |
try {
|
|
|
|
| 109 |
urlCacheRef.current.delete(key);
|
| 110 |
}
|
| 111 |
}
|
| 112 |
+
|
| 113 |
+
// create missing
|
| 114 |
+
for (const f of allFiles) {
|
| 115 |
+
if (!isImageFile(f)) continue;
|
| 116 |
+
const key = fingerprint(f);
|
| 117 |
+
if (!urlCacheRef.current.has(key)) {
|
| 118 |
+
urlCacheRef.current.set(key, URL.createObjectURL(f));
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
}, [allFiles]);
|
| 122 |
|
| 123 |
+
// unmount:全部 revoke
|
| 124 |
useEffect(() => {
|
| 125 |
return () => {
|
| 126 |
for (const url of urlCacheRef.current.values()) {
|
|
|
|
| 134 |
};
|
| 135 |
}, []);
|
| 136 |
|
| 137 |
+
const getPreviewUrl = (file: File) => {
|
|
|
|
| 138 |
const key = fingerprint(file);
|
| 139 |
+
return urlCacheRef.current.get(key);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
};
|
| 141 |
|
| 142 |
const filterSupportedFiles = (files: File[]) => {
|
|
|
|
| 200 |
|
| 201 |
const renderLeading = (file: File) => {
|
| 202 |
if (isImageFile(file)) {
|
| 203 |
+
const src = getPreviewUrl(file);
|
| 204 |
return (
|
| 205 |
<div className="h-12 w-12 rounded-md overflow-hidden bg-background border border-border flex-shrink-0">
|
| 206 |
+
{src ? (
|
| 207 |
+
<img
|
| 208 |
+
src={src}
|
| 209 |
+
alt={file.name}
|
| 210 |
+
className="h-full w-full object-cover"
|
| 211 |
+
draggable={false}
|
| 212 |
+
/>
|
| 213 |
+
) : (
|
| 214 |
+
<div className="h-full w-full flex items-center justify-center text-muted-foreground">
|
| 215 |
+
<ImageIcon className="h-5 w-5" />
|
| 216 |
+
</div>
|
| 217 |
+
)}
|
| 218 |
</div>
|
| 219 |
);
|
| 220 |
}
|
|
|
|
| 370 |
)}
|
| 371 |
</Card>
|
| 372 |
);
|
| 373 |
+
}
|