feat(V0.2): 任务模块 —— 项目 / 列表与看板 / 列-状态绑定 / 一级子任务
Docker Build and Push / build-image (push) Failing after 3m51s
Docker Build and Push / build-image (push) Failing after 3m51s
需求 142 条 (T-*), 设计见 docs/V0.2/任务模块/ 数据层: - 新增 6 张表: Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag - 看板列是实体(可增删改排序), Task.columnId 外键 RESTRICT 防误删列丢任务 - Task.status(todo/doing/done) 与 columnId 双落库, 二者正交 领域层 server/task-domain/ (唯一口径, 前后端不重复实现): - resolveStatus: 列决定状态(只看目标列, 无拖出回退); 状态与列不匹配时按列序取第一个绑定该状态的列自动归位, 无匹配列则不移动; 子任务完全豁免 - retention: 逐列 6 档保留窗口(永不显示/当天/3天/本周/本月/永久显示), 以 doneAt 判定 - copyTask: 跨项目深拷贝, 人员按目标项目成员过滤并回报丢弃名单 接口: /api/projects, /api/columns, /api/tasks (均校验项目成员) 前端: - 底部 Tab 插入「任务」, 位于「打卡」之后; 默认首页仍为打卡 - 列表视图(子任务缩进+折叠) / 看板视图(长按拖拽换列、拖到卡片降为子任务) - 列设置(列管理+状态绑定+保留选项) / 任务详情(扩展信息卡片默认收起) / 成员管理 - 视图记忆与上次项目存设备端 已验证: 95 文件语法 0 错误、相对导入 0 缺失、领域逻辑用例全部通过
This commit is contained in:
@@ -883,3 +883,216 @@ body.me-safe-top .app-safe-top { height: 0; }
|
||||
.mv-sheet-row.danger { color: var(--color-danger); }
|
||||
.mv-sheet-row.disabled { opacity: .45; cursor: default; }
|
||||
.mv-sheet-row .k { font-size: 11px; color: #b8bcc4; }
|
||||
|
||||
/* ================== 任务模块 (V0.2, 需求 T-*) ================== */
|
||||
/* 项目列表 / 列表视图 / 看板视图 / 列设置 / 任务详情 / 成员 */
|
||||
.task-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow-y: auto; }
|
||||
.task-content.is-board { overflow: hidden; }
|
||||
|
||||
/* ---- 项目列表 ---- */
|
||||
.proj-head { display: flex; align-items: center; padding: 12px 14px 8px; }
|
||||
.proj-head b { font-size: 16px; }
|
||||
.proj-head .right { margin-left: auto; display: flex; gap: 6px; }
|
||||
.proj-group { font-size: 11px; color: #b8bcc4; letter-spacing: .5px; padding: 10px 16px 6px; }
|
||||
.proj-item {
|
||||
display: flex; align-items: center; gap: 12px; background: #fff; border-radius: 10px;
|
||||
padding: 13px 14px; margin: 0 12px 8px; box-shadow: 0 1px 2px rgba(0,0,0,.04); cursor: pointer;
|
||||
}
|
||||
.proj-item .pico {
|
||||
width: 34px; height: 34px; border-radius: 9px; display: flex; align-items: center;
|
||||
justify-content: center; color: #fff; font-size: 14px; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.proj-item .pmain { flex: 1; min-width: 0; }
|
||||
.proj-item .ptitle { font-size: 14.5px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
||||
.proj-item .psub { font-size: 11.5px; color: var(--color-muted); margin-top: 2px; }
|
||||
.proj-item .pcount { font-size: 15px; color: #c0c4cc; flex-shrink: 0; }
|
||||
.badge { font-size: 10px; color: #ad6800; background: #fff7e6; border-radius: 4px; padding: 0 5px; font-weight: 400; }
|
||||
|
||||
/* ---- 视图头部 / 快速新增 ---- */
|
||||
.pv-head { padding: 10px 14px 8px; display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
||||
.pv-head .seg { display: flex; background: #f2f3f5; border-radius: 8px; padding: 3px; width: 112px; }
|
||||
.pv-head .seg div { flex: 1; text-align: center; font-size: 13px; padding: 5px 0; border-radius: 6px; color: var(--color-muted); cursor: pointer; }
|
||||
.pv-head .seg div.on { background: #fff; color: var(--color-text); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.06); }
|
||||
.pv-head .right { margin-left: auto; display: flex; gap: 6px; }
|
||||
.mini-btn {
|
||||
font-size: 11px; border: 1px solid var(--color-primary); color: var(--color-primary);
|
||||
border-radius: 999px; padding: 3px 10px; background: #fff; white-space: nowrap; cursor: pointer;
|
||||
}
|
||||
.mini-btn.solid { background: var(--color-primary); color: #fff; }
|
||||
.mini-btn.grey { border-color: #dcdcdc; color: var(--color-muted); }
|
||||
.quick-add { padding: 0 12px 8px; flex-shrink: 0; }
|
||||
|
||||
/* ---- 列表视图 ---- */
|
||||
.list-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 16px; }
|
||||
.group-title { display: flex; align-items: center; gap: 7px; padding: 12px 16px 6px; font-size: 12px; color: var(--color-muted); font-weight: 600; }
|
||||
.group-title::before { content: ""; width: 3px; height: 12px; border-radius: 2px; background: var(--color-primary); }
|
||||
.group-title .cnt { color: #b8bcc4; font-weight: 400; }
|
||||
.task-row {
|
||||
display: flex; align-items: flex-start; gap: 9px; background: #fff; border-radius: 9px;
|
||||
padding: 10px 12px; margin: 0 12px 7px; box-shadow: 0 1px 2px rgba(0,0,0,.04); cursor: pointer;
|
||||
}
|
||||
.task-row.done { background: #fafbfa; }
|
||||
.task-row .box, .sub-row .box {
|
||||
width: 19px; height: 19px; border-radius: 50%; border: 1.5px solid #d0d0d0;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; cursor: pointer;
|
||||
}
|
||||
.task-row.done .box, .sub-row.done .box { background: #00b578; border-color: #00b578; }
|
||||
.box svg { opacity: 0; }
|
||||
.task-row.done .box svg, .sub-row.done .box svg { opacity: 1; }
|
||||
.task-main { flex: 1; min-width: 0; }
|
||||
.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-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.none { color: #b8bcc4; font-weight: 400; }
|
||||
.task-meta .members { font-size: 10.5px; color: #a7adb6; }
|
||||
.task-right { display: flex; align-items: center; flex-shrink: 0; }
|
||||
.more { color: #c2c7cf; font-size: 16px; line-height: 1; padding: 0 2px; cursor: pointer; }
|
||||
.sub-row {
|
||||
display: flex; align-items: center; 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);
|
||||
position: relative; cursor: pointer;
|
||||
}
|
||||
.sub-row::before { content: ""; position: absolute; left: 14px; top: 0; bottom: 0; width: 1px; background: #e9ecf0; }
|
||||
.sub-row .box { width: 16px; height: 16px; }
|
||||
.sub-title { flex: 1; font-size: 13px; color: #444; }
|
||||
.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; }
|
||||
.sub-add-hint { font-size: 11px; color: #b8bcc4; padding: 0 0 8px 44px; cursor: pointer; }
|
||||
|
||||
/* ---- 状态点 ---- */
|
||||
.sdot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
||||
.sdot.todo { background: #c9ced6; }
|
||||
.sdot.doing { background: #1677ff; }
|
||||
.sdot.done { background: #00b578; }
|
||||
.sdot.pill { width: auto; height: auto; border-radius: 999px; font-size: 10px; padding: 0 6px; line-height: 15px; color: #fff; }
|
||||
.sdot.pill.todo { background: #a8b0ba; }
|
||||
.sdot.pill.doing { background: #1677ff; }
|
||||
.sdot.pill.done { background: #00b578; }
|
||||
|
||||
/* ---- 看板视图 ---- */
|
||||
.board-wrap {
|
||||
display: flex; gap: 10px; padding: 0 12px 8px; overflow-x: auto; overflow-y: hidden;
|
||||
flex: 1 1 auto; min-height: 0; align-items: stretch;
|
||||
}
|
||||
.bcol { flex: 0 0 186px; display: flex; flex-direction: column; min-height: 0; }
|
||||
.bcol-head { display: flex; align-items: center; gap: 5px; padding: 0 2px 7px; flex-shrink: 0; }
|
||||
.bcol-name { font-size: 12px; font-weight: 700; color: #3d4450; }
|
||||
.bcol-cnt { font-size: 11px; color: #999; background: #e8ebef; border-radius: 999px; padding: 0 6px; line-height: 15px; }
|
||||
.bcol-bind { font-size: 10px; color: var(--color-primary); background: #f0f5ff; border-radius: 4px; padding: 0 4px; margin-left: auto; }
|
||||
.bcol-bind.off { color: #999; background: #f2f3f5; }
|
||||
.bcol-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 7px; padding-bottom: 8px; }
|
||||
.bcol.dragover .bcol-body { background: #eaf2ff; border-radius: 8px; outline: 1.5px dashed var(--color-primary); }
|
||||
.bcol-empty { font-size: 11px; color: #c0c4cc; text-align: center; padding: 10px 0; }
|
||||
.bcard {
|
||||
background: #fff; border-radius: 8px; padding: 9px 10px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.05); cursor: pointer; flex-shrink: 0;
|
||||
}
|
||||
.bcard.dragging { opacity: .55; transform: scale(.97) rotate(-1deg); box-shadow: 0 8px 18px rgba(15,23,42,.16); }
|
||||
.bcard.hl { outline: 2px solid var(--color-primary); outline-offset: 1px; }
|
||||
.brow1 { display: flex; align-items: flex-start; gap: 6px; }
|
||||
.btitle { flex: 1; font-size: 12.5px; font-weight: 600; line-height: 1.45; word-break: break-word; }
|
||||
.bstat { display: flex; align-items: center; padding: 2px; cursor: pointer; }
|
||||
.bmeta { display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 10.5px; }
|
||||
.bassignee { font-size: 11px; font-weight: 600; color: #4a5160; }
|
||||
.bassignee.none { color: #b8bcc4; font-weight: 400; }
|
||||
.bmembers { font-size: 10px; color: #a7adb6; }
|
||||
.bsub {
|
||||
display: flex; align-items: center; gap: 5px; margin-top: 5px; padding-top: 5px;
|
||||
border-top: 1px dashed #eef0f3; font-size: 11px; color: #5a616b; cursor: pointer;
|
||||
}
|
||||
.bsub .sbox { width: 13px; height: 13px; border-radius: 50%; border: 1.3px solid #d0d0d0; flex-shrink: 0; }
|
||||
.bsub.done .sbox { background: #00b578; border-color: #00b578; }
|
||||
.bsub .stitle { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.bsub.done .stitle { color: #a8a8a8; text-decoration: line-through; }
|
||||
.bmore { font-size: 10px; color: #999; margin-top: 4px; }
|
||||
.bsub-add { font-size: 10.5px; color: #b8bcc4; margin-top: 6px; cursor: pointer; }
|
||||
.badd {
|
||||
background: rgba(255,255,255,.6); border: 1px dashed #d5dae1; border-radius: 8px;
|
||||
padding: 8px; text-align: center; font-size: 11.5px; color: #b8bcc4; cursor: pointer; flex-shrink: 0;
|
||||
}
|
||||
.drag-ghost {
|
||||
position: fixed; z-index: 999; transform: translate(-50%, -50%); pointer-events: none;
|
||||
background: rgba(22,119,255,.92); color: #fff; font-size: 12px; padding: 6px 10px;
|
||||
border-radius: 8px; max-width: 180px; box-shadow: 0 8px 18px rgba(15,23,42,.25);
|
||||
}
|
||||
|
||||
/* ---- 列设置 / 成员页 ---- */
|
||||
.cs-page { position: fixed; inset: 0; z-index: 1000; background: var(--color-bg); display: flex; flex-direction: column; }
|
||||
.cs-page .navbar-sub { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: #fff; border-bottom: 1px solid var(--color-border); flex-shrink: 0; }
|
||||
.cs-page .navbar-sub .back { font-size: 20px; color: #666; line-height: 1; cursor: pointer; }
|
||||
.cs-page .navbar-sub .title { font-size: 16px; font-weight: 700; }
|
||||
.cs-page .navbar-sub .right { margin-left: auto; display: flex; gap: 6px; }
|
||||
.cs-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 20px; }
|
||||
.set-row {
|
||||
display: flex; align-items: center; gap: 10px; background: #fff;
|
||||
padding: 12px 14px; margin-bottom: 1px;
|
||||
}
|
||||
.set-row .sname { font-size: 13px; font-weight: 600; width: 64px; flex-shrink: 0; cursor: pointer; }
|
||||
.set-row .sctrl { flex: 1; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
|
||||
.set-hint { font-size: 11px; color: #999; padding: 8px 14px 14px; line-height: 1.7; }
|
||||
.section-h { font-size: 11px; color: #b8bcc4; letter-spacing: .5px; display: flex; align-items: center; gap: 6px; }
|
||||
.drop {
|
||||
border: 1px solid var(--color-border); border-radius: 7px; padding: 4px 6px; font-size: 12px;
|
||||
color: var(--color-text); background: #fff; min-width: 96px;
|
||||
}
|
||||
.drop.off { color: #999; background: #f7f8fa; }
|
||||
.cs-ord { display: flex; flex-direction: column; font-size: 9px; color: #c0c4cc; line-height: 1.1; }
|
||||
.cs-ord span { cursor: pointer; }
|
||||
.cs-del { font-size: 11px; color: #ff3141; cursor: pointer; padding: 2px 4px; }
|
||||
.cs-count { font-size: 11px; color: #999; margin-left: 8px; }
|
||||
|
||||
/* ---- 任务详情 ---- */
|
||||
.sheet-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; word-break: break-word; }
|
||||
.sheet-sub { font-size: 11.5px; color: #999; margin-bottom: 14px; }
|
||||
.field { margin-bottom: 13px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--color-muted); margin-bottom: 6px; }
|
||||
.inp {
|
||||
border: 1px solid var(--color-border); border-radius: 8px; padding: 8px 10px;
|
||||
font-size: 14px; background: #fff; color: var(--color-text); width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
textarea.inp { resize: vertical; }
|
||||
.pick-row { display: flex; align-items: center; gap: 10px; padding: 10px 4px; border-bottom: 1px solid var(--color-border); cursor: pointer; }
|
||||
.pick-row:last-child { border-bottom: none; }
|
||||
.pick-row .pn { flex: 1; font-size: 13.5px; }
|
||||
.avatar {
|
||||
width: 26px; height: 26px; border-radius: 50%; background: #dfe6f0; color: #5a6a80;
|
||||
font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.ck { width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid #d0d0d0; flex-shrink: 0; position: relative; }
|
||||
.ck.on { background: var(--color-primary); border-color: var(--color-primary); }
|
||||
.ck.on::after {
|
||||
content: ""; position: absolute; left: 5px; top: 2px; width: 4px; height: 8px;
|
||||
border: solid #fff; border-width: 0 1.5px 1.5px 0; transform: rotate(45deg);
|
||||
}
|
||||
.btn-primary { text-align: center; background: var(--color-primary); color: #fff; border-radius: 8px; padding: 10px 0; font-size: 15px; font-weight: 600; margin-top: 18px; cursor: pointer; }
|
||||
.btn-ghost { text-align: center; border: 1px solid var(--color-border); border-radius: 8px; padding: 9px 0; font-size: 15px; color: var(--color-muted); margin-top: 8px; background: #fff; cursor: pointer; }
|
||||
.btn-danger { text-align: center; border: 1px solid #ff3141; border-radius: 8px; padding: 9px 0; font-size: 15px; color: #ff3141; margin-top: 8px; background: #fff; cursor: pointer; }
|
||||
.del-hint { font-size: 11px; font-weight: 400; }
|
||||
.extra-row { display: flex; gap: 8px; margin-top: 8px; align-items: center; }
|
||||
.extra-k { flex: 0 0 82px; }
|
||||
.extra-v { flex: 1; }
|
||||
.extra-del { border: none; background: none; color: #c0c4cc; font-size: 18px; line-height: 1; cursor: pointer; }
|
||||
.extras { margin-top: 4px; }
|
||||
.extras-head {
|
||||
display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600;
|
||||
padding: 9px 10px; background: #f7f8fa; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
.extras-head .caret { font-size: 10px; color: #999; }
|
||||
.extras-head .cv { margin-left: auto; color: #b8bcc4; font-size: 10px; font-weight: 400; }
|
||||
.extras-body { padding: 4px 0 2px; }
|
||||
.extras-empty { font-size: 11.5px; color: #b8bcc4; padding: 8px 2px; }
|
||||
.extras-add { font-size: 12px; color: var(--color-primary); background: none; border: none; padding: 6px 2px; cursor: pointer; }
|
||||
.tag { font-size: 10px; color: var(--color-primary); background: #f0f5ff; border-radius: 4px; padding: 0 5px; }
|
||||
.tag.grey { color: #999; background: #f2f3f5; }
|
||||
|
||||
/* ---- 空态 / 错误态 ---- */
|
||||
.empty { text-align: center; padding: 70px 20px; color: #999; }
|
||||
.empty .ico {
|
||||
width: 64px; height: 64px; margin: 0 auto 12px; border-radius: 50%; background: #f2f3f5;
|
||||
display: flex; align-items: center; justify-content: center; color: #c4c8ce; font-size: 30px;
|
||||
}
|
||||
.empty .ico.err { background: #fff1f0; color: #ff7875; }
|
||||
.empty p { font-size: 13px; margin-bottom: 12px; }
|
||||
|
||||
Reference in New Issue
Block a user