feat(V0.2): 任务类型标签 —— 项目级自定义 + 任务标记 + 列表/看板展示
Docker Build and Push / build-image (push) Successful in 1m21s
Docker Build and Push / build-image (push) Successful in 1m21s
- 数据: Project.taskKinds (JSON, 未配置回落默认 需求/Bug/FEATURE/优化), Task.kind (≤20字); 迁移 20261025030000_task_kind - 服务: 项目 PUT 支持整体替换 taskKinds (去重/上限30/至少1个); 任务 create/update 支持 kind (空串清空) - 项目管理: 新增「任务类型」分区, chips 展示 + 弹层编辑 (删除/新增) - 任务详情: 新增「类型」字段, 项目类型 chips 点选, 可清空 - 展示: 列表行左下与看板卡左下显示彩色小标签 (需求蓝/Bug红/FEATURE紫/优化橙, 自定义类型灰)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import StatusDot from './StatusDot.jsx'
|
||||
import { kindColor } from '../../pages/task/kind.js'
|
||||
|
||||
// 看板卡片 (T-UI-02): 轻量化
|
||||
// - 标题 + 状态点 (T-ST-09)
|
||||
@@ -66,6 +67,11 @@ export default function TaskCard({
|
||||
</div>
|
||||
))}
|
||||
{rest > 0 && <div className="bmore">+{rest} 个子任务</div>}
|
||||
{task.kind && (
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<span className="kind-tag" style={{ background: kindColor(task.kind) }}>{task.kind}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import StatusDot from './StatusDot.jsx'
|
||||
import { kindColor } from '../../pages/task/kind.js'
|
||||
|
||||
const statusLabel = (s) => (s === 'done' ? '完成' : s === 'doing' ? '执行中' : '未开始')
|
||||
|
||||
@@ -48,9 +49,14 @@ export default function TaskRow({
|
||||
<>
|
||||
<div className="task-main">
|
||||
<div className="task-title">{task.title}</div>
|
||||
{task.members?.length > 0 && (
|
||||
{(task.kind || task.members?.length > 0) && (
|
||||
<div className="task-meta">
|
||||
<span className="members">+ {task.members.map((m) => m.name).join('、')}</span>
|
||||
{task.kind && (
|
||||
<span className="kind-tag" style={{ background: kindColor(task.kind) }}>{task.kind}</span>
|
||||
)}
|
||||
{task.members?.length > 0 && (
|
||||
<span className="members">+ {task.members.map((m) => m.name).join('、')}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import useKeyboardInset from '../../hooks/useKeyboardInset.js'
|
||||
import useOverlayBack from '../../hooks/useOverlayBack.js'
|
||||
import ColumnSettings from './ColumnSettings.jsx'
|
||||
import ProjectMembers from './ProjectMembers.jsx'
|
||||
import { DEFAULT_KINDS, kindColor } from './kind.js'
|
||||
|
||||
// 项目管理 (T-PJ-11/12): 成员 / 看板列 / 项目信息 三个分区的统一入口
|
||||
// onExited: 项目被删除/退出/归档后, 上层应离开该项目(回落个人默认项目)
|
||||
@@ -16,6 +17,10 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
|
||||
const [name, setName] = useState(project.name)
|
||||
// 信息块编辑 (T-PJ-13): index=-1 新增, >=0 编辑既有块
|
||||
const [noteEdit, setNoteEdit] = useState(null)
|
||||
// 任务类型编辑: 编辑中的类型列表 + 新增输入
|
||||
const [kindEdit, setKindEdit] = useState(false)
|
||||
const [kinds, setKinds] = useState([])
|
||||
const [newKind, setNewKind] = useState('')
|
||||
|
||||
const members = project.members || []
|
||||
const notes = Array.isArray(project.notes) ? project.notes : []
|
||||
@@ -123,8 +128,28 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
|
||||
}
|
||||
}
|
||||
|
||||
async function quit() {
|
||||
const ok = await Dialog.confirm({
|
||||
// 任务类型: 添加 / 删除 (至少保留一个)
|
||||
function addKind() {
|
||||
const k = newKind.trim().slice(0, 20)
|
||||
if (!k) return
|
||||
if (kinds.includes(k)) { Toast.show({ content: '该类型已存在' }); return }
|
||||
setKinds((p) => [...p, k])
|
||||
setNewKind('')
|
||||
}
|
||||
|
||||
async function saveKinds() {
|
||||
if (!kinds.length) { Toast.show({ content: '至少保留一个任务类型' }); return }
|
||||
try {
|
||||
await projectApi.update(project.id, { taskKinds: kinds })
|
||||
Toast.show({ content: '已保存' })
|
||||
setKindEdit(false)
|
||||
onChanged?.()
|
||||
} catch (e) {
|
||||
Toast.show({ content: e.message })
|
||||
}
|
||||
}
|
||||
|
||||
async function quit() { const ok = await Dialog.confirm({
|
||||
content: '退出该项目?退出后将不再可见。',
|
||||
confirmText: '退出',
|
||||
})
|
||||
@@ -167,6 +192,16 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
|
||||
<span className="link">列设置 ›</span>
|
||||
</div>
|
||||
|
||||
<div className="section-h" style={{ margin: '14px 14px 8px' }}>任务类型</div>
|
||||
<div className="set-row" style={{ padding: '12px 14px' }} onClick={() => { setKinds(project.taskKinds || DEFAULT_KINDS); setNewKind(''); setKindEdit(true) }}>
|
||||
<div className="kind-chip-row" style={{ flex: 1 }}>
|
||||
{(project.taskKinds || DEFAULT_KINDS).map((k) => (
|
||||
<span key={k} className="kind-tag" style={{ background: kindColor(k) }}>{k}</span>
|
||||
))}
|
||||
</div>
|
||||
<span className="link">编辑 ›</span>
|
||||
</div>
|
||||
|
||||
<div className="section-h" style={{ margin: '14px 14px 8px' }}>信息块({notes.length})</div>
|
||||
{notes.map((n, i) => (
|
||||
<div
|
||||
@@ -228,6 +263,48 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
|
||||
onClose={() => setRenaming(false)}
|
||||
/>
|
||||
|
||||
{/* 任务类型编辑: chips 删除 + 输入新增 */}
|
||||
<Popup
|
||||
visible={kindEdit}
|
||||
onMaskClick={() => setKindEdit(false)}
|
||||
bodyStyle={{
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
padding: `16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px + ${kb}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="sheet-title">任务类型</div>
|
||||
<div className="sheet-sub">任务的分类标签,显示在任务卡片左下角</div>
|
||||
|
||||
<div className="kind-chip-row" style={{ marginTop: 12 }}>
|
||||
{kinds.map((k) => (
|
||||
<span key={k} className="kind-chip" style={{ background: kindColor(k) }}>
|
||||
{k}
|
||||
<span
|
||||
className="x"
|
||||
onClick={() => setKinds((p) => p.filter((x) => x !== k))}
|
||||
>×</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="extra-row" style={{ display: 'flex', gap: 8, marginTop: 14 }}>
|
||||
<input
|
||||
className="inp"
|
||||
style={{ flex: 1 }}
|
||||
placeholder="新类型名称"
|
||||
value={newKind}
|
||||
maxLength={20}
|
||||
onChange={(e) => setNewKind(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') addKind() }}
|
||||
/>
|
||||
<span className="mini-btn solid" style={{ padding: '0 16px', display: 'inline-flex', alignItems: 'center' }} onClick={addKind}>添加</span>
|
||||
</div>
|
||||
|
||||
<div className="btn-primary" onClick={saveKinds}>保存</div>
|
||||
<div className="btn-ghost" onClick={() => setKindEdit(false)}>取消</div>
|
||||
</Popup>
|
||||
|
||||
{/* 信息块编辑 (T-PJ-13): 标题 + 长文本, 独立编辑 */}
|
||||
<Popup
|
||||
visible={!!noteEdit}
|
||||
|
||||
@@ -271,6 +271,7 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit
|
||||
columns={columns}
|
||||
members={project.members || []}
|
||||
tasks={tasks || []}
|
||||
taskKinds={project.taskKinds}
|
||||
onClose={() => setOpenTask(null)}
|
||||
onSaved={() => { setOpenTask(null); load() }}
|
||||
onChanged={load}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { ActionSheet, Popup, Toast } from 'antd-mobile'
|
||||
import { taskApi } from '../../api/task.js'
|
||||
import { STATUS_OPTIONS, STATUS_LABEL } from './date.js'
|
||||
import { kindColor } from './kind.js'
|
||||
import UserPicker from '../../components/task/UserPicker.jsx'
|
||||
import StatusDot from '../../components/task/StatusDot.jsx'
|
||||
import useKeyboardInset from '../../hooks/useKeyboardInset.js'
|
||||
@@ -11,13 +12,14 @@ import useOverlayBack from '../../hooks/useOverlayBack.js'
|
||||
// - 状态: 三段式分段控件, 当前项高亮, 点按即切换
|
||||
// - 看板列: ActionSheet 选择, 列/状态联动由服务端裁决 (T-BD-*)
|
||||
// - 转为子任务 / 转为顶层任务 (T-ST-12): 从项目内顶层任务中选父任务
|
||||
export default function TaskDetail({ task, columns, members, tasks, onClose, onSaved, onChanged }) {
|
||||
export default function TaskDetail({ task, columns, members, tasks, taskKinds, onClose, onSaved, onChanged }) {
|
||||
const [form, setForm] = useState(() => ({
|
||||
title: task.title || '',
|
||||
description: task.description || '',
|
||||
memberIds: (task.members || []).map((m) => m.userId),
|
||||
assigneeId: task.assignee?.id ?? null,
|
||||
status: task.status,
|
||||
kind: task.kind || null,
|
||||
tags: task.tags || [],
|
||||
}))
|
||||
const [subs, setSubs] = useState(task.children || [])
|
||||
@@ -47,6 +49,7 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
|
||||
memberIds: (task.members || []).map((m) => m.userId),
|
||||
assigneeId: task.assignee?.id ?? null,
|
||||
status: task.status,
|
||||
kind: task.kind || null,
|
||||
tags: task.tags || [],
|
||||
})
|
||||
setSubs(task.children || [])
|
||||
@@ -62,6 +65,7 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
|
||||
assigneeId: form.assigneeId,
|
||||
memberIds: form.memberIds,
|
||||
status: form.status,
|
||||
kind: form.kind,
|
||||
tags: form.tags,
|
||||
})
|
||||
Toast.show({ content: '已保存' })
|
||||
@@ -184,6 +188,38 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 类型: 项目任务类型标签, 可清空 */}
|
||||
{(taskKinds || []).length > 0 && (
|
||||
<div className="field">
|
||||
<label>类型</label>
|
||||
<div className="kind-chip-row" style={{ flexWrap: 'wrap' }}>
|
||||
<span
|
||||
className="kind-tag"
|
||||
style={{
|
||||
background: form.kind ? 'transparent' : 'var(--color-primary)',
|
||||
color: form.kind ? '#999' : '#fff',
|
||||
border: form.kind ? '1px dashed #ccc' : 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setForm((p) => ({ ...p, kind: null }))}
|
||||
>无</span>
|
||||
{taskKinds.map((k) => (
|
||||
<span
|
||||
key={k}
|
||||
className="kind-tag"
|
||||
style={{
|
||||
background: kindColor(k),
|
||||
opacity: form.kind === k ? 1 : 0.45,
|
||||
cursor: 'pointer',
|
||||
outline: form.kind === k ? '2px solid rgba(22,119,255,.4)' : 'none',
|
||||
}}
|
||||
onClick={() => setForm((p) => ({ ...p, kind: p.kind === k ? null : k }))}
|
||||
>{k}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 看板列: 点击换列, 状态随目标列联动 */}
|
||||
<div className="field">
|
||||
<label>看板列</label>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 任务类型标签 (项目级自定义, 默认四类)
|
||||
const KIND_COLORS = {
|
||||
需求: '#1677ff',
|
||||
Bug: '#ff3141',
|
||||
bug: '#ff3141',
|
||||
FEATURE: '#a855f7',
|
||||
优化: '#ff8f1f',
|
||||
}
|
||||
|
||||
export const DEFAULT_KINDS = ['需求', 'Bug', 'FEATURE', '优化']
|
||||
|
||||
export const kindColor = (k) => KIND_COLORS[k] || '#8a919c'
|
||||
@@ -699,6 +699,20 @@ body.me-safe-top .app-safe-top { height: 0; }
|
||||
.mv-card.dragover.after { border-bottom: 3px solid var(--color-primary); }
|
||||
.mv-card.dragover.before { border-top: 3px solid var(--color-primary); }
|
||||
|
||||
/* 任务类型小标签: 列表行左下 / 看板卡左下 */
|
||||
.kind-tag {
|
||||
display: inline-flex; align-items: center;
|
||||
font-size: 10px; line-height: 1; font-weight: 600; color: #fff;
|
||||
border-radius: 4px; padding: 3px 5px; flex-shrink: 0; white-space: nowrap;
|
||||
}
|
||||
/* 项目管理: 任务类型编辑 chips */
|
||||
.kind-chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.kind-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-size: 12.5px; color: #fff; border-radius: 999px; padding: 5px 6px 5px 11px;
|
||||
}
|
||||
.kind-chip .x { font-size: 13px; color: rgba(255,255,255,.75); cursor: pointer; padding: 0 4px; }
|
||||
|
||||
/* 拖动跟手浮层: 克隆原卡片淡化后挂在 body 顶层, 跟随指针 */
|
||||
.drag-ghost {
|
||||
position: fixed; left: 0; top: 0; z-index: 1000;
|
||||
|
||||
Reference in New Issue
Block a user