|
|
|
@@ -253,6 +253,7 @@ function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleO
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
|
|
|
|
|
ensureNewTaskCss();
|
|
|
|
|
const { Button, Input, Select } = window.MaestroDesignSystem_a6a290;
|
|
|
|
|
const label = task.doc ? t.specLabel : task.ops ? t.opsLabel : null;
|
|
|
|
|
const text = task.doc || task.ops;
|
|
|
|
@@ -283,8 +284,37 @@ function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
|
|
|
|
|
{ops.requeue && canRequeue ? <button onClick={() => ops.requeue(task.id)} style={btn('green')}>↻ 重投</button> : null}
|
|
|
|
|
{ops.cancel && canCancel ? <button onClick={() => ops.cancel(task.id)} style={btn('amber')}>⊘ 取消</button> : null}
|
|
|
|
|
{ops.del ? <button onClick={() => { if (confirm('删除任务「' + task.title + '」?子任务一并删除,不可恢复。')) ops.del(task.id); }} style={btn('red')}>🗑 删除</button> : null}
|
|
|
|
|
{attachments.length ? <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--faint)' }}>📎 {attachments.map((a) => a.name).join(' · ')}</span> : null}
|
|
|
|
|
</div>
|
|
|
|
|
{/* 附件画廊:图片缩略图(点击新标签预览)+ 下载 + 删除(确认后调用 taskOps.delAttachment) */}
|
|
|
|
|
{attachments.length ? (
|
|
|
|
|
<div onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>{t.attSection} · {attachments.length}</div>
|
|
|
|
|
<div className="m-nt-grid">
|
|
|
|
|
{attachments.map((a) => {
|
|
|
|
|
const dn = attDiskName(a);
|
|
|
|
|
const isImg = (a.type || '').startsWith('image/');
|
|
|
|
|
const url = attUrl(task.id, dn);
|
|
|
|
|
return (
|
|
|
|
|
<div key={dn || a.name} className="m-nt-chip">
|
|
|
|
|
<a className="m-att-thumb-link" href={url} target="_blank" rel="noopener noreferrer" title={a.name}>
|
|
|
|
|
<span className="m-nt-thumb">
|
|
|
|
|
{isImg ? <img src={url} alt={a.name} /> : <span className="m-nt-ext">{(a.name.includes('.') ? a.name.split('.').pop() : (a.type || 'file').split('/').pop() || 'file').toUpperCase()}</span>}
|
|
|
|
|
</span>
|
|
|
|
|
</a>
|
|
|
|
|
<span className="m-nt-name" title={a.name}>{a.name}</span>
|
|
|
|
|
<span className="m-att-act">
|
|
|
|
|
<a className="m-att-btn" href={attUrl(task.id, dn, true)} download={a.name}>↓ {t.attDownload}</a>
|
|
|
|
|
{ops.delAttachment ? (
|
|
|
|
|
<button type="button" className="m-att-btn is-del"
|
|
|
|
|
onClick={() => { if (confirm(t.attDeleteConfirm.replace('{name}', a.name))) ops.delAttachment(task.id, dn); }}>✕</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
{/* 编辑表单 */}
|
|
|
|
|
{editing ? (
|
|
|
|
|
<div style={{ display: 'grid', gap: 12, padding: 12, border: '1px solid var(--line)', borderRadius: 6, background: 'var(--bg-deep)' }} onClick={(e) => e.stopPropagation()}>
|
|
|
|
@@ -373,10 +403,22 @@ function ensureNewTaskCss() {
|
|
|
|
|
.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); }
|
|
|
|
|
.m-att-thumb-link { display: block; width: 92px; height: 64px; }
|
|
|
|
|
.m-att-act { display: flex; gap: 6px; align-items: center; }
|
|
|
|
|
.m-att-btn { font-family: var(--mono); font-size: 10px; line-height: 1; padding: 3px 6px; border: 1px solid var(--line); border-radius: var(--radius-sm, 4px); background: var(--panel); color: var(--muted); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; transition: color .12s, border-color .12s, background .12s; }
|
|
|
|
|
.m-att-btn:hover { color: var(--green); border-color: var(--green-dim); background: var(--panel-2); }
|
|
|
|
|
.m-att-btn.is-del:hover { color: var(--red); border-color: var(--red-dim); }
|
|
|
|
|
`;
|
|
|
|
|
document.head.appendChild(s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 已建任务附件:磁盘文件名(后端 :name 标识符 = basename(att.path),非展示名 att.name)
|
|
|
|
|
function attDiskName(a) { return (a.path || '').split('/').pop() || a.name || ''; }
|
|
|
|
|
// 附件预览/下载 URL(UI kit 内联,零外部依赖;与 app/src/api.js attachmentUrl 同构)
|
|
|
|
|
function attUrl(taskId, name, download) {
|
|
|
|
|
return `/api/tasks/${taskId}/attachments/${encodeURIComponent(name)}${download ? '?download=1' : ''}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 三来源(选择/粘贴/拖拽)统一去重键:name+size+lastModified
|
|
|
|
|
function ntFileKey(f) { return (f.name || '') + ' |