|
9 | 9 | * @ai-context: 最后落库顺序——先 create_knowledge_system(global),再逐条 |
10 | 10 | * add_knowledge_node(domain_entry),最后第一条输出 add_knowledge_node |
11 | 11 | * (scenario)。三条 invoke 顺序在测试中有断言,勿改顺序。 |
| 12 | + * @ai-context: 批 4 T7 迁移:自建遮罩/面板几何(原 560 px 面板 → `Modal` 的 `l` 档 720, |
| 13 | + * +160)交给 `Modal`(barrel 导入,B5;规格 §5.2「L = 向导/工作台」)—— |
| 14 | + * 遮罩、ESC、焦点陷阱、层级、body 滚动锁都是它的独占职责(ADR-033 §7)。 |
| 15 | + * 开合态仍由父层持有(`KnowledgePage` 的 `wizardOpen` 条件挂载)⇒ `open` 恒为 `true`。 |
| 16 | + * **关闭语义一行未改**:`Modal` 的三条关闭通道(ESC / 点遮罩 / 关闭钮)全部 |
| 17 | + * 走同一个 `onClose` = `doClose`(含 `saving` 挡板与「未创建内容」确认), |
| 18 | + * 底栏「取消」仍直连 `onClose`(原行为:取消不确认)。 |
12 | 19 | */ |
13 | 20 | import { useMemo, useState } from "react"; |
14 | | -import { zIndex } from "../ui/zIndex"; |
| 21 | +import { Modal } from "../ui/primitives"; |
15 | 22 | import { invoke } from "@tauri-apps/api/core"; |
16 | 23 | import { confirm } from "@tauri-apps/plugin-dialog"; |
17 | 24 | import type { KnowledgeSystem, KnowledgeNodeType } from "../types/knowledge"; |
@@ -140,139 +147,125 @@ export default function KnowledgeSystemWizard({ onClose, onCreated }: Props) { |
140 | 147 |
|
141 | 148 | const stepTitle = (i: number) => `第 ${i} 步 · ${STEP_TITLES[i - 1]}`; |
142 | 149 |
|
143 | | - return ( |
144 | | - <div |
145 | | - style={{ |
146 | | - position: "fixed", inset: 0, background: "rgba(0,0,0,0.45)", |
147 | | - display: "flex", alignItems: "center", justifyContent: "center", zIndex: zIndex("modal"), |
148 | | - }} |
149 | | - onClick={doClose} |
150 | | - > |
151 | | - <div |
152 | | - data-testid="knowledge-wizard" |
153 | | - onClick={(e) => e.stopPropagation()} |
154 | | - style={{ width: 560, maxWidth: "92vw", background: "#fff", borderRadius: 10, border: "1px solid #e5e7eb", boxShadow: "0 10px 40px rgba(0,0,0,0.15)", overflow: "hidden" }} |
155 | | - > |
156 | | - {/* 头部:标题 + 关闭 */} |
157 | | - <div style={{ display: "flex", alignItems: "center", padding: "14px 18px", borderBottom: "1px solid #e5e7eb" }}> |
158 | | - <span style={{ fontWeight: 700, fontSize: 15, color: "#0f766e" }}>🧠 创建全局体系</span> |
159 | | - <button data-testid="wizard-close" onClick={() => void doClose()} style={{ marginLeft: "auto", border: "none", background: "none", cursor: "pointer", fontSize: 14, color: "#9ca3af" }} title="关闭"> |
160 | | - ✕ |
161 | | - </button> |
162 | | - </div> |
| 150 | + /** 底部行动区:原自绘底栏三枚按钮**逐字**搬进 `Modal` 的 `footer` 槽。 |
| 151 | + * `flex: 1` 间隔器保留 —— `Modal` 的 footer 是 `justify-content: flex-end`, |
| 152 | + * 没有它「上一步」会被挤到右侧(原布局是「上一步」左置)。 */ |
| 153 | + const footer = ( |
| 154 | + <> |
| 155 | + {step > 1 && ( |
| 156 | + <button data-testid="wizard-back" onClick={back} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #d1d5db", background: "#fff" }}> |
| 157 | + ← 上一步 |
| 158 | + </button> |
| 159 | + )} |
| 160 | + <span style={{ flex: 1 }} /> |
| 161 | + <button onClick={onClose} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #d1d5db", background: "#fff" }}> |
| 162 | + 取消 |
| 163 | + </button> |
| 164 | + {step < 3 ? ( |
| 165 | + <button data-testid="wizard-next" onClick={next} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #0f766e", background: "#f0fdfa", color: "#0f766e" }}> |
| 166 | + 下一步 → |
| 167 | + </button> |
| 168 | + ) : ( |
| 169 | + <button data-testid="wizard-finish" onClick={() => void runCreate()} disabled={saving} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #0f766e", background: "#f0fdfa", color: "#0f766e" }}> |
| 170 | + {saving ? "创建中…" : "✓ 完成,创建体系"} |
| 171 | + </button> |
| 172 | + )} |
| 173 | + </> |
| 174 | + ); |
163 | 175 |
|
164 | | - {/* 步骤指示器(三段) */} |
165 | | - <div style={{ display: "flex", padding: "10px 18px", gap: 6, borderBottom: "1px solid #f3f4f6" }}> |
166 | | - {STEP_TITLES.map((t, i) => { |
167 | | - const n = i + 1; |
168 | | - const active = step === n; |
169 | | - const done = step > n; |
170 | | - return ( |
171 | | - <span key={t} data-testid={`wizard-step-${n}`} style={{ fontSize: 11, padding: "3px 8px", borderRadius: 8, background: active ? "#f0fdfa" : done ? "#ecfdf5" : "#f9fafb", color: active ? "#0f766e" : done ? "#047857" : "#9ca3af", border: active ? "1px solid #14b8a6" : "1px solid #e5e7eb" }}> |
172 | | - {done ? "✓" : n}. {t} |
173 | | - </span> |
174 | | - ); |
175 | | - })} |
176 | | - </div> |
| 176 | + return ( |
| 177 | + <Modal open onClose={doClose} title="创建全局体系" size="l" testId="knowledge-wizard" footer={footer}> |
| 178 | + {/* 自绘头部(标题 + 关闭钮)与原面板几何已删:标题文本交给 `Modal` 的 head, |
| 179 | + 关闭钮由 `${testId}-close` 契约提供;步骤指示器以下内容原样搬入 */} |
| 180 | + {/* 步骤指示器(三段) */} |
| 181 | + <div style={{ display: "flex", padding: "10px 18px", gap: 6, borderBottom: "1px solid #f3f4f6" }}> |
| 182 | + {STEP_TITLES.map((t, i) => { |
| 183 | + const n = i + 1; |
| 184 | + const active = step === n; |
| 185 | + const done = step > n; |
| 186 | + return ( |
| 187 | + <span key={t} data-testid={`wizard-step-${n}`} style={{ fontSize: 11, padding: "3px 8px", borderRadius: 8, background: active ? "#f0fdfa" : done ? "#ecfdf5" : "#f9fafb", color: active ? "#0f766e" : done ? "#047857" : "#9ca3af", border: active ? "1px solid #14b8a6" : "1px solid #e5e7eb" }}> |
| 188 | + {done ? "✓" : n}. {t} |
| 189 | + </span> |
| 190 | + ); |
| 191 | + })} |
| 192 | + </div> |
177 | 193 |
|
178 | | - <div style={{ padding: "16px 18px" }}> |
179 | | - <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>{stepTitle(step)}</div> |
180 | | - <p style={{ fontSize: 12, color: "#6b7280", margin: "0 0 12px", lineHeight: 1.6 }}>{GUIDE[`step${step}`]}</p> |
181 | | - <p data-testid={`wizard-example-${step}`} style={{ fontSize: 11, color: "#0f766e", background: "#f0fdfa", border: "1px dashed #99f6e4", borderRadius: 6, padding: "6px 10px", margin: "0 0 12px" }}> |
182 | | - {EXAMPLES[step]} |
183 | | - </p> |
| 194 | + <div style={{ padding: "16px 18px" }}> |
| 195 | + <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>{stepTitle(step)}</div> |
| 196 | + <p style={{ fontSize: 12, color: "#6b7280", margin: "0 0 12px", lineHeight: 1.6 }}>{GUIDE[`step${step}`]}</p> |
| 197 | + <p data-testid={`wizard-example-${step}`} style={{ fontSize: 11, color: "#0f766e", background: "#f0fdfa", border: "1px dashed #99f6e4", borderRadius: 6, padding: "6px 10px", margin: "0 0 12px" }}> |
| 198 | + {EXAMPLES[step]} |
| 199 | + </p> |
184 | 200 |
|
185 | | - {step === 1 && ( |
186 | | - <div style={{ display: "flex", flexDirection: "column", gap: 10 }}> |
187 | | - <div> |
188 | | - <label style={{ fontSize: 12, fontWeight: 600, display: "block", marginBottom: 4 }}>体系名称(可选)</label> |
189 | | - <input |
190 | | - data-testid="wizard-name" |
191 | | - value={name} |
192 | | - onChange={(e) => setName(e.target.value)} |
193 | | - placeholder="默认:全局体系" |
194 | | - style={{ width: "100%", fontSize: 13, padding: "7px 10px", border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box" }} |
195 | | - /> |
196 | | - </div> |
197 | | - <div> |
198 | | - <label style={{ fontSize: 12, fontWeight: 600, display: "block", marginBottom: 4 }}>核心问题 *</label> |
199 | | - <textarea |
200 | | - data-testid="wizard-core-question" |
201 | | - value={coreQuestion} |
202 | | - onChange={(e) => setCoreQuestion(e.target.value)} |
203 | | - placeholder="一句话写下真正卡住你的那个问题" |
204 | | - rows={3} |
205 | | - style={{ width: "100%", fontSize: 13, padding: "7px 10px", border: "1px solid #e5e7eb", borderRadius: 6, resize: "vertical", boxSizing: "border-box" }} |
206 | | - /> |
207 | | - </div> |
| 201 | + {step === 1 && ( |
| 202 | + <div style={{ display: "flex", flexDirection: "column", gap: 10 }}> |
| 203 | + <div> |
| 204 | + <label style={{ fontSize: 12, fontWeight: 600, display: "block", marginBottom: 4 }}>体系名称(可选)</label> |
| 205 | + <input |
| 206 | + data-testid="wizard-name" |
| 207 | + value={name} |
| 208 | + onChange={(e) => setName(e.target.value)} |
| 209 | + placeholder="默认:全局体系" |
| 210 | + style={{ width: "100%", fontSize: 13, padding: "7px 10px", border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box" }} |
| 211 | + /> |
208 | 212 | </div> |
209 | | - )} |
210 | | - |
211 | | - {step === 2 && ( |
212 | | - <div style={{ display: "flex", flexDirection: "column", gap: 6 }}> |
213 | | - <div style={{ fontSize: 11, color: "#9ca3af", marginBottom: 4 }}>每个入口一行;可全部跳过(空行自动忽略)。</div> |
214 | | - {domainEntries.map((val, i) => ( |
215 | | - <input |
216 | | - key={i} |
217 | | - data-testid={`wizard-domain-${i}`} |
218 | | - value={val} |
219 | | - onChange={(e) => setDomainEntry(i, e.target.value)} |
220 | | - placeholder={`领域入口 ${i + 1}(可空)`} |
221 | | - style={{ width: "100%", fontSize: 13, padding: "6px 10px", border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box" }} |
222 | | - /> |
223 | | - ))} |
224 | | - <button |
225 | | - data-testid="wizard-add-domain" |
226 | | - onClick={() => setDomainEntries((prev) => prev.length >= 5 ? prev : [...prev, ""])} |
227 | | - disabled={domainEntries.length >= 5} |
228 | | - style={{ alignSelf: "flex-start", fontSize: 12, cursor: "pointer", padding: "3px 10px", borderRadius: 4, border: "1px solid #d1d5db", background: "#fff", color: domainEntries.length >= 5 ? "#9ca3af" : "#374151" }} |
229 | | - > |
230 | | - + 加一行 |
231 | | - </button> |
| 213 | + <div> |
| 214 | + <label style={{ fontSize: 12, fontWeight: 600, display: "block", marginBottom: 4 }}>核心问题 *</label> |
| 215 | + <textarea |
| 216 | + data-testid="wizard-core-question" |
| 217 | + value={coreQuestion} |
| 218 | + onChange={(e) => setCoreQuestion(e.target.value)} |
| 219 | + placeholder="一句话写下真正卡住你的那个问题" |
| 220 | + rows={3} |
| 221 | + style={{ width: "100%", fontSize: 13, padding: "7px 10px", border: "1px solid #e5e7eb", borderRadius: 6, resize: "vertical", boxSizing: "border-box" }} |
| 222 | + /> |
232 | 223 | </div> |
233 | | - )} |
| 224 | + </div> |
| 225 | + )} |
234 | 226 |
|
235 | | - {step === 3 && ( |
236 | | - <textarea |
237 | | - data-testid="wizard-first-output" |
238 | | - value={firstOutput} |
239 | | - onChange={(e) => setFirstOutput(e.target.value)} |
240 | | - placeholder="可选:本周要产出的第一个可验证输出" |
241 | | - rows={3} |
242 | | - style={{ width: "100%", fontSize: 13, padding: "7px 10px", border: "1px solid #e5e7eb", borderRadius: 6, resize: "vertical", boxSizing: "border-box" }} |
243 | | - /> |
244 | | - )} |
| 227 | + {step === 2 && ( |
| 228 | + <div style={{ display: "flex", flexDirection: "column", gap: 6 }}> |
| 229 | + <div style={{ fontSize: 11, color: "#9ca3af", marginBottom: 4 }}>每个入口一行;可全部跳过(空行自动忽略)。</div> |
| 230 | + {domainEntries.map((val, i) => ( |
| 231 | + <input |
| 232 | + key={i} |
| 233 | + data-testid={`wizard-domain-${i}`} |
| 234 | + value={val} |
| 235 | + onChange={(e) => setDomainEntry(i, e.target.value)} |
| 236 | + placeholder={`领域入口 ${i + 1}(可空)`} |
| 237 | + style={{ width: "100%", fontSize: 13, padding: "6px 10px", border: "1px solid #e5e7eb", borderRadius: 6, boxSizing: "border-box" }} |
| 238 | + /> |
| 239 | + ))} |
| 240 | + <button |
| 241 | + data-testid="wizard-add-domain" |
| 242 | + onClick={() => setDomainEntries((prev) => prev.length >= 5 ? prev : [...prev, ""])} |
| 243 | + disabled={domainEntries.length >= 5} |
| 244 | + style={{ alignSelf: "flex-start", fontSize: 12, cursor: "pointer", padding: "3px 10px", borderRadius: 4, border: "1px solid #d1d5db", background: "#fff", color: domainEntries.length >= 5 ? "#9ca3af" : "#374151" }} |
| 245 | + > |
| 246 | + + 加一行 |
| 247 | + </button> |
| 248 | + </div> |
| 249 | + )} |
245 | 250 |
|
246 | | - {/* 逐行红色错误提示 */} |
247 | | - {error && ( |
248 | | - <div data-testid="wizard-error" style={{ fontSize: 12, color: "#dc2626", marginTop: 10, background: "#fef2f2", border: "1px solid #fecaca", borderRadius: 6, padding: "6px 10px", lineHeight: 1.5 }}> |
249 | | - {error} |
250 | | - </div> |
251 | | - )} |
252 | | - </div> |
| 251 | + {step === 3 && ( |
| 252 | + <textarea |
| 253 | + data-testid="wizard-first-output" |
| 254 | + value={firstOutput} |
| 255 | + onChange={(e) => setFirstOutput(e.target.value)} |
| 256 | + placeholder="可选:本周要产出的第一个可验证输出" |
| 257 | + rows={3} |
| 258 | + style={{ width: "100%", fontSize: 13, padding: "7px 10px", border: "1px solid #e5e7eb", borderRadius: 6, resize: "vertical", boxSizing: "border-box" }} |
| 259 | + /> |
| 260 | + )} |
253 | 261 |
|
254 | | - {/* 底部:上一步 / 下一步 / 完成 */} |
255 | | - <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "12px 18px", borderTop: "1px solid #e5e7eb", background: "#fafafa" }}> |
256 | | - {step > 1 && ( |
257 | | - <button data-testid="wizard-back" onClick={back} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #d1d5db", background: "#fff" }}> |
258 | | - ← 上一步 |
259 | | - </button> |
260 | | - )} |
261 | | - <span style={{ flex: 1 }} /> |
262 | | - <button onClick={onClose} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #d1d5db", background: "#fff" }}> |
263 | | - 取消 |
264 | | - </button> |
265 | | - {step < 3 ? ( |
266 | | - <button data-testid="wizard-next" onClick={next} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #0f766e", background: "#f0fdfa", color: "#0f766e" }}> |
267 | | - 下一步 → |
268 | | - </button> |
269 | | - ) : ( |
270 | | - <button data-testid="wizard-finish" onClick={() => void runCreate()} disabled={saving} style={{ fontSize: 13, cursor: "pointer", padding: "6px 14px", borderRadius: 6, border: "1px solid #0f766e", background: "#f0fdfa", color: "#0f766e" }}> |
271 | | - {saving ? "创建中…" : "✓ 完成,创建体系"} |
272 | | - </button> |
273 | | - )} |
274 | | - </div> |
| 262 | + {/* 逐行红色错误提示 */} |
| 263 | + {error && ( |
| 264 | + <div data-testid="wizard-error" style={{ fontSize: 12, color: "#dc2626", marginTop: 10, background: "#fef2f2", border: "1px solid #fecaca", borderRadius: 6, padding: "6px 10px", lineHeight: 1.5 }}> |
| 265 | + {error} |
| 266 | + </div> |
| 267 | + )} |
275 | 268 | </div> |
276 | | - </div> |
| 269 | + </Modal> |
277 | 270 | ); |
278 | 271 | } |
0 commit comments