|
8 | 8 | * 清理登记(丢弃项)诚实展示,绝不静默。 |
9 | 9 | */ |
10 | 10 | import { useMemo, useState } from "react"; |
11 | | -import { zIndex } from "../ui/zIndex"; |
| 11 | +import { Modal } from "../ui/primitives"; |
12 | 12 | import type { GoalPlanView } from "../types/goals"; |
13 | 13 |
|
14 | 14 | interface Props { |
@@ -76,94 +76,100 @@ export default function GoalPlanApprovalDialog({ view, onConfirm, onClose, onUse |
76 | 76 | }; |
77 | 77 |
|
78 | 78 | 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 | + <> |
159 | 91 | <button onClick={onUseRules} style={ghostBtn} title="不采纳 AI 建议——回到 M1 规则草案(永远可用的降级基线)">改用规则草案</button> |
160 | 92 | <button onClick={onClose} style={ghostBtn}>再想想</button> |
161 | 93 | <button data-testid="plan-confirm" onClick={() => void confirm()} disabled={saving} style={primaryBtn}> |
162 | 94 | {saving ? "落库中…" : "✓ 确认采用"} |
163 | 95 | </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>} |
164 | 126 | </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> |
167 | 173 | ); |
168 | 174 | } |
169 | 175 |
|
|
0 commit comments