refactor(V0.2): 详情页精简 + 列表行样式调整
Docker Build and Push / build-image (push) Successful in 4m20s

- 任务详情: 移除底部「拷贝到其他项目 / 删除任务」按钮 (⋯ 菜单仍保留入口), 清理相关死代码
- 列表子任务: 改为与普通任务同构的两行布局 (标题 + 左侧状态点/状态文字, 字体略小)
- 列表任务卡片第二行不再显示看板分组标签
This commit is contained in:
jif2.zhang
2026-10-25 00:23:01 +08:00
parent 9467d4c242
commit 20a5105c23
3 changed files with 17 additions and 42 deletions
+10 -5
View File
@@ -1,7 +1,10 @@
import StatusDot from './StatusDot.jsx' import StatusDot from './StatusDot.jsx'
const statusLabel = (s) => (s === 'done' ? '完成' : s === 'doing' ? '执行中' : '未开始')
// 列表视图的一行 (Todoist 风格, T-UI-01) // 列表视图的一行 (Todoist 风格, T-UI-01)
// 点击行体 → 打开详情; 长按 400ms 后左右拖动换列 (bindDrag 来自 useRowDrag) // 点击行体 → 打开详情; 长按 400ms 后左右拖动换列 (bindDrag 来自 useRowDrag)
// 子任务与普通任务同构: 标题一行 + 状态元信息一行 (字体略小), 不显示分组标签
// 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动, // 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动,
// 避免长按行后抬手时合成的 click 落在控件上误触状态切换 // 避免长按行后抬手时合成的 click 落在控件上误触状态切换
export default function TaskRow({ export default function TaskRow({
@@ -27,18 +30,20 @@ export default function TaskRow({
</div> </div>
{indent ? ( {indent ? (
<> <div className="sub-main">
<span className="sub-title">{task.title}</span> <span className="sub-title">{task.title}</span>
<StatusDot status={task.status} pill /> <div className="task-meta sub">
</> <StatusDot status={task.status} />
<span>{statusLabel(task.status)}</span>
</div>
</div>
) : ( ) : (
<> <>
<div className="task-main"> <div className="task-main">
<div className="task-title">{task.title}</div> <div className="task-title">{task.title}</div>
<div className="task-meta"> <div className="task-meta">
<StatusDot status={task.status} /> <StatusDot status={task.status} />
<span>{task.status === 'done' ? '完成' : task.status === 'doing' ? '执行中' : '未开始'}</span> <span>{statusLabel(task.status)}</span>
{task.column?.name && <span className="col">{task.column.name}</span>}
<span className={`assignee${task.assignee ? '' : ' none'}`}> <span className={`assignee${task.assignee ? '' : ' none'}`}>
{task.assignee?.name || '未指派'} {task.assignee?.name || '未指派'}
</span> </span>
+2 -33
View File
@@ -1,10 +1,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ActionSheet, Dialog, Popup, Toast } from 'antd-mobile' import { ActionSheet, Popup, Toast } from 'antd-mobile'
import { taskApi } from '../../api/task.js' import { taskApi } from '../../api/task.js'
import { STATUS_OPTIONS, STATUS_LABEL } from './date.js' import { STATUS_OPTIONS, STATUS_LABEL } from './date.js'
import UserPicker from '../../components/task/UserPicker.jsx' import UserPicker from '../../components/task/UserPicker.jsx'
import StatusDot from '../../components/task/StatusDot.jsx' import StatusDot from '../../components/task/StatusDot.jsx'
import CopyTaskPopup from '../../components/task/CopyTaskPopup.jsx'
import useKeyboardInset from '../../hooks/useKeyboardInset.js' import useKeyboardInset from '../../hooks/useKeyboardInset.js'
import useOverlayBack from '../../hooks/useOverlayBack.js' import useOverlayBack from '../../hooks/useOverlayBack.js'
@@ -25,7 +24,6 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
const [newSub, setNewSub] = useState('') const [newSub, setNewSub] = useState('')
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [picker, setPicker] = useState(null) // 'assignee' | 'members' const [picker, setPicker] = useState(null) // 'assignee' | 'members'
const [copyOpen, setCopyOpen] = useState(false)
const [parentPick, setParentPick] = useState(false) // 转为子任务: 选择父任务 const [parentPick, setParentPick] = useState(false) // 转为子任务: 选择父任务
// 键盘弹出插入量: 抬升弹层内容, 输入框不被遮挡 // 键盘弹出插入量: 抬升弹层内容, 输入框不被遮挡
@@ -132,25 +130,7 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
} }
} }
async function removeTask() { return ( <Popup
const ok = await Dialog.confirm({
content: subs.length
? `删除该任务?将一并删除 ${subs.length} 条子任务。`
: '删除该任务?',
confirmText: '删除',
})
if (!ok) return
try {
await taskApi.remove(task.id)
Toast.show({ content: '已删除' })
onSaved?.()
} catch (e) {
Toast.show({ content: e.message })
}
}
return (
<Popup
visible visible
onMaskClick={onClose} onMaskClick={onClose}
position="bottom" position="bottom"
@@ -278,10 +258,6 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
</div> </div>
<div className="btn-primary" onClick={save}>{saving ? '保存中…' : '保存'}</div> <div className="btn-primary" onClick={save}>{saving ? '保存中…' : '保存'}</div>
<div className="btn-ghost" onClick={() => setCopyOpen(true)}>拷贝到其他项目</div>
<div className="btn-danger" onClick={removeTask}>
删除任务{subs.length > 0 && <span className="del-hint">(将一并删除 {subs.length} 条子任务)</span>}
</div>
<UserPicker <UserPicker
visible={picker === 'assignee'} visible={picker === 'assignee'}
@@ -301,13 +277,6 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
onConfirm={(ids) => setForm((p) => ({ ...p, memberIds: Array.isArray(ids) ? ids : [] }))} onConfirm={(ids) => setForm((p) => ({ ...p, memberIds: Array.isArray(ids) ? ids : [] }))}
/> />
<CopyTaskPopup
task={task}
visible={copyOpen}
onClose={() => setCopyOpen(false)}
onDone={onChanged}
/>
{/* 转为子任务: 从项目内顶层任务中选父任务 */} {/* 转为子任务: 从项目内顶层任务中选父任务 */}
<Popup <Popup
visible={parentPick} visible={parentPick}
+5 -4
View File
@@ -963,20 +963,21 @@ body.me-safe-top .app-safe-top { height: 0; }
.task-title { font-size: 14px; display: flex; align-items: center; gap: 6px; word-break: break-word; } .task-title { font-size: 14px; display: flex; align-items: center; gap: 6px; word-break: break-word; }
.task-row.done .task-title { color: #a0a0a0; text-decoration: line-through; } .task-row.done .task-title { color: #a0a0a0; text-decoration: line-through; }
.task-meta { display: flex; align-items: center; gap: 7px; margin-top: 4px; font-size: 11px; color: #999; flex-wrap: wrap; } .task-meta { display: flex; align-items: center; gap: 7px; margin-top: 4px; font-size: 11px; color: #999; flex-wrap: wrap; }
.task-meta .col { color: var(--color-primary); background: #f0f5ff; border-radius: 4px; padding: 0 5px; }
.task-meta .assignee { font-size: 12px; font-weight: 600; color: var(--color-text); } .task-meta .assignee { font-size: 12px; font-weight: 600; color: var(--color-text); }
.task-meta .assignee.none { color: #b8bcc4; font-weight: 400; } .task-meta .assignee.none { color: #b8bcc4; font-weight: 400; }
.task-meta .members { font-size: 10.5px; color: #a7adb6; } .task-meta .members { font-size: 10.5px; color: #a7adb6; }
.task-right { display: flex; align-items: center; flex-shrink: 0; } .task-right { display: flex; align-items: center; flex-shrink: 0; }
.more { color: #c2c7cf; font-size: 16px; line-height: 1; padding: 0 2px; cursor: pointer; } .more { color: #c2c7cf; font-size: 16px; line-height: 1; padding: 0 2px; cursor: pointer; }
.sub-row { .sub-row {
display: flex; align-items: center; gap: 8px; background: #fff; border-radius: 8px; display: flex; align-items: flex-start; gap: 8px; background: #fff; border-radius: 8px;
padding: 8px 12px 8px 30px; margin: -2px 12px 7px 26px; box-shadow: 0 1px 2px rgba(0,0,0,.03); padding: 8px 12px 8px 30px; margin: -2px 12px 7px 26px; box-shadow: 0 1px 2px rgba(0,0,0,.03);
position: relative; cursor: pointer; position: relative; cursor: pointer;
} }
.sub-row::before { content: ""; position: absolute; left: 14px; top: 0; bottom: 0; width: 1px; background: #e9ecf0; } .sub-row::before { content: ""; position: absolute; left: 14px; top: 0; bottom: 0; width: 1px; background: #e9ecf0; }
.sub-row .box { width: 16px; height: 16px; } .sub-row .box { width: 16px; height: 16px; margin-top: 2px; }
.sub-title { flex: 1; font-size: 13px; color: #444; } .sub-main { flex: 1; min-width: 0; }
.sub-title { display: block; font-size: 13px; color: #444; }
.sub-row .task-meta { font-size: 10.5px; margin-top: 2px; gap: 5px; }
.sub-row.done .sub-title { color: #a8a8a8; text-decoration: line-through; } .sub-row.done .sub-title { color: #a8a8a8; text-decoration: line-through; }
.collapse { font-size: 11px; color: var(--color-primary); padding: 2px 0 8px 30px; margin: 0 12px; cursor: pointer; } .collapse { font-size: 11px; color: var(--color-primary); padding: 2px 0 8px 30px; margin: 0 12px; cursor: pointer; }
.sub-add-hint { font-size: 11px; color: #b8bcc4; padding: 0 0 8px 44px; cursor: pointer; } .sub-add-hint { font-size: 11px; color: #b8bcc4; padding: 0 0 8px 44px; cursor: pointer; }