diff --git a/docs/V0.2/任务模块/任务模块功能设计.md b/docs/V0.2/任务模块/任务模块功能设计.md index db505a2..e7268fc 100644 --- a/docs/V0.2/任务模块/任务模块功能设计.md +++ b/docs/V0.2/任务模块/任务模块功能设计.md @@ -4,14 +4,14 @@ | --- | --- | | 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) | | 文档状态 | **设计基线**(待评审) | -| 上游文档 | `任务模块需求文档.md`(**148 条需求点**,编号 `T-*`) | +| 上游文档 | `任务模块需求文档.md`(**149 条需求点**,编号 `T-*`) | | 配套文档 | `任务模块设计文档-框架设计.md`、`任务模块数据库设计文档.md`、`任务模块界面设计.html` | | 对应关系 | **本文与需求点一一对应**:每条需求点均给出「设计落点(功能/组件/接口/表字段)」与「实现要点」 | > **阅读方式** > 1. 本文按需求文档的模块码组织(`VW` / `COL` / `BD` / `FL` / `TK` / `AS` / `ST` / `PJ` / `PM` / `CP` / `UI` / `DT` / `NF` / `EX`)。 > 2. 每条需求点一行:**编号 → 设计落点 → 实现要点**。设计落点中的组件名、接口路径、表字段均与《任务模块数据库设计文档》《任务模块界面设计》一致。 -> 3. 文末 §15 给出**覆盖自检表**,确保 148 条无遗漏。 +> 3. 文末 §15 给出**覆盖自检表**,确保 149 条无遗漏。 --- @@ -210,6 +210,7 @@ | T-PJ-10 | 列表查询按成员关系过滤 | `WHERE EXISTS ProjectMember(userId)` | | T-PJ-11 | 新组件 `ProjectManage`(项目管理页) | 分区承载:成员(复用 `ProjectMembers` 逻辑)、列设置(复用 `ColumnSettings`)、项目信息;`ProjectView` 头部仅保留「项目管理」一个入口 | | T-PJ-12 | `ProjectManage` 的「项目信息」分区 | 改名(`PUT /api/projects/:id {name}`)、归档/取消归档(`{archived}`)、删除(个人项目隐藏入口)、退出(仅创建人可见);删除与退出均有二次确认 | +| T-PJ-13 | `Project.notes` Json + `ProjectManage` 的「信息块」分区 | 存 `[{title, content}]`(≤50 块,标题 ≤50 字、内容 ≤20000 字);`PUT {notes}` 整体替换;底部弹层独立编辑(标题输入 + 多行 textarea + 删除),键盘弹出时复用 `useKeyboardInset` 抬升 | --- @@ -326,13 +327,13 @@ | 任务字段体系 | T-TK-01 ~ 13 | 13 | §5 | | 负责人 & 成员 | T-AS-01 ~ 07 | 7 | §6 | | 子任务体系 | T-ST-01 ~ 15 | 15 | §7 | -| 项目体系 | T-PJ-01 ~ 12 | 12 | §8 | +| 项目体系 | T-PJ-01 ~ 13 | 13 | §8 | | 项目权限与入口 | T-PM-01 ~ 07 | 7 | §9 | | 跨项目拷贝 | T-CP-01 ~ 09 | 9 | §10 | | UI 交互规范 | T-UI-01 ~ 11 | 11 | §11 | | 数据需求 | T-DT-01 ~ 09 | 9 | §12 | | 非功能需求 | T-NF-01 ~ 05 | 5 | §13 | | 异常与边界 | T-EX-01 ~ 19 | 19 | §14 | -| **合计** | — | **148** | — | +| **合计** | — | **149** | — | -> 需求文档共 **148 条**需求点,本文逐条给出设计落点,覆盖率 **148/148**。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19`、`T-VW-08~10`、`T-PJ-11/12`、`T-UI-11` 为评审后新增,编号不连续,非笔误。) +> 需求文档共 **149 条**需求点,本文逐条给出设计落点,覆盖率 **149/149**。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19`、`T-VW-08~10`、`T-PJ-11/12`、`T-UI-11` 为评审后新增,编号不连续,非笔误。) diff --git a/docs/V0.2/任务模块/任务模块需求文档.md b/docs/V0.2/任务模块/任务模块需求文档.md index 1431084..324cadd 100644 --- a/docs/V0.2/任务模块/任务模块需求文档.md +++ b/docs/V0.2/任务模块/任务模块需求文档.md @@ -289,6 +289,7 @@ | T-PJ-10 | 项目列表按用户可见范围返回,仅含自己为成员的项目 | 非成员项目不出现在列表中 | P0 | | T-PJ-11 | 项目的**成员管理、成员新增、列设置**统一收进「**项目管理**」入口;项目页头部不再散置多个功能入口 | 项目管理内含 成员 / 列设置 / 项目信息 三个分区;头部仅保留「项目管理」一个入口 | P0 | | T-PJ-12 | 项目管理内提供**项目信息管理**:改名(T-PJ-04)、归档 / 取消归档(T-PJ-09)、删除项目(T-PJ-02,个人默认项目禁用)、退出项目(T-PJ-07,创建人可退) | 各操作在项目管理中可用且遵循既有约束;个人项目不出现删除与退出入口 | P0 | +| T-PJ-13 | 项目支持**信息块**:可自由添加任意多个长文本信息块(标题 + 多行内容,如 Readme / 项目说明),每块**独立编辑与删除**;属项目级数据,全体成员可见可编辑 | 可增删改任意块;内容支持多行长文本;所有成员看到同一份信息块 | P0 | ### 3.9 项目权限(T-PM) @@ -384,7 +385,7 @@ ## 4. 优先级汇总(本期交付范围) -- **P0(必须)**:T-VW-01~04、T-VW-06~09;T-COL-01~07、T-COL-09、T-COL-10;T-BD-01~10、13~16;T-FL-01~13、15;T-TK-01~11;T-AS-01~05、07;T-ST-01~09、11~14;T-PJ-01~08、10~12;T-PM-01、02、04~07;T-CP-01~06、08;T-UI-01~05、07~11;T-DT-01~08;T-NF-01~04;T-EX-01~05、07、09~12、15~17。 +- **P0(必须)**:T-VW-01~04、T-VW-06~09;T-COL-01~07、T-COL-09、T-COL-10;T-BD-01~10、13~16;T-FL-01~13、15;T-TK-01~11;T-AS-01~05、07;T-ST-01~09、11~14;T-PJ-01~08、10~13;T-PM-01、02、04~07;T-CP-01~06、08;T-UI-01~05、07~11;T-DT-01~08;T-NF-01~04;T-EX-01~05、07、09~12、15~17。 - **P1(应该)**:T-VW-05、10;T-COL-08;T-BD-11、12;T-FL-14;T-TK-12、13;T-AS-06;T-ST-10、15;T-PJ-09;T-PM-03;T-CP-07、09;T-UI-06;T-DT-09;T-NF-05;T-EX-06、08、13、14、18、19。 --- @@ -430,6 +431,7 @@ | 31 | 成员 / 列设置入口的位置? | 统一收进「**项目管理**」入口(含成员、列设置、项目信息三区),项目页头部不再散置(T-PJ-11) | | 32 | 项目信息管理包含什么? | 改名、归档 / 取消归档、删除(个人项目禁用)、退出(创建人可退),入口在项目管理内(T-PJ-12) | | 33 | 任务详情底部按钮遮挡? | 弹层底部预留**安全区 + 12px** 距离;小屏下弹层可滚动,底部按钮完整可见(T-UI-11) | +| 34 | 项目相关长文本信息记录在哪? | 项目管理内新增**信息块**分区:任意多个 {标题, 长文本} 块,独立编辑,属项目级数据(T-PJ-13) | **仍待评审确认(影响实现口径):** diff --git a/prisma/migrations/20261024130000_add_project_notes/migration.sql b/prisma/migrations/20261024130000_add_project_notes/migration.sql new file mode 100644 index 0000000..990c210 --- /dev/null +++ b/prisma/migrations/20261024130000_add_project_notes/migration.sql @@ -0,0 +1,7 @@ +-- 项目信息块 (T-PJ-13): Project 增加 notes JSON, 存 [{ title, content }] 任意多个长文本块 +-- 分三步保证存量行安全: 先可空加列 -> 回填 '[]' -> 收紧为 NOT NULL +ALTER TABLE `Project` ADD COLUMN `notes` JSON NULL; + +UPDATE `Project` SET `notes` = '[]' WHERE `notes` IS NULL; + +ALTER TABLE `Project` MODIFY `notes` JSON NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 43e19d4..28cfc7c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -393,6 +393,7 @@ model Project { owner User @relation("ProjectOwner", fields: [ownerId], references: [id], onDelete: Cascade) isPersonal Boolean @default(false) archived Boolean @default(false) + notes Json @default("[]") // 项目信息块 [{ title, content }] (T-PJ-13) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/server/routes/projects.js b/server/routes/projects.js index cff2023..4183983 100644 --- a/server/routes/projects.js +++ b/server/routes/projects.js @@ -107,18 +107,19 @@ router.get('/:id', async (req, res, next) => { ...projectSummary(project), members: members.map((m) => ({ userId: m.userId, name: m.user.name, email: m.user.email, permission: m.permission })), columns, + notes: Array.isArray(project.notes) ? project.notes : [], }, }) } catch (e) { next(e) } }) -// PUT /api/projects/:id { name?, archived? } +// PUT /api/projects/:id { name?, archived?, notes? } router.put('/:id', async (req, res, next) => { try { const userId = req.user.sub const project = await loadProjectForUser(req.params.id, userId) if (!project) return res.status(404).json({ error: '项目不存在或无权限' }) - const { name, archived } = req.body || {} + const { name, archived, notes } = req.body || {} const data = {} if (name !== undefined) { const t = String(name).trim() @@ -127,6 +128,14 @@ router.put('/:id', async (req, res, next) => { data.name = t } if (archived !== undefined) data.archived = !!archived + // 项目信息块 (T-PJ-13): 任意多个 { title, content }, 整体替换 + if (notes !== undefined) { + if (!Array.isArray(notes)) return res.status(400).json({ error: '信息块格式非法' }) + data.notes = notes.slice(0, 50).map((n) => ({ + title: String(n?.title ?? '').slice(0, 50), + content: String(n?.content ?? '').slice(0, 20000), + })) + } const row = await prisma.project.update({ where: { id: project.id }, data }) res.json({ item: projectSummary(row) }) } catch (e) { next(e) } diff --git a/src/pages/task/ProjectManage.jsx b/src/pages/task/ProjectManage.jsx index e0f292c..30c9b24 100644 --- a/src/pages/task/ProjectManage.jsx +++ b/src/pages/task/ProjectManage.jsx @@ -1,7 +1,8 @@ import { useState } from 'react' -import { Dialog, Input, Toast } from 'antd-mobile' +import { Dialog, Input, Popup, Toast } from 'antd-mobile' import { projectApi } from '../../api/task.js' import { useAuth } from '../../auth/AuthContext.jsx' +import useKeyboardInset from '../../hooks/useKeyboardInset.js' import useOverlayBack from '../../hooks/useOverlayBack.js' import ColumnSettings from './ColumnSettings.jsx' import ProjectMembers from './ProjectMembers.jsx' @@ -13,13 +14,18 @@ export default function ProjectManage({ project, onClose, onChanged, onExited }) const [view, setView] = useState('main') // main | columns | members const [renaming, setRenaming] = useState(false) const [name, setName] = useState(project.name) + // 信息块编辑 (T-PJ-13): index=-1 新增, >=0 编辑既有块 + const [noteEdit, setNoteEdit] = useState(null) const members = project.members || [] + const notes = Array.isArray(project.notes) ? project.notes : [] const isOwner = user?.id === project.ownerId const isPersonal = !!project.isPersonal // 返回键: 内层视图(列设置/成员)先回主区, 主区再关闭整个项目管理 useOverlayBack(true, () => { if (view !== 'main') setView('main'); else onClose() }) + // 键盘弹出插入量: 信息块编辑弹层用 + const kb = useKeyboardInset() if (view === 'columns') { return ( @@ -86,6 +92,37 @@ export default function ProjectManage({ project, onClose, onChanged, onExited }) } } + // 信息块保存 (T-PJ-13): 整体替换 notes 数组 + async function saveNote() { + const { index, title, content } = noteEdit + const next = notes.map((n) => ({ title: n.title || '', content: n.content || '' })) + const block = { title: String(title || '').trim(), content: String(content || '') } + if (!block.title && !block.content) { Toast.show({ content: '请填写标题或内容' }); return } + if (index >= 0) next[index] = block + else next.push(block) + try { + await projectApi.update(project.id, { notes: next }) + Toast.show({ content: '已保存' }) + setNoteEdit(null) + onChanged?.() + } catch (e) { + Toast.show({ content: e.message }) + } + } + + async function removeNote() { + const { index } = noteEdit + const next = notes.filter((_, i) => i !== index) + try { + await projectApi.update(project.id, { notes: next }) + Toast.show({ content: '已删除' }) + setNoteEdit(null) + onChanged?.() + } catch (e) { + Toast.show({ content: e.message }) + } + } + async function quit() { const ok = await Dialog.confirm({ content: '退出该项目?退出后将不再可见。', @@ -130,6 +167,25 @@ export default function ProjectManage({ project, onClose, onChanged, onExited }) 列设置 › +