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: (
+
+ ),
+ 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; }