From 506db0795b9875d235c00d57ba4ee584e028104f Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Sat, 24 Oct 2026 21:51:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(V0.2):=20=E4=BB=BB=E5=8A=A1=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E8=8F=9C=E5=8D=95=20/=20=E7=8A=B6=E6=80=81=E6=8C=87?= =?UTF-8?q?=E7=A4=BA=20/=20=E7=A7=BB=E5=8A=A8=E4=B8=8E=E8=BD=AC=E5=AD=90?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=20/=20=E7=A7=BB=E9=99=A4=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 连续 --- docs/V0.2/任务模块/任务模块功能设计.md | 8 +- docs/V0.2/任务模块/任务模块需求文档.md | 9 +- src/components/task/CopyTaskPopup.jsx | 61 ++++++++++ src/components/task/ExtrasCard.jsx | 59 --------- src/components/task/TaskCard.jsx | 17 ++- src/pages/task/BoardView.jsx | 3 +- src/pages/task/ListView.jsx | 13 +- src/pages/task/ProjectView.jsx | 55 ++++++++- src/pages/task/TaskDetail.jsx | 160 ++++++++++++++++--------- src/styles/globals.css | 13 ++ 10 files changed, 252 insertions(+), 146 deletions(-) create mode 100644 src/components/task/CopyTaskPopup.jsx delete mode 100644 src/components/task/ExtrasCard.jsx diff --git a/docs/V0.2/任务模块/任务模块功能设计.md b/docs/V0.2/任务模块/任务模块功能设计.md index e7268fc..69fd4e8 100644 --- a/docs/V0.2/任务模块/任务模块功能设计.md +++ b/docs/V0.2/任务模块/任务模块功能设计.md @@ -145,10 +145,10 @@ | T-TK-01 | `Task` 表 | 字段:`title`/`description`/`projectId`/`columnId`/`status`/`assigneeId`/`creatorId`/`doneAt`/`sortOrder` + 关联成员/标签/扩展 | | T-TK-02 | `title` 非空校验 | 其余字段允许空;trim 后为空 ⇒ 400 | | T-TK-03 | `status` 枚举三值 | DB 层用 `VARCHAR(8)` + 应用层校验(避免 ENUM 改值困难) | -| T-TK-04 | `TaskDetail` → `ExtrasCard` 组件 | 任意条目增删改,无字段上限(建议 ≤ 20 条) | -| T-TK-05 | `Task.extras` JSON | 结构 `[{label, value, url?}]`;`value` 支持纯文本或 URL | -| T-TK-06 | `ExtrasCard` 默认 `collapsed=true` | 首次打开详情为收起态 | -| T-TK-07 | 无必填校验 | 全空可保存 | +| T-TK-04 | **已移除 UI**(2026-10-24):`ExtrasCard` 组件删除;信息记录由项目信息块(T-PJ-13)承载 | +| T-TK-05 | **随 T-TK-04 移除**;`extras` 字段在 DB 保留不再使用 | +| T-TK-06 | **随 T-TK-04 移除** | +| T-TK-07 | **随 T-TK-04 移除** | | T-TK-08 | `Task.assigneeId` 单值 | 重复设置以最后一次为准 | | T-TK-09 | 新建时服务端默认赋值 | 未显式传 `assigneeId` ⇒ 取 `req.user.id`(B-10) | | T-TK-10 | `assigneeId` 允许 `null` | 显式传 `null` 即清空,UI 显示「未指派」 | diff --git a/docs/V0.2/任务模块/任务模块需求文档.md b/docs/V0.2/任务模块/任务模块需求文档.md index 324cadd..7d9ffe9 100644 --- a/docs/V0.2/任务模块/任务模块需求文档.md +++ b/docs/V0.2/任务模块/任务模块需求文档.md @@ -230,10 +230,10 @@ | T-TK-01 | 任务包含**基础字段**:标题(必填)、描述、所属项目、看板列、任务状态、负责人、任务成员、标签、创建人、创建时间、更新时间 | 新建任务后上述字段均可正确保存与读取 | P0 | | T-TK-02 | 标题为**唯一必填**字段,其余字段均可为空 | 仅填标题即可保存成功;仅清空标题时拒绝保存并提示 | P0 | | T-TK-03 | 任务状态为三值枚举:`未开始` / `执行中` / `完成` | 任一任务的状态只能取这三个值之一,不存在第四种取值 | P0 | -| T-TK-04 | 内置**扩展信息卡片**:可增加任意数量的信息条目,承载开发源码等扩展信息 | 可新增 / 编辑 / 删除任意条目,条目数量不受固定字段限制 | P0 | -| T-TK-05 | 扩展信息条目支持**任意名称 + 值**,值可为单行文本或链接 | 可录入「仓库 = gitlab/xxx」「分支 = dev」「链接 = https://…」等自由内容 | P0 | -| T-TK-06 | 扩展信息卡片**可折叠**且**默认收起** | 首次打开任务详情,扩展区为收起状态;可展开 / 收起 | P0 | -| T-TK-07 | 扩展信息卡片**全部非必填**,全部留空不影响任务保存 | 只填标题保存后,扩展区为空仍成功 | P0 | +| T-TK-04 | ~~内置扩展信息卡片~~ **已移除**(2026-10-24 评审):相关信息记录改由**项目信息块**承载(T-PJ-13) | — | 已取消 | +| T-TK-05 | ~~扩展信息条目~~ **随 T-TK-04 一并移除** | — | 已取消 | +| T-TK-06 | ~~扩展信息卡片折叠~~ **随 T-TK-04 一并移除** | — | 已取消 | +| T-TK-07 | ~~扩展信息非必填~~ **随 T-TK-04 一并移除** | — | 已取消 | | T-TK-08 | 每条任务有**负责人(单人)** | 任务最多只有一个负责人,重复设置时以最后一次为准 | P0 | | T-TK-09 | 新建任务时**负责人默认填入创建者本人** | 新建任务后未手动修改时,负责人即为创建者 | P0 | | T-TK-10 | 负责人**允许清空为空** | 清空后任务可正常保存,展示为「未指派」 | P0 | @@ -432,6 +432,7 @@ | 32 | 项目信息管理包含什么? | 改名、归档 / 取消归档、删除(个人项目禁用)、退出(创建人可退),入口在项目管理内(T-PJ-12) | | 33 | 任务详情底部按钮遮挡? | 弹层底部预留**安全区 + 12px** 距离;小屏下弹层可滚动,底部按钮完整可见(T-UI-11) | | 34 | 项目相关长文本信息记录在哪? | 项目管理内新增**信息块**分区:任意多个 {标题, 长文本} 块,独立编辑,属项目级数据(T-PJ-13) | +| 35 | 任务级「扩展信息」的去留? | **移除 UI**(2026-10-24 评审):任务不再带扩展信息,项目相关长文本由项目信息块(T-PJ-13)承载;DB 字段保留不用 | **仍待评审确认(影响实现口径):** diff --git a/src/components/task/CopyTaskPopup.jsx b/src/components/task/CopyTaskPopup.jsx new file mode 100644 index 0000000..a3ea884 --- /dev/null +++ b/src/components/task/CopyTaskPopup.jsx @@ -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 ( + +
跨项目拷贝
+
「{task.title}」→ 选择目标项目;源任务保持不变
+ + {!projects &&
加载项目…
} + {projects && projects.length === 0 &&
没有其他可拷贝的项目
} + {(projects || []).map((p) => ( +
doCopy(p.id, p.name)}> +
{(p.name || '?').slice(0, 1)}
+ {p.name} + 拷贝 +
+ ))} +
取消
+
+ ) +} diff --git a/src/components/task/ExtrasCard.jsx b/src/components/task/ExtrasCard.jsx deleted file mode 100644 index 78b0e73..0000000 --- a/src/components/task/ExtrasCard.jsx +++ /dev/null @@ -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 ( -
-
setOpen((v) => !v)}> - {open ? '▾' : '▸'} - 扩展信息 - {list.length > 0 && {list.length}} - {open ? '收起' : '默认收起 · 全部非必填'} -
- - {open && ( -
- {list.length === 0 && ( -
暂无条目,可记录仓库 / 分支 / 链接等任意信息
- )} - {list.map((e, i) => ( -
- update(i, 'label', ev.target.value)} - /> - update(i, 'value', ev.target.value)} - /> - {!readOnly && ( - - )} -
- ))} - {!readOnly && ( - - )} -
- )} -
- ) -} diff --git a/src/components/task/TaskCard.jsx b/src/components/task/TaskCard.jsx index 43d0820..23a9ecb 100644 --- a/src/components/task/TaskCard.jsx +++ b/src/components/task/TaskCard.jsx @@ -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 && ( + {task.members.map((m) => m.name).join('、')} )} + {onMore && ( + { e.stopPropagation(); onMore(task) }} + >⋯ + )} {shown.map((c) => ( @@ -52,15 +58,6 @@ export default function TaskCard({ ))} {rest > 0 &&
+{rest} 个子任务
} - - {onAddSub && ( -
{ e.stopPropagation(); onAddSub(task) }} - > - + 子任务 -
- )} ) } diff --git a/src/pages/task/BoardView.jsx b/src/pages/task/BoardView.jsx index a21c7f8..0f31c09 100644 --- a/src/pages/task/BoardView.jsx +++ b/src/pages/task/BoardView.jsx @@ -6,7 +6,7 @@ import useLongPressDrag from '../../components/task/useLongPressDrag.js' // 看板视图 (T-BD-01, T-UI-07, T-ST-06/12) // 列横向滚动; 长按卡片拖拽换列 / 降为子任务 export default function BoardView({ - tasks, columns, onMoveColumn, onMakeSubtask, onOpen, onToggleSub, onToggle, onQuickAdd, + tasks, columns, onMoveColumn, onMakeSubtask, onOpen, onToggleSub, onToggle, onMore, onQuickAdd, }) { const [overCol, setOverCol] = useState(null) @@ -54,6 +54,7 @@ export default function BoardView({ onClick={() => { if (!drag.dragging) onOpen?.(t) }} onToggleSub={onToggleSub} onToggle={onToggle} + onMore={onMore} /> ))} {list.length === 0 &&
暂无任务
} diff --git a/src/pages/task/ListView.jsx b/src/pages/task/ListView.jsx index 6b70c26..48db7c3 100644 --- a/src/pages/task/ListView.jsx +++ b/src/pages/task/ListView.jsx @@ -2,10 +2,10 @@ import { useState } from 'react' import TaskRow from '../../components/task/TaskRow.jsx' // 列表视图 (T-VW-01, T-ST-04/05) -// 按看板列分组展示(列表不分组也允许, 这里默认分组更接近 Todoist) -// 子任务缩进 + 折叠展开 +// 按看板列分组展示; 子任务缩进 + 折叠展开 +// 子任务的添加只在任务详情页完成 (或把别的任务转为子任务), 列表页不放入口 export default function ListView({ - tasks, columns, onToggle, onOpen, onAddSub, + tasks, columns, onToggle, onOpen, onMore, }) { const [collapsed, setCollapsed] = useState({}) @@ -47,12 +47,12 @@ export default function ListView({ task={t} onToggle={onToggle} onClick={() => onOpen(t)} - onMore={() => onOpen(t)} + onMore={onMore} /> {kids.length > 0 && ( <> {!isCollapsed && kids.map((c) => ( - onOpen(c)} /> + onOpen(c)} onMore={onMore} /> ))}
)} - {kids.length === 0 && onAddSub && ( -
onAddSub(t)}>+ 子任务
- )}
) })} diff --git a/src/pages/task/ProjectView.jsx b/src/pages/task/ProjectView.jsx index 6d8687b..ebc47f8 100644 --- a/src/pages/task/ProjectView.jsx +++ b/src/pages/task/ProjectView.jsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { DotLoading, Toast } from 'antd-mobile' +import { ActionSheet, Dialog, DotLoading, Toast } from 'antd-mobile' import { projectApi, taskApi } from '../../api/task.js' import { filterVisible, msToNextBoundary } from './date.js' import { getViewMode, setViewMode } from './prefs.js' @@ -7,6 +7,7 @@ import ListView from './ListView.jsx' import BoardView from './BoardView.jsx' import TaskDetail from './TaskDetail.jsx' import ProjectManage from './ProjectManage.jsx' +import CopyTaskPopup from '../../components/task/CopyTaskPopup.jsx' // 项目视图容器: 列表 / 看板 切换 + 数据装载 + 乐观更新 // 嵌在任务首屏下部 (T-VW-09); 头部仅保留「项目管理」与「+ 新增」(T-PJ-11) @@ -20,6 +21,7 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit const [showManage, setShowManage] = useState(false) const [adding, setAdding] = useState(false) const [tick, setTick] = useState(0) // 保留窗口重算触发 + const [copyTask, setCopyTask] = useState(null) // ⋯菜单触发的跨项目拷贝 const load = useCallback(() => { setErr('') @@ -136,6 +138,44 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit } } + /** 任务 ⋯ 菜单: 编辑 / 拷贝到其他项目 / 删除 */ + function openTaskMenu(task) { + ActionSheet.show({ + extra: ( +
+
{task.title}
+
+ ), + actions: [ + { text: '编辑', key: 'edit' }, + { text: '拷贝到其他项目', key: 'copy' }, + { text: '删除', key: 'delete', bold: true, danger: true }, + ], + cancelText: '取消', + onAction: async (item) => { + if (item.key === 'edit') setOpenTask(task) + else if (item.key === 'copy') setCopyTask(task) + else if (item.key === 'delete') { + const kids = task.children?.length || 0 + const ok = await Dialog.confirm({ + content: kids + ? `删除「${task.title}」?将一并删除 ${kids} 条子任务。` + : `删除「${task.title}」?`, + confirmText: '删除', + }) + if (!ok) return + try { + await taskApi.remove(task.id) + Toast.show({ content: '已删除' }) + load() + } catch (e) { + Toast.show({ content: e.message }) + } + } + }, + }) + } + if (err) { return (
@@ -196,7 +236,7 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit columns={columns} onToggle={toggleDone} onOpen={(t) => setOpenTask(t)} - onAddSub={(parent) => setOpenTask(parent)} + onMore={openTaskMenu} /> ) : ( setOpenTask(t)} onToggleSub={(c) => toggleDone(c)} onToggle={(t) => toggleDone(t)} + onMore={openTaskMenu} onQuickAdd={() => { setAdding(true) }} /> )} @@ -217,12 +258,22 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit task={openTask} columns={columns} members={project.members || []} + tasks={tasks || []} onClose={() => setOpenTask(null)} onSaved={() => { setOpenTask(null); load() }} onChanged={load} /> )} + {copyTask && ( + setCopyTask(null)} + onDone={load} + /> + )} + {showManage && ( ({ title: task.title || '', description: task.description || '', - extras: task.extras || [], memberIds: (task.members || []).map((m) => m.userId), assigneeId: task.assignee?.id ?? null, status: task.status, @@ -22,22 +24,28 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o const [subs, setSubs] = useState(task.children || []) const [newSub, setNewSub] = useState('') const [saving, setSaving] = useState(false) - const [picker, setPicker] = useState(null) // 'assignee' | 'members' - const [copies, setCopies] = useState(null) // 其他项目列表(拷贝用) + const [picker, setPicker] = useState(null) // 'assignee' | 'members' + const [copyOpen, setCopyOpen] = useState(false) + const [parentPick, setParentPick] = useState(false) // 转为子任务: 选择父任务 - // 键盘弹出插入量: 抬升弹层内容, 输入框不被遮挡 (bug#4) + // 键盘弹出插入量: 抬升弹层内容, 输入框不被遮挡 const kb = useKeyboardInset() // 返回键: 先关详情弹层, 不离开任务页 useOverlayBack(true, onClose) const assigneeName = members.find((m) => m.userId === form.assigneeId)?.name const memberNames = members.filter((m) => form.memberIds.includes(m.userId)).map((m) => m.name) + const currentCol = (columns || []).find((c) => c.id === task.columnId) + const isSubtask = task.parentId != null + // 可作为父任务的候选: 同项目顶层任务, 排除自己与已有子任务的任务 (T-ST-01/13) + const parentCandidates = (tasks || []).filter( + (t) => t.id !== task.id && (t.children || []).length === 0, + ) useEffect(() => { setForm({ title: task.title || '', description: task.description || '', - extras: task.extras || [], memberIds: (task.members || []).map((m) => m.userId), assigneeId: task.assignee?.id ?? null, status: task.status, @@ -53,7 +61,6 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o const r = await taskApi.update(task.id, { title: form.title.trim(), description: form.description, - extras: form.extras, assigneeId: form.assigneeId, memberIds: form.memberIds, status: form.status, @@ -68,6 +75,39 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o } } + /** 换列: 服务端按目标列绑定裁决状态 (T-BD-04/07) */ + function pickColumn() { + ActionSheet.show({ + actions: (columns || []).map((c) => ({ + text: c.name + (c.id === task.columnId ? ' ✓' : ''), + key: String(c.id), + })), + cancelText: '取消', + onAction: async (item) => { + const columnId = Number(item.key) + if (columnId === task.columnId) return + try { + const r = await taskApi.update(task.id, { columnId }) + Toast.show({ content: `已移入 ${(columns || []).find((c) => c.id === columnId)?.name || '目标列'}` }) + onSaved?.(r.item) + } catch (e) { + Toast.show({ content: e.message }) + } + }, + }) + } + + /** 转为子任务 (T-ST-12) / 转为顶层 */ + async function changeParent(parentId) { + try { + const r = await taskApi.update(task.id, { parentId }) + Toast.show({ content: parentId ? '已转为子任务' : '已转为顶层任务' }) + onSaved?.(r.item) + } catch (e) { + Toast.show({ content: e.message }) + } + } + async function addSub() { const t = newSub.trim() if (!t) return @@ -109,32 +149,6 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o } } - async function openCopy() { - try { - const d = await projectApi.list() - setCopies((d.items || []).filter((p) => p.id !== task.projectId && !p.archived)) - } catch (e) { - Toast.show({ content: e.message }) - } - } - - async function doCopy(projectId) { - 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 - ? `已拷贝到目标项目;${lost.join('、')} 不是该项目成员,已不保留` - : '已拷贝到目标项目', - }) - setCopies(null) - onChanged?.() - } catch (e) { - Toast.show({ content: e.message }) - } - } - return (
{task.title}
- {task.column?.name} · {STATUS_LABEL[task.status]} + {currentCol?.name} · {STATUS_LABEL[task.status]}
@@ -175,6 +188,7 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o />
+ {/* 状态: 三段式, 当前项高亮, 点按即切换 */}
@@ -190,6 +204,31 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o
+ {/* 看板列: 点击换列, 状态随目标列联动 */} +
+ +
+ {currentCol?.name || '未分组'} + 更换 › +
+
+ + {/* 层级: 转为子任务 / 转为顶层 (T-ST-12) */} +
+ + {isSubtask ? ( +
changeParent(null)}> + 子任务 · 可转为顶层任务 + 转为顶层 +
+ ) : ( +
setParentPick(true)}> + 顶层任务 · 可挂到其他任务下 + 转为子任务 › +
+ )} +
+
setPicker('assignee')}> @@ -216,11 +255,6 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o
- setForm((p) => ({ ...p, extras: next }))} - /> -
子任务 {subs.length}
{subs.map((s) => (
toggleSub(s)}> @@ -238,14 +272,13 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o onChange={(e) => setNewSub(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') addSub() }} onFocus={(e) => { - // 键盘弹出动画 (~300ms) 后把输入框滚到可视区中部, 双保险防遮挡 setTimeout(() => e.target.scrollIntoView({ block: 'center', behavior: 'smooth' }), 300) }} />
{saving ? '保存中…' : '保存'}
-
拷贝到其他项目
+
setCopyOpen(true)}>拷贝到其他项目
删除任务{subs.length > 0 && (将一并删除 {subs.length} 条子任务)}
@@ -268,22 +301,33 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o onConfirm={(ids) => setForm((p) => ({ ...p, memberIds: Array.isArray(ids) ? ids : [] }))} /> + setCopyOpen(false)} + onDone={onChanged} + /> + + {/* 转为子任务: 从项目内顶层任务中选父任务 */} setCopies(null)} - bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)' }} + visible={parentPick} + onMaskClick={() => setParentPick(false)} + bodyStyle={{ + borderTopLeftRadius: 12, borderTopRightRadius: 12, + maxHeight: '70vh', overflowY: 'auto', + padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)', + }} > -
跨项目拷贝
-
源任务保持不变;新任务的创建人是你
- {(copies || []).length === 0 &&
没有其他可拷贝的项目
} - {(copies || []).map((p) => ( -
doCopy(p.id)}> -
{(p.name || '?').slice(0, 1)}
- {p.name} - 拷贝 +
转为子任务
+
选择父任务(仅支持一级;已有子任务的任务不能作为父任务)
+ {parentCandidates.length === 0 &&
没有可选的父任务
} + {parentCandidates.map((t) => ( +
{ setParentPick(false); changeParent(t.id) }}> + {t.title} + 选定
))} -
setCopies(null)}>取消
+
setParentPick(false)}>取消
) diff --git a/src/styles/globals.css b/src/styles/globals.css index 73521d5..34122b3 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1143,3 +1143,16 @@ textarea.inp { resize: vertical; } font-size: 11.5px; color: #999; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* ---- 通用分段控件 (状态选择等): 当前项高亮 ---- + 此前只有 .pv-head .seg 有定义, 详情弹层里裸用 .seg 会堆成三行 */ +.seg { display: flex; background: #f2f3f5; border-radius: 8px; padding: 3px; } +.seg div { flex: 1; text-align: center; font-size: 13px; padding: 6px 0; border-radius: 6px; color: var(--color-muted); cursor: pointer; } +.seg div.on { background: #fff; color: var(--color-text); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.06); } + +/* ---- 看板卡片 ⋯ 菜单入口 ---- */ +.bmore-btn { margin-left: auto; color: #c2c7cf; font-size: 14px; line-height: 1; padding: 2px 4px; cursor: pointer; } + +/* ---- ActionSheet 标题 (任务 ⋯ 菜单) ---- */ +.as-extra { padding: 4px 0 8px; } +.as-title { font-size: 13px; font-weight: 600; color: var(--color-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }