+ {page.rows.length === 0 ? (
+ 落第一笔:输入表达式,回车起一行 · #1 引用上文结果
+ ) : (
+ {page.rows.map((row, index) => {
+ const result: CellResult = evaluation.results.get(row.id) ?? { status: 'empty' }
+ const activeRowNum = activeRowId
+ ? page.rows.findIndex((r) => r.id === activeRowId) + 1
+ : 0
+ const inboundFrom =
+ activeRowId && activeRowNum > 0 && upstream.has(row.id)
+ ? evaluation.directDeps.get(activeRowId)?.includes(row.id)
+ ? activeRowNum
+ : undefined
+ : undefined
+ const refColorFor = (pos: number) => {
+ const target = page.rows[pos - 1]
+ return target ? colorForSeq(target.seq) : '#8b94b5'
+ }
+ return (
+ page.rows[pos - 1]?.subtitle}
+ refOptions={page.rows.map((r, i) => ({ pos: i + 1, subtitle: r.subtitle }))}
+ pageOptions={doc.pages.map((p) => ({
+ id: p.id,
+ name: p.name,
+ rows: p.rows.map((r, i) => ({ pos: i + 1, subtitle: r.subtitle }))
+ }))}
+ atSubtitleFor={(page) => doc.pages[Number(page) - 1]?.name}
+ atRowSubtitleFor={(page, ref) =>
+ doc.pages[Number(page) - 1]?.rows[Number(ref) - 1]?.subtitle
+ }
+ onAtClick={(page, ref) => {
+ const targetPage = doc.pages[Number(page) - 1]
+ if (!targetPage) {
+ showToast(`未找到第 ${page} 个草稿`)
+ return
+ }
+ const row = targetPage.rows[Number(ref) - 1]
+ if (!row) {
+ showToast(`草稿「${targetPage.name}」中没有第 ${ref} 行`)
+ return
+ }
+ const go = () => {
+ setFlashRowId(row.id)
+ if (flashTimer.current) window.clearTimeout(flashTimer.current)
+ flashTimer.current = window.setTimeout(() => setFlashRowId(null), 1200)
+ document
+ .querySelector(`[data-row-id="${row.id}"]`)
+ ?.scrollIntoView({ block: 'center', behavior: 'smooth' })
+ }
+ if (targetPage.id !== doc.activePageId) {
+ dispatch({ type: 'setActivePage', pageId: targetPage.id })
+ window.setTimeout(go, 80)
+ } else {
+ go()
+ }
+ }}
+ isEditing={editingRowId === row.id}
+ isActive={activeRowId === row.id}
+ isUpstream={activeRowId !== row.id && upstream.has(row.id)}
+ inboundFrom={inboundFrom}
+ isFlashed={flashRowId === row.id}
+ onRefClick={(pos) => {
+ const target = page.rows[pos - 1]
+ if (target) jumpToRow(target.id)
+ else showToast(`#${pos} 还没写`)
+ }}
+ onChange={(expr) =>
+ dispatch({ type: 'editRow', pageId: page.id, rowId: row.id, expr })
+ }
+ onFocus={() => setActiveRowId(row.id)}
+ onBlur={() => {
+ setActiveRowId((current) => (current === row.id ? null : current))
+ setEditingRowId((current) => (current === row.id ? null : current))
+ }}
+ onEnter={() => addRow(row.id)}
+ onEscape={() => {
+ // Esc 退回选中模式,保留选中态
+ setEditingRowId(null)
+ }}
+ onNavigate={navigateRow}
+ onInputTab={(fromInput, direction) => handleInputTab(row.id, fromInput, direction)}
+ subtitleFocusPending={pendingSubtitleId === row.id}
+ onSubtitleChange={(subtitle) =>
+ dispatch({ type: 'editSubtitle', pageId: page.id, rowId: row.id, subtitle })
+ }
+ onCopy={handleCopy}
+ onInject={handleInject}
+ onRequestEdit={() => {
+ setEditingRowId(row.id)
+ setActiveRowId(row.id)
+ }}
+ onDelete={() => {
+ // 至少保留一行:删最后一行改为清空内容
+ if (page.rows.length <= 1) {
+ dispatch({ type: 'editRow', pageId: page.id, rowId: row.id, expr: '' })
+ return
+ }
+ setEditingRowId((c) => (c === row.id ? null : c))
+ setActiveRowId((c) => (c === row.id ? null : c))
+ const neighbor = page.rows[index + 1] ?? page.rows[index - 1]
+ if (neighbor && neighbor.id !== row.id) setPendingFocusId(neighbor.id)
+ dispatch({ type: 'deleteRow', pageId: page.id, rowId: row.id })
+ }}
+ onMove={handleMove}
+ onDragStateChange={setDragging}
+ />
+ )
+ })}
+
+ )}
+ addRow(page.rows[page.rows.length - 1]?.id)}
+ >
+ {Array.from({ length: 40 }, (_, i) => (
+
+ {i === 0 && !activeRowId && !editingRowId && (
+ 双击或回车,新建一行
+ )}
+
+ ))}
+
+