diff --git a/design/ui_kits/console/TaskTree.jsx b/design/ui_kits/console/TaskTree.jsx index 90085ac..1a8da5a 100644 --- a/design/ui_kits/console/TaskTree.jsx +++ b/design/ui_kits/console/TaskTree.jsx @@ -354,13 +354,101 @@ function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) { ); } +// 新建任务面板的附件区样式(dropzone 高亮 / 缩略图 chip / 删除角标),一次性注入避免污染全局 +function ensureNewTaskCss() { + if (document.getElementById('maestro-kit-newtask-css')) return; + const s = document.createElement('style'); + s.id = 'maestro-kit-newtask-css'; + s.textContent = ` +.m-nt-drop { display: flex; flex-direction: column; gap: 8px; padding: 10px; border: 1px dashed var(--line); border-radius: var(--radius-sm, 4px); background: var(--bg-deep); transition: border-color .12s, background .12s; } +.m-nt-drop.is-over { border-color: var(--green-dim); background: var(--panel-2); } +.m-nt-hint { font-size: 10.5px; color: var(--faint); letter-spacing: .04em; } +.m-nt-hint.is-over { color: var(--green); } +.m-nt-grid { display: flex; flex-wrap: wrap; gap: 8px; } +.m-nt-chip { position: relative; width: 92px; display: flex; flex-direction: column; gap: 3px; } +.m-nt-thumb { width: 92px; height: 64px; border: 1px solid var(--line); border-radius: var(--radius-sm, 4px); background: var(--panel-2); overflow: hidden; display: flex; align-items: center; justify-content: center; } +.m-nt-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.m-nt-ext { font-family: var(--mono); font-size: 12px; color: var(--cyan); text-transform: uppercase; letter-spacing: .06em; } +.m-nt-name { font-size: 10px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.m-nt-size { font-size: 9.5px; color: var(--faint); font-family: var(--mono); } +.m-nt-del { position: absolute; top: -6px; right: -6px; width: 18px; height: 18px; padding: 0; line-height: 1; border: 1px solid var(--line); border-radius: 50%; background: var(--panel); color: var(--muted); cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: color .12s, border-color .12s, background .12s; } +.m-nt-del:hover { color: var(--red); border-color: var(--red-dim); background: var(--panel-2); } +`; + document.head.appendChild(s); +} + +// 三来源(选择/粘贴/拖拽)统一去重键:name+size+lastModified +function ntFileKey(f) { return (f.name || '') + ' ' + f.size + ' ' + f.lastModified; } +// 由 MIME 推断扩展名(剪贴板图片常无文件名) +function ntExtFromType(type) { + if (!type) return ''; + const sub = String(type).split('/')[1] || ''; + const m = { jpeg: 'jpg', svg: 'svg', 'svg+xml': 'svg' }; + return sub ? '.' + (m[sub] || sub) : ''; +} +// 为无名文件(粘贴的截图)补一个稳定文件名 +function ntNamed(f, i) { + if (f.name) return f; + const name = 'pasted-' + Date.now() + (i ? '-' + i : '') + ntExtFromType(f.type); + try { return new File([f], name, { type: f.type, lastModified: f.lastModified }); } catch { return f; } +} +// chip 角标显示的扩展名文字 +function ntExtLabel(f) { + const fromName = f.name && f.name.includes('.') ? f.name.split('.').pop() : ''; + return (fromName || ntExtFromType(f.type).replace(/^\./, '') || 'file').toUpperCase(); +} + function NewTaskPanel({ tasks, onCancel, onCreate, t }) { + ensureNewTaskCss(); const { Button, Input, Select, ComplexitySeg } = window.MaestroDesignSystem_a6a290; const [title, setTitle] = React.useState(''); const [complexity, setComplexity] = React.useState('auto'); const [priority, setPriority] = React.useState('1'); const [parentId, setParentId] = React.useState(''); const [files, setFiles] = React.useState([]); + 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 key = ntFileKey(f); + const map = urlMapRef.current; + if (!map.has(key)) map.set(key, URL.createObjectURL(f)); + return map.get(key); + }; + // 组件卸载时释放全部 objectURL + React.useEffect(() => () => { + urlMapRef.current.forEach((u) => URL.revokeObjectURL(u)); + urlMapRef.current.clear(); + }, []); + // 合并新文件并按复合键去重(剪贴板无名图先补名) + const addFiles = (incoming) => { + if (!incoming || !incoming.length) return; + const named = Array.from(incoming).map((f, i) => ntNamed(f, i)); + 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); } }); + return merged; + }); + }; + const removeFile = (key) => { + setFiles((prev) => prev.filter((f) => ntFileKey(f) !== key)); + const map = urlMapRef.current; + if (map.has(key)) { URL.revokeObjectURL(map.get(key)); map.delete(key); } + }; + const onPaste = (e) => { + const items = (e.clipboardData && e.clipboardData.items) || []; + const imgs = []; + for (let i = 0; i < items.length; i++) { + const it = items[i]; + if (it.kind === 'file' && it.type && it.type.startsWith('image/')) { + const f = it.getAsFile(); + if (f) imgs.push(f); + } + } + if (imgs.length) { e.preventDefault(); addFiles(imgs); } + }; const submit = (e) => { e.preventDefault(); if (!title.trim()) return; @@ -372,7 +460,7 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) { walk(tasks, ''); return (