feat(V0.2): 任务操作菜单 / 状态指示 / 移动与转子任务 / 移除扩展信息
Docker Build and Push / build-image (push) Successful in 4m13s

1. 任务 ⋯ 菜单 (列表行与看板卡片): 编辑 / 拷贝到其他项目 / 删除
   (antd-mobile ActionSheet, 带任务名标题; 拷贝抽出为 CopyTaskPopup 复用)
2. 点击任务非按钮区域 = 打开编辑页 (原本即如此, ⋯ 不再误触为编辑)
3. 修复状态选择器堆成三行: 此前 .seg 只有 .pv-head 作用域样式,
   详情弹层裸用导致纵向堆叠; 新增通用 .seg 分段样式, 当前项高亮点按即切
4. 移除任务的扩展信息 UI (T-TK-04~07 标注已取消, 决策 35):
   ExtrasCard 组件删除, DB extras 字段保留不再使用;
   项目相关长文本由项目信息块 (T-PJ-13) 承载
5. 任务移动入口: 详情页新增「看板列」ActionSheet 换列(联动裁决不变)
   与「层级」区(转为子任务=选父任务 / 转为顶层), 覆盖无法拖拽的场景
6. 列表页移除「+ 子任务」入口: 添加子任务只在详情页, 或经转子任务

已验证: 99 文件语法 0 错误、导入 0 缺失、文档 149 条/决策 35 连续
This commit is contained in:
jif2.zhang
2026-10-24 21:51:26 +08:00
parent b71ceca3f2
commit 506db0795b
10 changed files with 252 additions and 146 deletions
+61
View File
@@ -0,0 +1,61 @@
import { useEffect, useState } from 'react'
import { DotLoading, Popup, Toast } from 'antd-mobile'
import { projectApi, taskApi } from '../../api/task.js'
// 跨项目拷贝弹层 (T-CP-*): 选择目标项目 → 深拷贝; 人员不属于目标项目时丢弃并提示
export default function CopyTaskPopup({ task, visible, onClose, onDone }) {
const [projects, setProjects] = useState(null)
useEffect(() => {
if (!visible) return
setProjects(null)
projectApi.list()
.then((d) => setProjects((d.items || []).filter((p) => p.id !== task.projectId && !p.archived)))
.catch((e) => {
Toast.show({ content: e.message })
setProjects([])
})
}, [visible, task.projectId])
async function doCopy(projectId, name) {
try {
const r = await taskApi.copy(task.id, projectId)
const dropped = r.dropped || {}
const lost = [dropped.assignee?.name, ...(dropped.members || []).map((m) => m.name)].filter(Boolean)
Toast.show({
content: lost.length
? `已拷贝到「${name}」;${lost.join('、')} 不是该项目成员,已不保留`
: `已拷贝到「${name}」`,
})
onClose?.()
onDone?.()
} catch (e) {
Toast.show({ content: e.message })
}
}
return (
<Popup
visible={visible}
onMaskClick={onClose}
bodyStyle={{
borderTopLeftRadius: 12, borderTopRightRadius: 12,
padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)',
}}
>
<div className="sheet-title">跨项目拷贝</div>
<div className="sheet-sub">「{task.title}」→ 选择目标项目;源任务保持不变</div>
{!projects && <div className="empty" style={{ padding: '20px 0' }}><DotLoading color="#1677ff" /> 加载项目…</div>}
{projects && projects.length === 0 && <div className="extras-empty">没有其他可拷贝的项目</div>}
{(projects || []).map((p) => (
<div className="pick-row" key={p.id} onClick={() => doCopy(p.id, p.name)}>
<div className="avatar">{(p.name || '?').slice(0, 1)}</div>
<span className="pn">{p.name}</span>
<span className="link">拷贝</span>
</div>
))}
<div className="btn-ghost" onClick={onClose}>取消</div>
</Popup>
)
}
-59
View File
@@ -1,59 +0,0 @@
import { useState } from 'react'
// 扩展信息卡片 (T-TK-04~07): 任意条目, 默认收起, 全部非必填
// 条目结构 { label, value, url? }
export default function ExtrasCard({ extras = [], onChange, readOnly = false }) {
const [open, setOpen] = useState(false)
const list = Array.isArray(extras) ? extras : []
const update = (idx, key, val) => {
const next = list.map((e, i) => (i === idx ? { ...e, [key]: val } : e))
onChange?.(next)
}
const add = () => onChange?.([...list, { label: '', value: '', url: '' }])
const remove = (idx) => onChange?.(list.filter((_, i) => i !== idx))
return (
<div className="extras">
<div className="extras-head" onClick={() => setOpen((v) => !v)}>
<span className="caret">{open ? '▾' : '▸'}</span>
<span>扩展信息</span>
{list.length > 0 && <span className="tag">{list.length}</span>}
<span className="cv">{open ? '收起' : '默认收起 · 全部非必填'}</span>
</div>
{open && (
<div className="extras-body">
{list.length === 0 && (
<div className="extras-empty">暂无条目,可记录仓库 / 分支 / 链接等任意信息</div>
)}
{list.map((e, i) => (
<div className="extra-row" key={i}>
<input
className="inp extra-k"
placeholder="名称"
value={e.label || ''}
disabled={readOnly}
onChange={(ev) => update(i, 'label', ev.target.value)}
/>
<input
className="inp extra-v"
placeholder="值"
value={e.value || ''}
disabled={readOnly}
onChange={(ev) => update(i, 'value', ev.target.value)}
/>
{!readOnly && (
<button type="button" className="extra-del" onClick={() => remove(i)}>×</button>
)}
</div>
))}
{!readOnly && (
<button type="button" className="extras-add" onClick={add}>+ 添加一条</button>
)}
</div>
)}
</div>
)
}
+7 -10
View File
@@ -7,7 +7,7 @@ import StatusDot from './StatusDot.jsx'
const MAX_SUB = 3
export default function TaskCard({
task, bindDrag, onClick, onToggleSub, onToggle, onAddSub, dragging = false, highlight = false,
task, bindDrag, onClick, onToggleSub, onToggle, onMore, dragging = false, highlight = false,
}) {
const children = task.children || []
const shown = children.slice(0, MAX_SUB)
@@ -38,6 +38,12 @@ export default function TaskCard({
{task.members?.length > 0 && (
<span className="bmembers">+ {task.members.map((m) => m.name).join('、')}</span>
)}
{onMore && (
<span
className="bmore-btn"
onClick={(e) => { e.stopPropagation(); onMore(task) }}
>⋯</span>
)}
</div>
{shown.map((c) => (
@@ -52,15 +58,6 @@ export default function TaskCard({
</div>
))}
{rest > 0 && <div className="bmore">+{rest} 个子任务</div>}
{onAddSub && (
<div
className="bsub-add"
onClick={(e) => { e.stopPropagation(); onAddSub(task) }}
>
+ 子任务
</div>
)}
</div>
)
}