fix(V0.2): BUILD_ID 时区 / 新建弹框残留 / 项目信息块
Docker Build and Push / build-image (push) Successful in 4m19s
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user