chore(console): 验证附件创建面板已上线 + 低风险加固(粘贴/拖拽体量上限 + render 副作用收敛)[tsk_ia4uiOfk6ZdV]
任务功能(统一 files 状态 / 缩略图预览 / 单项删除 / 客户端去重 / 5 语 i18n)
已于 65cd5ad、00e6bc7 实现并合并,后端同源存储型 XSS 防护已于 d9d3de2 落实,
逐条核对全部满足,故不重复实现。仅做两处低风险加固(范围限 design/ui_kits/console/**):
- NewTaskPanel.addFiles 增加单文件 25MB 软上限,超限跳过并内联提示,
新增 i18n key attTooLarge(zh/en/es/ja/fr 五语齐备)
- 缩略图 objectURL 改为在 addFiles 中预生成(ensureThumb),render 仅读,
消除 map 渲染期写 urlMapRef 的副作用,避免 React 严格模式下重复创建/泄漏
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -451,13 +451,15 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
|
||||
const [dragOver, setDragOver] = React.useState(false);
|
||||
// 图片缩略图 objectURL 缓存(按去重键),卸载/移除时显式 revoke 防泄漏
|
||||
const urlMapRef = React.useRef(new Map());
|
||||
const thumbUrl = (f) => {
|
||||
if (!f.type || !f.type.startsWith('image/')) return null;
|
||||
const [warn, setWarn] = React.useState('');
|
||||
// 为图片预生成缩略图 objectURL(幂等,按去重键缓存)——只在 addFiles 调用,render 仅读,避免严格模式下重复 render 泄漏
|
||||
const ensureThumb = (f) => {
|
||||
if (!f.type || !f.type.startsWith('image/')) return;
|
||||
const key = ntFileKey(f);
|
||||
const map = urlMapRef.current;
|
||||
if (!map.has(key)) map.set(key, URL.createObjectURL(f));
|
||||
return map.get(key);
|
||||
};
|
||||
const thumbUrl = (f) => (f.type && f.type.startsWith('image/')) ? (urlMapRef.current.get(ntFileKey(f)) || null) : null;
|
||||
// 组件卸载时释放全部 objectURL
|
||||
React.useEffect(() => () => {
|
||||
urlMapRef.current.forEach((u) => URL.revokeObjectURL(u));
|
||||
@@ -466,11 +468,16 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
|
||||
// 合并新文件并按复合键去重(剪贴板无名图先补名)
|
||||
const addFiles = (incoming) => {
|
||||
if (!incoming || !incoming.length) return;
|
||||
const MAX_BYTES = 25 * 1024 * 1024; // 单文件软上限,避免超大截图卡住上传
|
||||
const named = Array.from(incoming).map((f, i) => ntNamed(f, i));
|
||||
const ok = named.filter((f) => f.size <= MAX_BYTES);
|
||||
setWarn(ok.length < named.length ? t.attTooLarge : '');
|
||||
if (!ok.length) return;
|
||||
ok.forEach(ensureThumb); // 预生成缩略图,render 不再产生副作用
|
||||
setFiles((prev) => {
|
||||
const seen = new Set(prev.map(ntFileKey));
|
||||
const merged = prev.slice();
|
||||
named.forEach((f) => { const k = ntFileKey(f); if (!seen.has(k)) { seen.add(k); merged.push(f); } });
|
||||
ok.forEach((f) => { const k = ntFileKey(f); if (!seen.has(k)) { seen.add(k); merged.push(f); } });
|
||||
return merged;
|
||||
});
|
||||
};
|
||||
@@ -530,6 +537,7 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
|
||||
onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }}
|
||||
style={{ fontFamily: 'var(--mono)', fontSize: 12, color: 'var(--ink)' }} />
|
||||
<span className={'m-nt-hint' + (dragOver ? ' is-over' : '')}>{dragOver ? t.dropHint : t.pasteHint}</span>
|
||||
{warn ? <span className="m-nt-hint" style={{ color: 'var(--amber)' }}>{warn}</span> : null}
|
||||
{files.length ? (
|
||||
<div className="m-nt-grid">
|
||||
{files.map((f) => {
|
||||
|
||||
Reference in New Issue
Block a user