From 65cd5add5c89c8d03f1123343405fc2d8b608e3a Mon Sep 17 00:00:00 2001 From: maestro Date: Tue, 30 Jun 2026 01:36:43 +0800 Subject: [PATCH] =?UTF-8?q?maestro(tsk=5FtqxHX6NRQ3iA):=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20NewTaskPanel=EF=BC=9A=E5=89=AA=E8=B4=B4=E6=9D=BF?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E5=9B=BE=E7=89=87=20+=20=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=20+=20=E7=BB=9F=E4=B8=80=20files=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=20+=20=E7=BC=A9=E7=95=A5=E5=9B=BE=E9=A2=84=E8=A7=88?= =?UTF-8?q?=20+=20=E5=8D=95=E4=B8=AA=E5=88=A0=E9=99=A4=20+=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=8E=BB=E9=87=8D=20+=205=20=E8=AF=AD=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/ui_kits/console/TaskTree.jsx | 130 +++++++++++++++++++++++++--- design/ui_kits/console/i18n.js | 5 ++ 2 files changed, 125 insertions(+), 10 deletions(-) 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 (
+ onSubmit={submit} onPaste={onPaste}>
@@ -389,16 +477,38 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
- - {files.length ? ( -
- {files.map((f) => f.name).join(' · ')} +
diff --git a/design/ui_kits/console/i18n.js b/design/ui_kits/console/i18n.js index 5c6132f..7d2d8e1 100644 --- a/design/ui_kits/console/i18n.js +++ b/design/ui_kits/console/i18n.js @@ -40,6 +40,7 @@ window.MAESTRO_I18N = { taskSection: '任务树', newTask: '+ 新建任务', titleLabel: '标题', titlePlaceholder: '要做什么', complexity: '复杂度', cplxAuto: '智能', parentTask: '父任务', topLevel: '(顶层)', priority: '优先级', p0: 'P0 · 高', p1: 'P1 · 中', p2: 'P2 · 低', create: '创建任务', cancel: '取消', + attachLabel: '附件 · 图片/文件(可选)', dropHint: '拖拽文件到此处,或粘贴截图', removeFile: '移除', pasteHint: '支持 ⌘V 粘贴图片', depsWait: '待依赖', specLabel: '改动方案(SPEC)', opsLabel: '将执行的操作(OPERATIONS)', pendingDoc: '待 Claude Code 产出(经 MCP 写入并提交评审)', emptyTasks: '该项目暂无任务 —— 点「+ 新建任务」或经 MCP 创建', empty: '空', @@ -90,6 +91,7 @@ window.MAESTRO_I18N = { taskSection: 'Task tree', newTask: '+ New task', titleLabel: 'Title', titlePlaceholder: 'What needs doing', complexity: 'Complexity', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(top level)', priority: 'Priority', p0: 'P0 · high', p1: 'P1 · medium', p2: 'P2 · low', create: 'Create task', cancel: 'Cancel', + attachLabel: 'Attachments · images/files (optional)', dropHint: 'Drop files here, or paste a screenshot', removeFile: 'Remove', pasteHint: '⌘V to paste images', depsWait: 'deps', specLabel: 'CHANGE SPEC', opsLabel: 'PLANNED OPERATIONS', pendingDoc: 'Awaiting Claude Code output (written via MCP, then submitted for review)', emptyTasks: 'No tasks in this project — hit "+ New task" or create via MCP', empty: 'EMPTY', @@ -140,6 +142,7 @@ window.MAESTRO_I18N = { taskSection: 'Árbol de tareas', newTask: '+ Nueva tarea', titleLabel: 'Título', titlePlaceholder: 'Qué hay que hacer', complexity: 'Complejidad', cplxAuto: 'AUTO', parentTask: 'Padre', topLevel: '(raíz)', priority: 'Prioridad', p0: 'P0 · alta', p1: 'P1 · media', p2: 'P2 · baja', create: 'Crear tarea', cancel: 'Cancelar', + attachLabel: 'Adjuntos · imágenes/archivos (opcional)', dropHint: 'Suelta archivos aquí o pega una captura', removeFile: 'Quitar', pasteHint: '⌘V para pegar imágenes', depsWait: 'deps', specLabel: 'SPEC DE CAMBIOS', opsLabel: 'OPERACIONES PREVISTAS', pendingDoc: 'Esperando salida de Claude Code (vía MCP, luego a revisión)', emptyTasks: 'Sin tareas — pulsa "+ Nueva tarea" o crea vía MCP', empty: 'VACÍO', @@ -190,6 +193,7 @@ window.MAESTRO_I18N = { taskSection: 'タスクツリー', newTask: '+ 新規タスク', titleLabel: 'タイトル', titlePlaceholder: '何をしますか', complexity: '複雑度', cplxAuto: '智能', parentTask: '親タスク', topLevel: '(トップ)', priority: '優先度', p0: 'P0 · 高', p1: 'P1 · 中', p2: 'P2 · 低', create: 'タスク作成', cancel: 'キャンセル', + attachLabel: '添付 · 画像/ファイル(任意)', dropHint: 'ここにファイルをドロップ、または貼り付け', removeFile: '削除', pasteHint: '⌘Vで画像を貼り付け', depsWait: '依存待ち', specLabel: 'SPEC · 変更案', opsLabel: 'OPERATIONS · 操作', pendingDoc: 'Claude Code の出力待ち(MCP 経由で書き込み後レビューへ)', emptyTasks: 'タスクなし — 「+ 新規タスク」または MCP で作成', empty: '空', @@ -240,6 +244,7 @@ window.MAESTRO_I18N = { taskSection: 'Arbre des tâches', newTask: '+ Nouvelle tâche', titleLabel: 'Titre', titlePlaceholder: 'Que faut-il faire', complexity: 'Complexité', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(racine)', priority: 'Priorité', p0: 'P0 · haute', p1: 'P1 · moyenne', p2: 'P2 · basse', create: 'Créer la tâche', cancel: 'Annuler', + attachLabel: 'Pièces jointes · images/fichiers (facultatif)', dropHint: 'Déposez des fichiers ici ou collez une capture', removeFile: 'Retirer', pasteHint: '⌘V pour coller des images', depsWait: 'deps', specLabel: 'SPEC DES CHANGEMENTS', opsLabel: 'OPÉRATIONS PRÉVUES', pendingDoc: 'En attente de la sortie de Claude Code (via MCP, puis revue)', emptyTasks: 'Aucune tâche — « + Nouvelle tâche » ou via MCP', empty: 'VIDE',