feat(任务): 详情页编号右置到副标题行、换列后自动收起、固定头尾滚动

- 完整任务编号从标题下方独立一行, 移到「看板列 · 状态」同一行右侧
  (左侧可省略号截断, 编号 flex-shrink:0 不被挤压)
- 切换看板列后 ActionSheet 未收起: 补 handler.close?.(), 且放在 onAction
  最前 (选中即收, 不等接口返回), 与项目视图 ⋯ 菜单同口径
- 滚动优化: 原整个弹层是一个滚动容器, 标题与保存按钮都会随内容滚走。
  改为三段式弹性布局 —— .td-head 固定 / .td-body 唯一滚动区 /
  .td-foot 固定 (保存按钮), 键盘弹出时底部随 kb 上抬

验证: 390x720 矮屏渲染真实 TaskDetail (14 条子任务撑出溢出), 11 项断言通过 ——
主体 scrollH 1531 > clientH 465; 滚动 1066px 到底后头部 top 86->86、
底部 top 657->657 均不变; 保存按钮 bottom 708 <= 720 始终在屏内;
头+体+底高度之和等于弹层总高 (无重叠/无空隙)。
This commit is contained in:
jif2.zhang
2026-10-27 15:49:06 +08:00
parent 0fc45aa6ec
commit 1d800c536e
2 changed files with 57 additions and 22 deletions
+23 -7
View File
@@ -128,13 +128,15 @@ export default function TaskDetail({ task, projectCode, columns, members, tasks,
* 这里始终按 String() 比对, 不要 Number(): 否则结果变 NaN, 落库列被改成 "NaN" 报「看板列不存在」 */ * 这里始终按 String() 比对, 不要 Number(): 否则结果变 NaN, 落库列被改成 "NaN" 报「看板列不存在」 */
function pickColumn() { function pickColumn() {
const current = String(task.columnId) const current = String(task.columnId)
ActionSheet.show({ const handler = ActionSheet.show({
actions: (columns || []).map((c) => ({ actions: (columns || []).map((c) => ({
text: c.name + (String(c.id) === current ? ' ✓' : ''), text: c.name + (String(c.id) === current ? ' ✓' : ''),
key: String(c.id), key: String(c.id),
})), })),
cancelText: '取消', cancelText: '取消',
onAction: async (item) => { onAction: async (item) => {
// 选中即收起 sheet (与项目视图 ⋯ 菜单同口径); 否则改完列 sheet 还挂在那
handler.close?.()
const columnId = String(item.key) const columnId = String(item.key)
if (columnId === current) return if (columnId === current) return
try { try {
@@ -200,13 +202,20 @@ export default function TaskDetail({ task, projectCode, columns, members, tasks,
borderTopLeftRadius: 12, borderTopLeftRadius: 12,
borderTopRightRadius: 12, borderTopRightRadius: 12,
maxHeight: `calc(88vh - ${kb}px)`, maxHeight: `calc(88vh - ${kb}px)`,
overflowY: 'auto', // 三段式: 固定头 / 滚动体 / 固定底 (标题与保存按钮始终可见)
padding: `16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px + ${kb}px)`, display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
padding: 0,
}} }}
> >
{/* 固定头部: 标题 + 看板列/状态 + 编号 */}
<div className="td-head">
<div className="sheet-title">{task.title}</div> <div className="sheet-title">{task.title}</div>
{/* 看板列 · 状态 与完整任务编号同一行, 编号靠右 (点按复制) */}
<div className="sheet-sub">
<span className="sub-meta">{currentCol?.name} · {STATUS_LABEL[task.status]}</span>
{fullId && ( {fullId && (
<div className="sheet-idrow">
<span <span
className="task-id-chip" className="task-id-chip"
role="button" role="button"
@@ -216,12 +225,12 @@ export default function TaskDetail({ task, projectCode, columns, members, tasks,
<span className="tid">{fullId}</span> <span className="tid">{fullId}</span>
<span className="tid-copy">{copied ? '已复制' : '复制'}</span> <span className="tid-copy">{copied ? '已复制' : '复制'}</span>
</span> </span>
</div>
)} )}
<div className="sheet-sub"> </div>
{currentCol?.name} · {STATUS_LABEL[task.status]}
</div> </div>
{/* 可滚动主体: 只有这一段滚动 */}
<div className="td-body">
<div className="field"> <div className="field">
<label>标题</label> <label>标题</label>
<input <input
@@ -386,8 +395,15 @@ export default function TaskDetail({ task, projectCode, columns, members, tasks,
}} }}
/> />
</div> </div>
</div>{/* /.td-body */}
{/* 固定底部: 保存 (键盘弹出时随 kb 上抬) */}
<div
className="td-foot"
style={{ paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + 12px + ${kb}px)` }}
>
<div className="btn-primary" onClick={save}>{saving ? '保存中…' : '保存'}</div> <div className="btn-primary" onClick={save}>{saving ? '保存中…' : '保存'}</div>
</div>
<UserPicker <UserPicker
visible={picker === 'assignee'} visible={picker === 'assignee'}
+22 -3
View File
@@ -1135,18 +1135,37 @@ body.me-safe-top .app-safe-top { height: 0; }
.cs-count { font-size: 11px; color: #999; margin-left: 8px; } .cs-count { font-size: 11px; color: #999; margin-left: 8px; }
/* ---- 任务详情 ---- */ /* ---- 任务详情 ---- */
/* 三段式弹层: 固定头 / 滚动体 / 固定底 —— 标题与保存按钮始终可见 */
.td-head {
flex: 0 0 auto; padding: 16px 16px 8px; background: #fff;
border-bottom: 1px solid var(--color-border);
}
.td-body {
flex: 1 1 auto; min-height: 0; overflow-y: auto;
padding: 12px 16px 4px;
-webkit-overflow-scrolling: touch; overscroll-behavior: contain;
}
.td-foot {
flex: 0 0 auto; padding: 10px 16px 12px; background: #fff;
border-top: 1px solid var(--color-border);
}
/* 底部固定后不再需要上外边距 */
.td-foot .btn-primary { margin-top: 0; }
.sheet-title { .sheet-title {
font-size: 16px; font-weight: 700; margin-bottom: 4px; word-break: break-word; font-size: 16px; font-weight: 700; margin-bottom: 4px; word-break: break-word;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
} }
/* 详情页副标题行: 左侧「看板列 · 状态」, 右侧完整任务编号 (点按复制) */
.sheet-sub { .sheet-sub {
display: flex; align-items: center; gap: 8px;
font-size: 11.5px; color: #999; margin-bottom: 14px; font-size: 11.5px; color: #999; margin-bottom: 14px;
}
.sheet-sub .sub-meta {
flex: 1 1 auto; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
} }
/* 详情页标题下的完整任务编号 (项目编号-序号): 点按复制 */
.sheet-idrow { margin: -1px 0 6px; }
.task-id-chip { .task-id-chip {
display: inline-flex; align-items: center; gap: 6px; display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
font-size: 11px; line-height: 1; font-weight: 600; font-size: 11px; line-height: 1; font-weight: 600;
color: #5b6b7c; background: #eef2f6; border-radius: 999px; padding: 5px 9px; color: #5b6b7c; background: #eef2f6; border-radius: 999px; padding: 5px 9px;
cursor: pointer; -webkit-user-select: none; user-select: none; cursor: pointer; -webkit-user-select: none; user-select: none;