From 06e9a3e6943c3b462206272e9a02fdaf2abcb142 Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Tue, 27 Oct 2026 10:06:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BB=BB=E5=8A=A1):=20=E7=BC=96=E5=8F=B7?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20#=E5=BA=8F=E5=8F=B7=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E3=80=81=E8=AF=A6=E6=83=85=E9=A1=B5=E5=AE=8C=E6=95=B4=E7=BC=96?= =?UTF-8?q?=E5=8F=B7=E5=8F=AF=E5=A4=8D=E5=88=B6=E3=80=81=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=94=B9=E5=88=97=E6=8A=A5=E3=80=8C=E7=9C=8B?= =?UTF-8?q?=E6=9D=BF=E5=88=97=E4=B8=8D=E5=AD=98=E5=9C=A8=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 列表/看板任务编号徽标改为 `#序号`(如 #18), 移除不再需要的 codePrefix 透传 - 详情页标题下新增完整任务编号胶囊 (项目编号-序号, 如 ADMIN-18), 点按复制: Clipboard API 优先, 非安全上下文/旧 WebView 回退 execCommand, 并提示已复制 - 修复详情页「看板列」切换报错: 0.2.1 本地库列 id 为 uuid 字符串, ActionSheet 的 key 恒为字符串, 旧代码 Number(key) 得到 NaN -> update 校验失败抛「看板列不存在」。改为字符串比对 (currentCol/勾选标记同步) - 详情页保存后先就地替换该行再回源刷新 --- src/components/task/TaskCard.jsx | 9 ++-- src/components/task/TaskRow.jsx | 9 ++-- src/pages/task/BoardView.jsx | 3 +- src/pages/task/ListView.jsx | 3 +- src/pages/task/ProjectView.jsx | 12 +++-- src/pages/task/TaskDetail.jsx | 75 ++++++++++++++++++++++++++++---- src/pages/task/date.js | 17 ++++++++ src/styles/globals.css | 15 +++++++ 8 files changed, 119 insertions(+), 24 deletions(-) diff --git a/src/components/task/TaskCard.jsx b/src/components/task/TaskCard.jsx index 9f3a781..a609c81 100644 --- a/src/components/task/TaskCard.jsx +++ b/src/components/task/TaskCard.jsx @@ -1,6 +1,7 @@ import StatusDot from './StatusDot.jsx' import Avatar from './Avatar.jsx' import { kindColor } from '../../pages/task/kind.js' +import { taskShortId } from '../../pages/task/date.js' // 看板卡片 (T-UI-02): 轻量化 // - 标题 + 状态点 (T-ST-09) @@ -9,11 +10,13 @@ import { kindColor } from '../../pages/task/kind.js' const MAX_SUB = 3 export default function TaskCard({ - task, codePrefix, bindDrag, onClick, onToggleSub, onToggle, onMore, 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) const rest = children.length - shown.length + // 编号只显示 `#序号`; 完整编号 (项目编号-序号) 放详情页 (见 TaskDetail) + const shortId = taskShortId(task) return (
- {codePrefix && task.number != null && ( - {codePrefix}-{task.number} - )} + {shortId && {shortId}} {task.markDate && ( {task.markDate.slice(5, 10).replace('-', '/')} )} diff --git a/src/components/task/TaskRow.jsx b/src/components/task/TaskRow.jsx index 9bddf99..53880db 100644 --- a/src/components/task/TaskRow.jsx +++ b/src/components/task/TaskRow.jsx @@ -1,6 +1,7 @@ import StatusDot from './StatusDot.jsx' import Avatar from './Avatar.jsx' import { kindColor } from '../../pages/task/kind.js' +import { taskShortId } from '../../pages/task/date.js' const statusLabel = (s) => (s === 'done' ? '完成' : s === 'doing' ? '执行中' : '未开始') @@ -10,9 +11,11 @@ const statusLabel = (s) => (s === 'done' ? '完成' : s === 'doing' ? '执行中 // 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动, // 避免长按行后抬手时合成的 click 落在控件上误触状态切换 export default function TaskRow({ - task, codePrefix, onToggle, onClick, onMore, bindDrag, bindDragHandle, dragging = false, dragOver, indent = false, children, + task, onToggle, onClick, onMore, bindDrag, bindDragHandle, dragging = false, dragOver, indent = false, children, }) { const done = task.status === 'done' + // 编号只显示 `#序号`; 完整编号 (项目编号-序号) 放详情页 (见 TaskDetail) + const shortId = indent ? null : taskShortId(task) return ( <>
{task.title}
{/* 无类型/成员也保留该行, 与有标签的行等高 */}
- {codePrefix && task.number != null && ( - {codePrefix}-{task.number} - )} + {shortId && {shortId}} {task.markDate && ( {task.markDate.slice(5, 10).replace('-', '/')} )} diff --git a/src/pages/task/BoardView.jsx b/src/pages/task/BoardView.jsx index 8cc932d..a434c7b 100644 --- a/src/pages/task/BoardView.jsx +++ b/src/pages/task/BoardView.jsx @@ -36,7 +36,7 @@ function ScrollBody({ children }) { // 横向滚列; 列内独立纵向滚动, 列头固定, 滚动时右缘显示滚动条 // 不做"拖到卡片上转子任务" —— 统一走详情页「转为子任务」 export default function BoardView({ - tasks, columns, codePrefix, onMoveColumn, onOpen, onToggleSub, onToggle, onMore, onQuickAdd, + tasks, columns, onMoveColumn, onOpen, onToggleSub, onToggle, onMore, onQuickAdd, }) { const drag = useColumnDrag({ onDrop: onMoveColumn }) @@ -69,7 +69,6 @@ export default function BoardView({ { if (!drag.dragTask && !drag.justEnded()) onOpen?.(t) }} diff --git a/src/pages/task/ListView.jsx b/src/pages/task/ListView.jsx index c93600c..326abff 100644 --- a/src/pages/task/ListView.jsx +++ b/src/pages/task/ListView.jsx @@ -7,7 +7,7 @@ import useRowDrag from '../../components/task/useRowDrag.js' // 换列: 行/把手拖动, 跟手浮层, 手指划到哪个分组松手就落到哪 (与看板一致) // (列表只展示有任务的列, 空列需到看板视图拖入) export default function ListView({ - tasks, columns, codePrefix, onToggle, onOpen, onMore, onMoveColumn, onReorder, + tasks, columns, onToggle, onOpen, onMore, onMoveColumn, onReorder, }) { const [collapsed, setCollapsed] = useState({}) const drag = useRowDrag({ columns, onDrop: onMoveColumn, onReorder }) @@ -49,7 +49,6 @@ export default function ListView({
{ if (!drag.justEnded()) onOpen(t) }} onMore={onMore} diff --git a/src/pages/task/ProjectView.jsx b/src/pages/task/ProjectView.jsx index 6f13b6b..c9c995d 100644 --- a/src/pages/task/ProjectView.jsx +++ b/src/pages/task/ProjectView.jsx @@ -71,6 +71,13 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit const replaceTask = (fresh) => setTasks((prev) => (prev || []).map((t) => (t.id === fresh.id ? fresh : t))) + /** 详情页保存后: 先就地替换该行, 再回源刷新 (不回源也能立刻看到编号/列变更) */ + const handleSaved = (fresh) => { + if (fresh?.id) replaceTask(fresh) + setOpenTask(null) + load() + } + /** 更新任务(列/状态/字段), 服务端裁决后以返回值为准 */ async function updateTask(id, patch) { const snapshot = tasks @@ -276,7 +283,6 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit setOpenTask(t)} onMore={openTaskMenu} @@ -287,7 +293,6 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit setOpenTask(t)} onToggleSub={(c) => toggleDone(c)} @@ -301,12 +306,13 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit {openTask && ( setOpenTask(null)} - onSaved={() => { setOpenTask(null); load() }} + onSaved={handleSaved} onChanged={load} /> )} diff --git a/src/pages/task/TaskDetail.jsx b/src/pages/task/TaskDetail.jsx index fe2df7f..208a2ce 100644 --- a/src/pages/task/TaskDetail.jsx +++ b/src/pages/task/TaskDetail.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { ActionSheet, Popup, Toast, DatePicker } from 'antd-mobile' import { taskApi } from '../../api/task.js' -import { STATUS_OPTIONS, STATUS_LABEL } from './date.js' +import { STATUS_OPTIONS, STATUS_LABEL, taskFullId } from './date.js' import { kindColor } from './kind.js' import { parseTimePrefix, splitTitle } from './quickAdd.js' import UserPicker from '../../components/task/UserPicker.jsx' @@ -9,11 +9,35 @@ import StatusDot from '../../components/task/StatusDot.jsx' import useKeyboardInset from '../../hooks/useKeyboardInset.js' import useOverlayBack from '../../hooks/useOverlayBack.js' +// 复制文本: 优先 Clipboard API (需安全上下文), 打包端 http 明文 / 旧 WebView 回落 execCommand +async function copyText(text) { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text) + return true + } + } catch { /* 继续走兜底 */ } + try { + const ta = document.createElement('textarea') + ta.value = text + ta.setAttribute('readonly', '') + ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none' + document.body.appendChild(ta) + ta.select() + ta.setSelectionRange(0, text.length) + const ok = document.execCommand('copy') + document.body.removeChild(ta) + return ok + } catch { + return false + } +} + // 任务详情 (T-TK-*, T-ST-03, T-AS-05) // - 状态: 三段式分段控件, 当前项高亮, 点按即切换 // - 看板列: ActionSheet 选择, 列/状态联动由服务端裁决 (T-BD-*) // - 转为子任务 / 转为顶层任务 (T-ST-12): 从项目内顶层任务中选父任务 -export default function TaskDetail({ task, columns, members, tasks, taskKinds, onClose, onSaved, onChanged }) { +export default function TaskDetail({ task, projectCode, columns, members, tasks, taskKinds, onClose, onSaved, onChanged }) { const [form, setForm] = useState(() => ({ title: task.title || '', description: task.description || '', @@ -38,8 +62,25 @@ export default function TaskDetail({ task, columns, members, tasks, taskKinds, o 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 currentCol = (columns || []).find((c) => String(c.id) === String(task.columnId)) const isSubtask = task.parentId != null + // 完整任务编号 (项目编号-序号), 点按复制; 未定号 (离线新建未上行) 不展示 + const fullId = taskFullId(task, projectCode) + const [copied, setCopied] = useState(false) + useEffect(() => { setCopied(false) }, [task.id]) + useEffect(() => { + if (!copied) return undefined + const t = setTimeout(() => setCopied(false), 1500) + return () => clearTimeout(t) + }, [copied]) + + async function copyId(e) { + e.stopPropagation() + const ok = await copyText(fullId) + setCopied(ok) + Toast.show({ content: ok ? `已复制 ${fullId}` : '复制失败,请长按手动复制' }) + } + // 可作为父任务的候选: 同项目顶层任务, 排除自己与已有子任务的任务 (T-ST-01/13) const parentCandidates = (tasks || []).filter( (t) => t.id !== task.id && (t.children || []).length === 0, @@ -82,23 +123,26 @@ export default function TaskDetail({ task, columns, members, tasks, taskKinds, o } } - /** 换列: 服务端按目标列绑定裁决状态 (T-BD-04/07) */ + /** 换列: 服务端按目标列绑定裁决状态 (T-BD-04/07) + * 本地库的列 id 为 uuid 字符串, 而 ActionSheet 的 key 会被转成字符串 —— + * 这里始终按 String() 比对, 不要 Number(): 否则结果变 NaN, 落库列被改成 "NaN" 报「看板列不存在」 */ function pickColumn() { + const current = String(task.columnId) ActionSheet.show({ actions: (columns || []).map((c) => ({ - text: c.name + (c.id === task.columnId ? ' ✓' : ''), + text: c.name + (String(c.id) === current ? ' ✓' : ''), key: String(c.id), })), cancelText: '取消', onAction: async (item) => { - const columnId = Number(item.key) - if (columnId === task.columnId) return + const columnId = String(item.key) + if (columnId === current) return try { const r = await taskApi.update(task.id, { columnId }) - Toast.show({ content: `已移入 ${(columns || []).find((c) => c.id === columnId)?.name || '目标列'}` }) + Toast.show({ content: `已移入 ${(columns || []).find((c) => String(c.id) === columnId)?.name || '目标列'}` }) onSaved?.(r.item) } catch (e) { - Toast.show({ content: e.message }) + Toast.show({ content: e.message || '换列失败' }) } }, }) @@ -161,6 +205,19 @@ export default function TaskDetail({ task, columns, members, tasks, taskKinds, o }} >
{task.title}
+ {fullId && ( +
+ + {fullId} + {copied ? '已复制' : '复制'} + +
+ )}
{currentCol?.name} · {STATUS_LABEL[task.status]}
diff --git a/src/pages/task/date.js b/src/pages/task/date.js index 19c5f60..fad7b9f 100644 --- a/src/pages/task/date.js +++ b/src/pages/task/date.js @@ -16,6 +16,23 @@ export const RETENTION_OPTIONS = ['never', 'today', '3d', 'week', 'month', 'fore export const STATUS_LABEL = { todo: '未开始', doing: '执行中', done: '完成' } export const STATUS_OPTIONS = ['todo', 'doing', 'done'] +// 任务编号 (T-TK-编号): 服务端分配 `Project.code` + 项目内自增 `Task.number` +// - 列表 / 看板: 只显示 `#序号` (短, 不占卡片宽度) +// - 详情页: 显示完整编号 `项目编号-序号`, 点按复制 +// - 离线新建、上行定号前的任务 number 为空 -> 两处都不出编号 (不做假编号) +/** 列表 / 看板的短编号: '#12'; 未定号返回 null */ +export function taskShortId(task) { + const n = task?.number + return n === null || n === undefined || n === '' ? null : `#${n}` +} + +/** 完整任务编号: 'IB-12'; 未定号返回 null */ +export function taskFullId(task, code) { + const n = task?.number + if (n === null || n === undefined || n === '') return null + return code ? `${code}-${n}` : `#${n}` +} + const FMT = new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit', }) diff --git a/src/styles/globals.css b/src/styles/globals.css index e2a4f12..7adf449 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1207,6 +1207,21 @@ body.me-safe-top .app-safe-top { height: 0; } font-size: 11.5px; color: #999; margin-bottom: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* 详情页标题下的完整任务编号 (项目编号-序号): 点按复制 */ +.sheet-idrow { margin: -1px 0 6px; } +.task-id-chip { + display: inline-flex; align-items: center; gap: 6px; + font-size: 11px; line-height: 1; font-weight: 600; + color: #5b6b7c; background: #eef2f6; border-radius: 999px; padding: 5px 9px; + cursor: pointer; -webkit-user-select: none; user-select: none; + font-variant-numeric: tabular-nums; +} +.task-id-chip:active { background: #e2e8ef; } +.task-id-chip .tid { letter-spacing: .3px; } +.task-id-chip .tid-copy { + color: var(--color-primary); font-weight: 500; + padding-left: 6px; border-left: 1px solid rgba(91,107,124,.22); +} .field { margin-bottom: 13px; } .field label { display: block; font-size: 12px; color: var(--color-muted); margin-bottom: 6px; } .inp {