Skip to content

Commit 57a78c7

Browse files
committed
refactor(dialogs): migrate seven overlays to modal
1 parent 0228a01 commit 57a78c7

8 files changed

Lines changed: 621 additions & 502 deletions

‎app/src/components/ChatSaveNoteDialog.tsx‎

Lines changed: 66 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* (onOpenNote 已由 ChatPage 透传)+ 关闭;失败红字不关窗(改后再试)。
99
*/
1010
import { useState } from "react";
11-
import { zIndex } from "../ui/zIndex";
11+
import { Modal } from "../ui/primitives";
1212
import { invoke } from "@tauri-apps/api/core";
1313
import type { Note, NoteGroup } from "../types";
1414

@@ -63,87 +63,71 @@ export default function ChatSaveNoteDialog({ initialTitle, content, groups, onOp
6363
};
6464

6565
return (
66-
<>
67-
<div
68-
onClick={onClose}
69-
data-testid="chat-note-backdrop"
70-
style={{ position: "fixed", inset: 0, zIndex: zIndex("modal"), background: "rgba(0,0,0,0.18)" }}
71-
/>
72-
<div
73-
data-testid="chat-note-dialog"
74-
style={{
75-
position: "fixed", zIndex: zIndex("modal"), top: "50%", left: "50%",
76-
transform: "translate(-50%, -50%)",
77-
width: 380, background: "#fff", borderRadius: 10,
78-
border: "1px solid #e5e7eb", boxShadow: "0 12px 32px rgba(0,0,0,0.18)",
79-
padding: 14, fontSize: 12.5,
80-
}}
81-
>
82-
<div style={{ fontWeight: 700, fontSize: 14, color: "#111827", marginBottom: 10 }}>
83-
📄 另存为笔记
84-
</div>
85-
86-
{savedId != null ? (
87-
<>
88-
<div data-testid="chat-note-saved" style={{ color: "#047857", marginBottom: 10 }}>
89-
{/* 信息9(审查裸号):去 `#id`——成功态以标题语义回显(REQ-277);
90-
title 恒非空(保存前置校验 + 默认值)——空时仅兜底不带书名号 */}
91-
✓ 已保存为笔记{title.trim() ? `《${title.trim()}》` : ""}
92-
</div>
93-
<div style={{ display: "flex", gap: 8 }}>
94-
<button
95-
data-testid="chat-note-open"
96-
style={{ ...BTN_BASE, background: "#0d9488", color: "#fff", border: "none", fontWeight: 600 }}
97-
onClick={() => onOpenNote(savedId)}
98-
>
99-
在笔记页打开 →
100-
</button>
101-
<button style={BTN_BASE} onClick={onClose}>关闭</button>
102-
</div>
103-
</>
104-
) : (
105-
<>
106-
<label style={{ display: "block", color: "#6b7280", marginBottom: 4 }}>笔记标题</label>
107-
<input
108-
data-testid="chat-note-title"
109-
value={title}
110-
onChange={(e) => setTitle(e.target.value)}
111-
style={{ width: "100%", padding: "6px 8px", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box", marginBottom: 8 }}
112-
/>
113-
<label style={{ display: "block", color: "#6b7280", marginBottom: 4 }}>目标组(可选)</label>
114-
<select
115-
data-testid="chat-note-group"
116-
value={groupId ?? ""}
117-
onChange={(e) => setGroupId(e.target.value ? Number(e.target.value) : null)}
118-
style={{ width: "100%", padding: "6px 8px", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box", marginBottom: 8, background: "#fff" }}
66+
/* 批 4 T5:自建遮罩 + 面板几何 + 手写关闭(原 `:67-84`)整段交给 `Modal`。
67+
外观与键盘语义(遮罩点关 / ESC / 焦点陷阱 / 滚动锁)由原语持有,本组件只留业务。
68+
`closeOnEsc` 默认 true:原实现**没有** ESC 路径(无 keydown 监听),迁移后新增了一条
69+
"ESC 即关闭",与遮罩点击同义(两条路径本已都不看 `busy`)—— 见报告「行为等价性」表。 */
70+
<Modal open onClose={onClose} title="另存为笔记" size="s" testId="chat-note-dialog">
71+
{savedId != null ? (
72+
<>
73+
<div data-testid="chat-note-saved" style={{ color: "#047857", marginBottom: 10 }}>
74+
{/* 信息9(审查裸号):去 `#id`——成功态以标题语义回显(REQ-277);
75+
title 恒非空(保存前置校验 + 默认值)——空时仅兜底不带书名号 */}
76+
✓ 已保存为笔记{title.trim() ? `《${title.trim()}》` : ""}
77+
</div>
78+
<div style={{ display: "flex", gap: 8 }}>
79+
<button
80+
data-testid="chat-note-open"
81+
style={{ ...BTN_BASE, background: "#0d9488", color: "#fff", border: "none", fontWeight: 600 }}
82+
onClick={() => onOpenNote(savedId)}
83+
>
84+
在笔记页打开 →
85+
</button>
86+
<button style={BTN_BASE} onClick={onClose}>关闭</button>
87+
</div>
88+
</>
89+
) : (
90+
<>
91+
<label style={{ display: "block", color: "#6b7280", marginBottom: 4 }}>笔记标题</label>
92+
<input
93+
data-testid="chat-note-title"
94+
value={title}
95+
onChange={(e) => setTitle(e.target.value)}
96+
style={{ width: "100%", padding: "6px 8px", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box", marginBottom: 8 }}
97+
/>
98+
<label style={{ display: "block", color: "#6b7280", marginBottom: 4 }}>目标组(可选)</label>
99+
<select
100+
data-testid="chat-note-group"
101+
value={groupId ?? ""}
102+
onChange={(e) => setGroupId(e.target.value ? Number(e.target.value) : null)}
103+
style={{ width: "100%", padding: "6px 8px", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box", marginBottom: 8, background: "#fff" }}
104+
>
105+
<option value="">不归组(全部笔记)</option>
106+
{groups.map((g) => (
107+
<option key={g.id} value={g.id}>
108+
{g.name}
109+
</option>
110+
))}
111+
</select>
112+
<div style={{ fontSize: 11.5, color: "#6b7280", marginBottom: 10 }}>
113+
共 {content.length} 字符 · 对话以提问引用 + AI 回答全文的完整形式保存
114+
</div>
115+
{status && (
116+
<div data-testid="chat-note-error" style={{ color: "#dc2626", marginBottom: 8 }}>{status}</div>
117+
)}
118+
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
119+
<button data-testid="chat-note-cancel" style={BTN_BASE} onClick={onClose} disabled={busy}>取消</button>
120+
<button
121+
data-testid="chat-note-save"
122+
style={{ ...BTN_BASE, background: "#0d9488", color: "#fff", border: "none", fontWeight: 600 }}
123+
onClick={() => void save()}
124+
disabled={busy || !title.trim()}
119125
>
120-
<option value="">不归组(全部笔记)</option>
121-
{groups.map((g) => (
122-
<option key={g.id} value={g.id}>
123-
{g.name}
124-
</option>
125-
))}
126-
</select>
127-
<div style={{ fontSize: 11.5, color: "#6b7280", marginBottom: 10 }}>
128-
共 {content.length} 字符 · 对话以提问引用 + AI 回答全文的完整形式保存
129-
</div>
130-
{status && (
131-
<div data-testid="chat-note-error" style={{ color: "#dc2626", marginBottom: 8 }}>{status}</div>
132-
)}
133-
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
134-
<button data-testid="chat-note-cancel" style={BTN_BASE} onClick={onClose} disabled={busy}>取消</button>
135-
<button
136-
data-testid="chat-note-save"
137-
style={{ ...BTN_BASE, background: "#0d9488", color: "#fff", border: "none", fontWeight: 600 }}
138-
onClick={() => void save()}
139-
disabled={busy || !title.trim()}
140-
>
141-
{busy ? "保存中…" : "保存"}
142-
</button>
143-
</div>
144-
</>
145-
)}
146-
</div>
147-
</>
126+
{busy ? "保存中…" : "保存"}
127+
</button>
128+
</div>
129+
</>
130+
)}
131+
</Modal>
148132
);
149133
}

‎app/src/components/GoalPlanApprovalDialog.tsx‎

Lines changed: 89 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* 清理登记(丢弃项)诚实展示,绝不静默。
99
*/
1010
import { useMemo, useState } from "react";
11-
import { zIndex } from "../ui/zIndex";
11+
import { Modal } from "../ui/primitives";
1212
import type { GoalPlanView } from "../types/goals";
1313

1414
interface Props {
@@ -76,94 +76,100 @@ export default function GoalPlanApprovalDialog({ view, onConfirm, onClose, onUse
7676
};
7777

7878
return (
79-
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.35)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: zIndex("modal") }}>
80-
<div data-testid="plan-approval" style={{ width: 560, maxHeight: "86vh", overflow: "auto", background: "#fff", borderRadius: 10, padding: 18, boxSizing: "border-box" }}>
81-
<div style={{ display: "flex", alignItems: "center", marginBottom: 6 }}>
82-
<span style={{ fontWeight: 700, fontSize: 15 }}>✨ AI 规划建议(草案——确认后落库)</span>
83-
<button onClick={onClose} style={{ marginLeft: "auto", border: "none", background: "none", fontSize: 14, cursor: "pointer", color: "#9ca3af" }}>✕</button>
84-
</div>
85-
{view.proposal.summary && (
86-
<p style={{ fontSize: 12, color: "#374151", background: "#fafaf9", padding: 8, borderRadius: 6, margin: "0 0 8px" }}>{view.proposal.summary}</p>
87-
)}
88-
{view.dropped.droppedMilestones.length + view.dropped.droppedGroups.length + view.dropped.droppedSystems.length > 0 && (
89-
<p style={{ fontSize: 11, color: "#b45309", background: "#fffbeb", padding: "6px 8px", borderRadius: 6, margin: "0 0 8px" }}>
90-
已剔除不合法建议 {view.dropped.droppedMilestones.length + view.dropped.droppedGroups.length + view.dropped.droppedSystems.length} 条(详见详情页记载)
91-
</p>
92-
)}
93-
{view.honestNote && <p style={{ fontSize: 11, color: "#b45309", margin: "0 0 8px" }}>{view.honestNote}</p>}
94-
95-
<SubTitle>里程碑({selectedMilestones.length} 条已选)</SubTitle>
96-
{view.proposal.milestones.length === 0 && <Empty>无里程碑建议——可改用规则草案或稍后在详情页添加</Empty>}
97-
{view.proposal.milestones.map((m, i) => (
98-
<div key={i} style={{ display: "flex", gap: 6, alignItems: "center", padding: "3px 0" }}>
99-
<input
100-
type="checkbox"
101-
checked={selectedMilestones.includes(i)}
102-
onChange={(e) => setSelectedMilestones((s) => e.target.checked ? [...s, i] : s.filter((x) => x !== i))}
103-
/>
104-
<span style={{ fontSize: 11, color: "#9ca3af", width: 46 }}>{m.dueWeeks}周</span>
105-
<input
106-
data-testid={`plan-milestone-${i}`}
107-
value={milestoneTitles[i]}
108-
onChange={(e) => setEditingTitles((t) => ({ ...t, [i]: e.target.value }))}
109-
style={{ flex: 1, fontSize: 12, padding: "2px 6px", border: "1px solid #e5e7eb", borderRadius: 4 }}
110-
/>
111-
{m.criteriaType === "group_settled" && <span style={{ fontSize: 10, color: "#0f766e" }}>随结算</span>}
112-
</div>
113-
))}
114-
115-
<SubTitle>绑定组({selectedGroups.length} 条已选)</SubTitle>
116-
{view.proposal.groups.length === 0 && <Empty>无组建议</Empty>}
117-
{view.proposal.groups.map((g, i) => (
118-
<label key={i} style={{ display: "flex", gap: 6, alignItems: "center", fontSize: 12, padding: "2px 0", cursor: "pointer" }}>
119-
<input
120-
type="checkbox"
121-
checked={selectedGroups.includes(i)}
122-
onChange={(e) => setSelectedGroups((s) => e.target.checked ? [...s, i] : s.filter((x) => x !== i))}
123-
/>
124-
组 #{g.groupId} {g.reason && <span style={{ color: "#9ca3af", fontSize: 11 }}>({g.reason})</span>}
125-
</label>
126-
))}
127-
128-
<SubTitle>体系({selectedSystems.length} 条已选)</SubTitle>
129-
{view.proposal.systems.length === 0 && <Empty>无体系建议——可稍后手动挂接/新建</Empty>}
130-
{view.proposal.systems.map((s, i) => (
131-
<label key={i} style={{ display: "flex", gap: 6, alignItems: "flex-start", fontSize: 12, padding: "2px 0", cursor: "pointer" }}>
132-
<input
133-
type="checkbox"
134-
checked={selectedSystems.includes(i)}
135-
onChange={(e) => setSelectedSystems((sel) => e.target.checked ? [...sel, i] : sel.filter((x) => x !== i))}
136-
/>
137-
<span>
138-
{s.action === "create" ? `新建体系「${s.name}」(核心问题:${s.coreQuestion};入口 ${s.domainEntries.length};初始概念 ${s.concepts.length})`
139-
: `挂接现有体系 #${s.systemId}`}
140-
</span>
141-
</label>
142-
))}
143-
144-
{view.proposal.weeklyContract && (
145-
<>
146-
<SubTitle>周契约建议(可改)</SubTitle>
147-
<div style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 12 }}>
148-
<span>每周</span>
149-
<input type="number" min={1} max={7} value={days} onChange={(e) => setDays(Math.max(1, Math.min(7, Number(e.target.value) || 1)))} style={numStyle} />
150-
<span>天 ·</span>
151-
<input type="number" min={1} max={200} value={cards} onChange={(e) => setCards(Math.max(1, Math.min(200, Number(e.target.value) || 1)))} style={numStyle} />
152-
<span>卡(弹性承诺:断签不清零)</span>
153-
</div>
154-
</>
155-
)}
156-
157-
{err && <p data-testid="plan-error" style={{ fontSize: 11, color: "#dc2626", margin: "6px 0 0" }}>{err}</p>}
158-
<div style={{ display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" }}>
79+
/* 批 4 T5:自建遮罩 + 面板几何 + 手写标题/关闭(原 `:79-84`)交给 `Modal`。
80+
档位 `l`(720) 对齐规格 §5.2 的"向导/工作台",原面板 560 ⇒ **+160 px**(已登记为可见变化)。
81+
原实现没有 ESC 路径,迁移后新增(同遮罩点击);本组件在 `InterviewDialog` 内嵌套渲染,
82+
`Modal` 的 React 树深度机制会把它认成内层(ESC 只关这一层)—— 见报告。 */
83+
<Modal
84+
open
85+
onClose={onClose}
86+
title="✨ AI 规划建议(草案——确认后落库)"
87+
size="l"
88+
testId="plan-approval"
89+
footer={
90+
<>
15991
<button onClick={onUseRules} style={ghostBtn} title="不采纳 AI 建议——回到 M1 规则草案(永远可用的降级基线)">改用规则草案</button>
16092
<button onClick={onClose} style={ghostBtn}>再想想</button>
16193
<button data-testid="plan-confirm" onClick={() => void confirm()} disabled={saving} style={primaryBtn}>
16294
{saving ? "落库中…" : "✓ 确认采用"}
16395
</button>
96+
</>
97+
}
98+
>
99+
{view.proposal.summary && (
100+
<p style={{ fontSize: 12, color: "#374151", background: "#fafaf9", padding: 8, borderRadius: 6, margin: "0 0 8px" }}>{view.proposal.summary}</p>
101+
)}
102+
{view.dropped.droppedMilestones.length + view.dropped.droppedGroups.length + view.dropped.droppedSystems.length > 0 && (
103+
<p style={{ fontSize: 11, color: "#b45309", background: "#fffbeb", padding: "6px 8px", borderRadius: 6, margin: "0 0 8px" }}>
104+
已剔除不合法建议 {view.dropped.droppedMilestones.length + view.dropped.droppedGroups.length + view.dropped.droppedSystems.length} 条(详见详情页记载)
105+
</p>
106+
)}
107+
{view.honestNote && <p style={{ fontSize: 11, color: "#b45309", margin: "0 0 8px" }}>{view.honestNote}</p>}
108+
109+
<SubTitle>里程碑({selectedMilestones.length} 条已选)</SubTitle>
110+
{view.proposal.milestones.length === 0 && <Empty>无里程碑建议——可改用规则草案或稍后在详情页添加</Empty>}
111+
{view.proposal.milestones.map((m, i) => (
112+
<div key={i} style={{ display: "flex", gap: 6, alignItems: "center", padding: "3px 0" }}>
113+
<input
114+
type="checkbox"
115+
checked={selectedMilestones.includes(i)}
116+
onChange={(e) => setSelectedMilestones((s) => e.target.checked ? [...s, i] : s.filter((x) => x !== i))}
117+
/>
118+
<span style={{ fontSize: 11, color: "#9ca3af", width: 46 }}>{m.dueWeeks}周</span>
119+
<input
120+
data-testid={`plan-milestone-${i}`}
121+
value={milestoneTitles[i]}
122+
onChange={(e) => setEditingTitles((t) => ({ ...t, [i]: e.target.value }))}
123+
style={{ flex: 1, fontSize: 12, padding: "2px 6px", border: "1px solid #e5e7eb", borderRadius: 4 }}
124+
/>
125+
{m.criteriaType === "group_settled" && <span style={{ fontSize: 10, color: "#0f766e" }}>随结算</span>}
164126
</div>
165-
</div>
166-
</div>
127+
))}
128+
129+
<SubTitle>绑定组({selectedGroups.length} 条已选)</SubTitle>
130+
{view.proposal.groups.length === 0 && <Empty>无组建议</Empty>}
131+
{view.proposal.groups.map((g, i) => (
132+
<label key={i} style={{ display: "flex", gap: 6, alignItems: "center", fontSize: 12, padding: "2px 0", cursor: "pointer" }}>
133+
<input
134+
type="checkbox"
135+
checked={selectedGroups.includes(i)}
136+
onChange={(e) => setSelectedGroups((s) => e.target.checked ? [...s, i] : s.filter((x) => x !== i))}
137+
/>
138+
组 #{g.groupId} {g.reason && <span style={{ color: "#9ca3af", fontSize: 11 }}>({g.reason})</span>}
139+
</label>
140+
))}
141+
142+
<SubTitle>体系({selectedSystems.length} 条已选)</SubTitle>
143+
{view.proposal.systems.length === 0 && <Empty>无体系建议——可稍后手动挂接/新建</Empty>}
144+
{view.proposal.systems.map((s, i) => (
145+
<label key={i} style={{ display: "flex", gap: 6, alignItems: "flex-start", fontSize: 12, padding: "2px 0", cursor: "pointer" }}>
146+
<input
147+
type="checkbox"
148+
checked={selectedSystems.includes(i)}
149+
onChange={(e) => setSelectedSystems((sel) => e.target.checked ? [...sel, i] : sel.filter((x) => x !== i))}
150+
/>
151+
<span>
152+
{s.action === "create" ? `新建体系「${s.name}」(核心问题:${s.coreQuestion};入口 ${s.domainEntries.length};初始概念 ${s.concepts.length})`
153+
: `挂接现有体系 #${s.systemId}`}
154+
</span>
155+
</label>
156+
))}
157+
158+
{view.proposal.weeklyContract && (
159+
<>
160+
<SubTitle>周契约建议(可改)</SubTitle>
161+
<div style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 12 }}>
162+
<span>每周</span>
163+
<input type="number" min={1} max={7} value={days} onChange={(e) => setDays(Math.max(1, Math.min(7, Number(e.target.value) || 1)))} style={numStyle} />
164+
<span>天 ·</span>
165+
<input type="number" min={1} max={200} value={cards} onChange={(e) => setCards(Math.max(1, Math.min(200, Number(e.target.value) || 1)))} style={numStyle} />
166+
<span>卡(弹性承诺:断签不清零)</span>
167+
</div>
168+
</>
169+
)}
170+
171+
{err && <p data-testid="plan-error" style={{ fontSize: 11, color: "#dc2626", margin: "6px 0 0" }}>{err}</p>}
172+
</Modal>
167173
);
168174
}
169175

0 commit comments

Comments
 (0)