fix(V0.2): BUILD_ID 时区 / 新建弹框残留 / 项目信息块
Docker Build and Push / build-image (push) Successful in 4m19s

1. BUILD_ID 改用 UTC+8 生成: CI runner 是 UTC 时区, 原来取本地时间会错位;
   统一偏移 +8h 后按 UTC 读取 (vite.config.js makeBuildId)

2. 修复「我的项目」页反复弹新建框: createSignal 一次性消费,
   ProjectList 处理后回调清零 (此前 TaskPage 状态在 /task 与 /task/list
   间保留, 信号残留导致每次进入都触发)

3. 新增项目信息块 (T-PJ-13):
   - Project.notes Json ([{title,content}], <=50 块 / 标题<=50 字 / 内容<=20000 字)
   - 迁移 20261024130000: 加列(NULL)->回填'[]'->收紧 NOT NULL, 存量行安全
   - GET 详情返回 notes; PUT 接受 notes 整体替换
   - 项目管理新增「信息块」分区: 列表预览 + 底部弹层独立编辑(标题+多行
     textarea+删除), 键盘弹出复用 useKeyboardInset 抬升
   - 文档: 需求 T-PJ-13 (149 条) + 功能设计对应行

已验证: 109 文件语法 0 错误、文档 149/149 覆盖
This commit is contained in:
jif2.zhang
2026-10-24 21:37:05 +08:00
parent 1fd7522fa6
commit b71ceca3f2
9 changed files with 189 additions and 54 deletions
+100 -1
View File
@@ -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 })
<span className="link">列设置 ›</span>
</div>
<div className="section-h" style={{ margin: '14px 14px 8px' }}>信息块({notes.length})</div>
{notes.map((n, i) => (
<div
className="set-row note-row"
key={i}
style={{ padding: '11px 14px', alignItems: 'flex-start' }}
onClick={() => setNoteEdit({ index: i, title: n.title || '', content: n.content || '' })}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div className="note-title">{n.title || '未命名'}</div>
<div className="note-preview">{String(n.content || '').split('\n')[0] || '(空)'}</div>
</div>
<span className="link" style={{ flexShrink: 0, marginTop: 2 }}>编辑</span>
</div>
))}
<div className="set-row" style={{ padding: '11px 14px' }} onClick={() => setNoteEdit({ index: -1, title: '', content: '' })}>
<span className="pn" style={{ flex: 1, fontSize: 13.5, color: 'var(--color-primary)' }}>+ 添加信息块</span>
</div>
<div className="section-h" style={{ margin: '14px 14px 8px' }}>项目信息</div>
<div className="set-row" style={{ padding: '12px 14px' }} onClick={() => { setName(project.name); setRenaming(true) }}>
<span className="pn" style={{ flex: 1, fontSize: 13.5 }}>项目名称</span>
@@ -171,6 +227,49 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
]}
onClose={() => setRenaming(false)}
/>
{/* 信息块编辑 (T-PJ-13): 标题 + 长文本, 独立编辑 */}
<Popup
visible={!!noteEdit}
onMaskClick={() => setNoteEdit(null)}
position="bottom"
bodyStyle={{
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
maxHeight: '86vh',
overflowY: 'auto',
padding: `16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px + ${kb}px)`,
}}
>
<div className="sheet-title">{noteEdit?.index >= 0 ? '编辑信息块' : '添加信息块'}</div>
<div className="sheet-sub">用于记录 Readme 等项目相关长文本,每块独立编辑</div>
<div className="field">
<label>标题</label>
<Input
placeholder="如:项目说明 / Readme"
value={noteEdit?.title || ''}
maxLength={50}
onChange={(v) => setNoteEdit((p) => ({ ...(p || {}), title: v }))}
/>
</div>
<div className="field">
<label>内容</label>
<textarea
className="inp"
rows={10}
placeholder="支持多行长文本…"
value={noteEdit?.content || ''}
onChange={(e) => setNoteEdit((p) => ({ ...(p || {}), content: e.target.value }))}
/>
</div>
<div className="btn-primary" onClick={saveNote}>保存</div>
{noteEdit?.index >= 0 && (
<div className="btn-danger" onClick={removeNote}>删除此信息块</div>
)}
<div className="btn-ghost" onClick={() => setNoteEdit(null)}>取消</div>
</Popup>
</div>
)
}
+10 -3
View File
@@ -80,6 +80,7 @@ export default function TaskPage() {
onOpen={(id) => { selectProject(id); navigate('/task') }}
onChanged={load}
createSignal={createSignal}
onCreateConsumed={() => setCreateSignal(0)}
/>
</div>
</div>
@@ -132,7 +133,7 @@ const MOVE_TOLERANCE = 8
// 完整项目列表 (从「更多项目」进入)
// 顺序: 手动排序(recentOrder, 拖动结果)优先 > 最近访问 > 列表序; 已归档固定在底部
function ProjectList({ projects, err, onRetry, onOpen, onChanged, createSignal }) {
function ProjectList({ projects, err, onRetry, onOpen, onChanged, createSignal, onCreateConsumed }) {
const [creating, setCreating] = useState(false)
const [name, setName] = useState('')
const [saving, setSaving] = useState(false)
@@ -144,8 +145,14 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged, createSignal }
const activeRef = useRef(false)
const dragIdRef = useRef(null)
// 头部「+ 新建」→ 弹起新建对话框
useEffect(() => { if (createSignal) setCreating(true) }, [createSignal])
// 头部「+ 新建」→ 弹起新建对话框 (一次性: 消费后清零, 否则下次进入该页会再次弹出)
useEffect(() => {
if (createSignal) {
setCreating(true)
onCreateConsumed?.()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [createSignal])
const active = useMemo(() => {
const manual = getRecentOrder()
+8
View File
@@ -1135,3 +1135,11 @@ textarea.inp { resize: vertical; }
/* ---- 项目列表拖动排序 (bug#3) ---- */
.proj-item.dragging { opacity: .65; transform: scale(.98); box-shadow: 0 8px 18px rgba(15,23,42,.16); z-index: 5; position: relative; }
/* ---- 项目信息块 (T-PJ-13) ---- */
.note-row { cursor: pointer; }
.note-title { font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.note-preview {
font-size: 11.5px; color: #999; margin-top: 3px; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}