@houser/edus-plugin 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,3 +67,9 @@ npm run build # 清理后依次执行以上两步
67
67
  ```
68
68
 
69
69
  `tsdown.config.ts` 会将浏览器代码包装成 Harness 的 `window.__ModuleLoader__.load(...)` 模块格式,因此不要手动编辑 `lib/client.js`;应始终修改 `src/client/` 下的源码。
70
+
71
+ ## 发布版本
72
+
73
+ 发布版本的时候必须指定 tag,tag 会关联 deepseek harness 版本(一般就是当前分支的名称)
74
+
75
+ 示例 `npm publish --registry=https://registry.npmjs.org --tag dsh-v0.1.6-alpha.2`
package/lib/client.js CHANGED
@@ -6120,6 +6120,364 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6120
6120
  return (0, react.useSyncExternalStore)(EdusAuthStore.subscribe, EdusAuthStore.getSnapshot, () => null);
6121
6121
  }
6122
6122
  //#endregion
6123
+ //#region lib/types/client/EdusLoginPanel.js
6124
+ /**
6125
+ * 阿里云滑动验证码承载页。`clientType=iframe` 时它会把结果
6126
+ * `{ type: 'success' | 'close', data }` postMessage 给父窗口,
6127
+ * 与课成 web 端 `send-sms-code.tsx` 使用的是同一个页面。
6128
+ */
6129
+ const CAPTCHA_ORIGIN = "https://h5.daanjiexi.com";
6130
+ const CAPTCHA_URL = `${CAPTCHA_ORIGIN}/aliyun_captcha_view.html?clientType=iframe`;
6131
+ const PHONE_PATTERN = /^1[3-9]\d{9}$/;
6132
+ const RESEND_SECONDS = 60;
6133
+ const base = {
6134
+ form: {
6135
+ display: "grid",
6136
+ gap: 10
6137
+ },
6138
+ field: {
6139
+ boxSizing: "border-box",
6140
+ width: "100%",
6141
+ border: "1px solid var(--dsw-alias-border-l2)",
6142
+ borderRadius: 8,
6143
+ padding: "10px 11px",
6144
+ color: "var(--dsw-alias-label-primary)",
6145
+ background: "var(--dsw-alias-bg-base)",
6146
+ font: "inherit",
6147
+ fontSize: 13
6148
+ },
6149
+ button: {
6150
+ border: "1px solid var(--dsw-alias-border-l2)",
6151
+ borderRadius: 6,
6152
+ padding: "4px 8px",
6153
+ background: "transparent",
6154
+ color: "var(--dsw-alias-label-primary)",
6155
+ font: "inherit",
6156
+ fontSize: 12,
6157
+ cursor: "pointer"
6158
+ },
6159
+ primary: {
6160
+ border: "none",
6161
+ borderRadius: 7,
6162
+ padding: "6px 11px",
6163
+ background: "var(--dsw-alias-button-primary-fill)",
6164
+ color: "var(--dsw-alias-label-primary-inverted)",
6165
+ font: "inherit",
6166
+ fontSize: 12,
6167
+ fontWeight: 650,
6168
+ cursor: "pointer"
6169
+ },
6170
+ error: {
6171
+ margin: 0,
6172
+ color: "var(--dsw-alias-state-error-primary)",
6173
+ fontSize: 12,
6174
+ lineHeight: 1.45
6175
+ },
6176
+ codeRow: {
6177
+ display: "flex",
6178
+ alignItems: "stretch",
6179
+ gap: 8
6180
+ },
6181
+ sendButton: {
6182
+ flex: "none",
6183
+ whiteSpace: "nowrap",
6184
+ border: "1px solid var(--dsw-alias-border-l2)",
6185
+ borderRadius: 8,
6186
+ padding: "0 12px",
6187
+ background: "transparent",
6188
+ color: "var(--dsw-alias-brand-primary)",
6189
+ font: "inherit",
6190
+ fontSize: 12,
6191
+ cursor: "pointer"
6192
+ },
6193
+ hint: {
6194
+ margin: 0,
6195
+ color: "var(--dsw-alias-label-tertiary)",
6196
+ fontSize: 12,
6197
+ lineHeight: 1.5
6198
+ },
6199
+ actions: {
6200
+ display: "flex",
6201
+ justifyContent: "flex-end",
6202
+ gap: 8,
6203
+ marginTop: 10
6204
+ },
6205
+ switchRow: {
6206
+ marginTop: 10,
6207
+ textAlign: "center"
6208
+ },
6209
+ switchLink: {
6210
+ border: 0,
6211
+ padding: 0,
6212
+ background: "transparent",
6213
+ color: "var(--dsw-alias-brand-primary)",
6214
+ font: "inherit",
6215
+ fontSize: 12,
6216
+ cursor: "pointer"
6217
+ },
6218
+ captchaOverlay: {
6219
+ position: "fixed",
6220
+ inset: 0,
6221
+ zIndex: 1300,
6222
+ display: "grid",
6223
+ placeItems: "center",
6224
+ background: "rgb(9 19 16 / 56%)",
6225
+ backdropFilter: "blur(4px)"
6226
+ },
6227
+ captchaFrame: {
6228
+ width: 330,
6229
+ height: 330,
6230
+ border: "1px solid var(--dsw-alias-border-l2)",
6231
+ borderRadius: 10,
6232
+ background: "#f5f5f5",
6233
+ boxShadow: "0 20px 60px rgb(0 0 0 / 35%)"
6234
+ }
6235
+ };
6236
+ function reason(error) {
6237
+ return error instanceof Error ? error.message : String(error);
6238
+ }
6239
+ /** 发送短信验证码:先过阿里云图形验证码,再请求后端下发短信。 */
6240
+ function SendSmsCode({ phone, remote, onError, onSent, style }) {
6241
+ const [seconds, setSeconds] = (0, react.useState)(0);
6242
+ const [captchaOpen, setCaptchaOpen] = (0, react.useState)(false);
6243
+ const [sending, setSending] = (0, react.useState)(false);
6244
+ const latest = (0, react.useRef)({
6245
+ phone,
6246
+ remote,
6247
+ onError,
6248
+ onSent
6249
+ });
6250
+ latest.current = {
6251
+ phone,
6252
+ remote,
6253
+ onError,
6254
+ onSent
6255
+ };
6256
+ (0, react.useEffect)(() => {
6257
+ if (seconds <= 0) return;
6258
+ const timer = setTimeout(() => setSeconds((value) => value - 1), 1e3);
6259
+ return () => clearTimeout(timer);
6260
+ }, [seconds]);
6261
+ (0, react.useEffect)(() => {
6262
+ if (!captchaOpen) return;
6263
+ const onMessage = (event) => {
6264
+ if (event.origin !== CAPTCHA_ORIGIN) return;
6265
+ const payload = event.data;
6266
+ if (payload === null || typeof payload !== "object") return;
6267
+ if (payload.type === "close") {
6268
+ setCaptchaOpen(false);
6269
+ return;
6270
+ }
6271
+ if (payload.type !== "success" || typeof payload.data !== "string") return;
6272
+ const captcha = payload.data;
6273
+ setCaptchaOpen(false);
6274
+ setSending(true);
6275
+ (async () => {
6276
+ try {
6277
+ await latest.current.remote.sendPhoneCode(latest.current.phone, captcha);
6278
+ latest.current.onSent();
6279
+ setSeconds(RESEND_SECONDS);
6280
+ } catch (error) {
6281
+ latest.current.onError(reason(error));
6282
+ } finally {
6283
+ setSending(false);
6284
+ }
6285
+ })();
6286
+ };
6287
+ window.addEventListener("message", onMessage);
6288
+ return () => window.removeEventListener("message", onMessage);
6289
+ }, [captchaOpen]);
6290
+ const open = (0, react.useCallback)(() => {
6291
+ if (seconds > 0 || sending) return;
6292
+ if (!PHONE_PATTERN.test(phone)) {
6293
+ onError("请输入正确的手机号");
6294
+ return;
6295
+ }
6296
+ onError("");
6297
+ setCaptchaOpen(true);
6298
+ }, [
6299
+ onError,
6300
+ phone,
6301
+ seconds,
6302
+ sending
6303
+ ]);
6304
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
6305
+ type: "button",
6306
+ style: {
6307
+ ...style,
6308
+ cursor: seconds > 0 || sending ? "default" : "pointer"
6309
+ },
6310
+ disabled: seconds > 0 || sending,
6311
+ onClick: open,
6312
+ children: sending ? "发送中…" : seconds > 0 ? `${seconds} 秒后重发` : "发送验证码"
6313
+ }), captchaOpen && (0, react_jsx_runtime.jsx)("div", {
6314
+ style: base.captchaOverlay,
6315
+ role: "presentation",
6316
+ onMouseDown: (event) => {
6317
+ event.stopPropagation();
6318
+ setCaptchaOpen(false);
6319
+ },
6320
+ children: (0, react_jsx_runtime.jsx)("iframe", {
6321
+ title: "图形验证码",
6322
+ src: CAPTCHA_URL,
6323
+ scrolling: "no",
6324
+ style: base.captchaFrame,
6325
+ onMouseDown: (event) => event.stopPropagation()
6326
+ })
6327
+ })] });
6328
+ }
6329
+ /** 手机号密码 / 手机号验证码两种方式共用的登录表单。 */
6330
+ function EdusLoginPanel({ remote, onSuccess, onCancel, onBusyChange, submitLabel = "登录", cancelLabel = "取消", intro, notice = "", defaultMode = "code", theme = {} }) {
6331
+ const [mode, setMode] = (0, react.useState)(defaultMode);
6332
+ const [phone, setPhone] = (0, react.useState)("");
6333
+ const [password, setPassword] = (0, react.useState)("");
6334
+ const [code, setCode] = (0, react.useState)("");
6335
+ const [busy, setBusy] = (0, react.useState)(false);
6336
+ const [error, setError] = (0, react.useState)("");
6337
+ const [tip, setTip] = (0, react.useState)("");
6338
+ const field = {
6339
+ ...base.field,
6340
+ ...theme.field
6341
+ };
6342
+ const button = {
6343
+ ...base.button,
6344
+ ...theme.button
6345
+ };
6346
+ const primary = {
6347
+ ...base.primary,
6348
+ ...theme.primary
6349
+ };
6350
+ const errorStyle = {
6351
+ ...base.error,
6352
+ ...theme.error
6353
+ };
6354
+ (0, react.useEffect)(() => {
6355
+ onBusyChange?.(busy);
6356
+ }, [busy, onBusyChange]);
6357
+ const switchMode = (next) => {
6358
+ setMode(next);
6359
+ setError("");
6360
+ setTip("");
6361
+ };
6362
+ const notifyError = (0, react.useCallback)((message) => {
6363
+ setError(message);
6364
+ if (message !== "") setTip("");
6365
+ }, []);
6366
+ const notifySent = (0, react.useCallback)(() => {
6367
+ setError("");
6368
+ setTip("验证码已发送,请注意查收短信。");
6369
+ }, []);
6370
+ const submit = async (event) => {
6371
+ event.preventDefault();
6372
+ if (!PHONE_PATTERN.test(phone)) {
6373
+ setError("请输入正确的手机号");
6374
+ return;
6375
+ }
6376
+ if (mode === "password" && password === "") {
6377
+ setError("请输入密码");
6378
+ return;
6379
+ }
6380
+ if (mode === "code" && code.trim() === "") {
6381
+ setError("请输入短信验证码");
6382
+ return;
6383
+ }
6384
+ setBusy(true);
6385
+ setError("");
6386
+ setTip("");
6387
+ try {
6388
+ await onSuccess(mode === "password" ? await remote.login(phone, password) : await remote.loginByPhone(phone, code.trim()));
6389
+ } catch (failure) {
6390
+ setError(reason(failure));
6391
+ } finally {
6392
+ setBusy(false);
6393
+ }
6394
+ };
6395
+ const message = error !== "" ? error : notice;
6396
+ return (0, react_jsx_runtime.jsxs)("form", {
6397
+ style: base.form,
6398
+ onSubmit: (event) => void submit(event),
6399
+ children: [
6400
+ intro,
6401
+ (0, react_jsx_runtime.jsx)("input", {
6402
+ style: field,
6403
+ autoComplete: "tel",
6404
+ inputMode: "numeric",
6405
+ maxLength: 11,
6406
+ placeholder: "手机号",
6407
+ value: phone,
6408
+ onChange: (event) => setPhone(event.target.value.replace(/\D/g, ""))
6409
+ }),
6410
+ mode === "password" ? (0, react_jsx_runtime.jsx)("input", {
6411
+ style: field,
6412
+ autoComplete: "current-password",
6413
+ type: "password",
6414
+ placeholder: "密码",
6415
+ value: password,
6416
+ onChange: (event) => setPassword(event.target.value)
6417
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
6418
+ style: base.codeRow,
6419
+ children: [(0, react_jsx_runtime.jsx)("input", {
6420
+ style: {
6421
+ ...field,
6422
+ flex: 1,
6423
+ minWidth: 0
6424
+ },
6425
+ autoComplete: "one-time-code",
6426
+ inputMode: "numeric",
6427
+ placeholder: "短信验证码",
6428
+ value: code,
6429
+ onChange: (event) => setCode(event.target.value)
6430
+ }), (0, react_jsx_runtime.jsx)(SendSmsCode, {
6431
+ phone,
6432
+ remote,
6433
+ onError: notifyError,
6434
+ onSent: notifySent,
6435
+ style: base.sendButton
6436
+ })]
6437
+ }), (0, react_jsx_runtime.jsx)("p", {
6438
+ style: base.hint,
6439
+ children: "首次登录将自动注册课成账号。"
6440
+ })] }),
6441
+ tip !== "" && (0, react_jsx_runtime.jsx)("p", {
6442
+ style: {
6443
+ ...base.hint,
6444
+ color: "var(--dsw-alias-brand-primary)"
6445
+ },
6446
+ role: "status",
6447
+ children: tip
6448
+ }),
6449
+ message !== "" && (0, react_jsx_runtime.jsx)("p", {
6450
+ style: errorStyle,
6451
+ children: message
6452
+ }),
6453
+ (0, react_jsx_runtime.jsxs)("div", {
6454
+ style: base.actions,
6455
+ children: [onCancel !== void 0 && (0, react_jsx_runtime.jsx)("button", {
6456
+ type: "button",
6457
+ style: button,
6458
+ disabled: busy,
6459
+ onClick: onCancel,
6460
+ children: cancelLabel
6461
+ }), (0, react_jsx_runtime.jsx)("button", {
6462
+ type: "submit",
6463
+ style: primary,
6464
+ disabled: busy,
6465
+ children: busy ? "登录中…" : submitLabel
6466
+ })]
6467
+ }),
6468
+ (0, react_jsx_runtime.jsx)("div", {
6469
+ style: base.switchRow,
6470
+ children: (0, react_jsx_runtime.jsx)("button", {
6471
+ type: "button",
6472
+ style: base.switchLink,
6473
+ onClick: () => switchMode(mode === "password" ? "code" : "password"),
6474
+ children: mode === "password" ? "使用短信验证码登录" : "使用密码登录"
6475
+ })
6476
+ })
6477
+ ]
6478
+ });
6479
+ }
6480
+ //#endregion
6123
6481
  //#region lib/types/client/HtmlPublishPreview.js
6124
6482
  function fileRef(address) {
6125
6483
  if (!address.startsWith("dsh-resource://file/session/")) return null;
@@ -6243,10 +6601,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6243
6601
  font: "inherit",
6244
6602
  fontSize: 13
6245
6603
  },
6246
- form: {
6247
- display: "grid",
6248
- gap: 10
6249
- },
6250
6604
  option: {
6251
6605
  display: "flex",
6252
6606
  alignItems: "flex-start",
@@ -6331,8 +6685,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6331
6685
  const [stage, setStage] = (0, react.useState)("closed");
6332
6686
  const [destination, setDestination] = (0, react.useState)("new");
6333
6687
  const [title, setTitle] = (0, react.useState)("");
6334
- const [phone, setPhone] = (0, react.useState)("");
6335
- const [password, setPassword] = (0, react.useState)("");
6336
6688
  const [busy, setBusy] = (0, react.useState)(false);
6337
6689
  const [error, setError] = (0, react.useState)("");
6338
6690
  const [shareUrl, setShareUrl] = (0, react.useState)(null);
@@ -6372,21 +6724,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6372
6724
  const close = () => {
6373
6725
  if (!busy) setStage("closed");
6374
6726
  };
6375
- const submitLogin = async (event) => {
6376
- event.preventDefault();
6377
- setBusy(true);
6727
+ const afterLogin = (0, react.useCallback)((result) => {
6728
+ EdusAuthStore.set(result);
6378
6729
  setError("");
6379
- try {
6380
- const result = await remote.login(phone, password);
6381
- EdusAuthStore.set(result);
6382
- setDestination(marker !== null && marker.owner === result.phone ? "current" : "new");
6383
- setStage("destination");
6384
- } catch (reason) {
6385
- setError(reason instanceof Error ? reason.message : String(reason));
6386
- } finally {
6387
- setBusy(false);
6388
- }
6389
- };
6730
+ setDestination(marker !== null && marker.owner === result.phone ? "current" : "new");
6731
+ setStage("destination");
6732
+ }, [marker]);
6390
6733
  const submitPublish = (0, react.useCallback)(async () => {
6391
6734
  if (ref === null || account === null) return;
6392
6735
  const newWork = destination === "new";
@@ -6472,50 +6815,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6472
6815
  style: styles$1.copy,
6473
6816
  children: "正在通过课成账号服务确认当前登录状态…"
6474
6817
  }),
6475
- stage === "login" && (0, react_jsx_runtime.jsxs)("form", {
6476
- style: styles$1.form,
6477
- onSubmit: (event) => void submitLogin(event),
6478
- children: [
6479
- (0, react_jsx_runtime.jsx)("p", {
6480
- style: styles$1.copy,
6481
- children: "登录后即可选择更新已关联作品,或创建一个新的作品发布。"
6482
- }),
6483
- (0, react_jsx_runtime.jsx)("input", {
6484
- style: styles$1.field,
6485
- required: true,
6486
- autoComplete: "tel",
6487
- placeholder: "手机号",
6488
- value: phone,
6489
- onChange: (event) => setPhone(event.target.value)
6490
- }),
6491
- (0, react_jsx_runtime.jsx)("input", {
6492
- style: styles$1.field,
6493
- required: true,
6494
- autoComplete: "current-password",
6495
- type: "password",
6496
- placeholder: "密码",
6497
- value: password,
6498
- onChange: (event) => setPassword(event.target.value)
6499
- }),
6500
- error !== "" && (0, react_jsx_runtime.jsx)("p", {
6501
- style: styles$1.error,
6502
- children: error
6503
- }),
6504
- (0, react_jsx_runtime.jsxs)("div", {
6505
- style: styles$1.actions,
6506
- children: [(0, react_jsx_runtime.jsx)("button", {
6507
- style: styles$1.button,
6508
- type: "button",
6509
- onClick: close,
6510
- children: "取消"
6511
- }), (0, react_jsx_runtime.jsx)("button", {
6512
- style: styles$1.primary,
6513
- disabled: busy,
6514
- type: "submit",
6515
- children: busy ? "登录中…" : "继续"
6516
- })]
6517
- })
6518
- ]
6818
+ stage === "login" && (0, react_jsx_runtime.jsx)(EdusLoginPanel, {
6819
+ remote,
6820
+ intro: (0, react_jsx_runtime.jsx)("p", {
6821
+ style: styles$1.copy,
6822
+ children: "登录后即可选择更新已关联作品,或创建一个新的作品发布。"
6823
+ }),
6824
+ notice: error,
6825
+ submitLabel: "继续",
6826
+ onCancel: close,
6827
+ onBusyChange: setBusy,
6828
+ onSuccess: afterLogin,
6829
+ theme: {
6830
+ field: styles$1.field,
6831
+ button: styles$1.button,
6832
+ primary: styles$1.primary,
6833
+ error: styles$1.error
6834
+ }
6519
6835
  }),
6520
6836
  stage === "destination" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6521
6837
  account !== null && (0, react_jsx_runtime.jsxs)("p", {
@@ -6709,6 +7025,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6709
7025
  rx: "1.5"
6710
7026
  }), (0, react_jsx_runtime.jsx)("path", { d: "M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2" })] }),
6711
7027
  folder: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("path", { d: "M1.8 4.5h4l1.3 1.7h7.1v6.3a1.4 1.4 0 0 1-1.4 1.4H3.2a1.4 1.4 0 0 1-1.4-1.4z" }), (0, react_jsx_runtime.jsx)("path", { d: "M1.8 4.5V3.8a1.3 1.3 0 0 1 1.3-1.3h3l1.4 2h5.3a1.4 1.4 0 0 1 1.4 1.4v.3" })] }),
7028
+ logout: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7029
+ (0, react_jsx_runtime.jsx)("path", { d: "M6 2.5H3.7a1.2 1.2 0 0 0-1.2 1.2v8.6a1.2 1.2 0 0 0 1.2 1.2H6" }),
7030
+ (0, react_jsx_runtime.jsx)("path", { d: "M10.3 11.2L13.5 8l-3.2-3.2" }),
7031
+ (0, react_jsx_runtime.jsx)("path", { d: "M13.5 8H6" })
7032
+ ] }),
6712
7033
  open: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("path", { d: "M9.5 2.5h4v4M13.2 2.8L7.5 8.5" }), (0, react_jsx_runtime.jsx)("path", { d: "M7 4H3.5a1 1 0 0 0-1 1v7.5a1 1 0 0 0 1 1H11a1 1 0 0 0 1-1V9" })] }),
6713
7034
  refresh: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("path", { d: "M13.2 5.4A5.8 5.8 0 1 0 13 11" }), (0, react_jsx_runtime.jsx)("path", { d: "M13.2 2.6v2.9h-2.9" })] }),
6714
7035
  search: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("circle", {
@@ -6814,10 +7135,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6814
7135
  fontSize: 13,
6815
7136
  lineHeight: 1.5
6816
7137
  },
7138
+ subtitleRow: {
7139
+ display: "flex",
7140
+ alignItems: "center",
7141
+ gap: 12
7142
+ },
6817
7143
  close: {
6818
7144
  width: 32,
6819
7145
  height: 32,
6820
- marginLeft: "auto",
6821
7146
  display: "grid",
6822
7147
  placeItems: "center",
6823
7148
  flex: "none",
@@ -7002,6 +7327,38 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7002
7327
  marginLeft: "auto",
7003
7328
  color: "var(--dsw-alias-brand-primary)",
7004
7329
  fontSize: 12
7330
+ },
7331
+ quiet: {
7332
+ display: "inline-flex",
7333
+ alignItems: "center",
7334
+ gap: 4,
7335
+ flex: "none",
7336
+ border: 0,
7337
+ padding: 0,
7338
+ background: "transparent",
7339
+ color: "var(--dsw-alias-label-tertiary)",
7340
+ font: "inherit",
7341
+ fontSize: 12,
7342
+ lineHeight: 1,
7343
+ cursor: "pointer"
7344
+ },
7345
+ confirmRow: {
7346
+ display: "inline-flex",
7347
+ alignItems: "center",
7348
+ gap: 6,
7349
+ flex: "none",
7350
+ color: "#ff9800",
7351
+ fontSize: 12
7352
+ },
7353
+ danger: {
7354
+ border: 0,
7355
+ padding: "0 4px",
7356
+ background: "transparent",
7357
+ color: "var(--dsw-alias-state-error-primary)",
7358
+ font: "inherit",
7359
+ fontSize: 12,
7360
+ fontWeight: 650,
7361
+ cursor: "pointer"
7005
7362
  }
7006
7363
  };
7007
7364
  function formatDate(value) {
@@ -7056,12 +7413,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7056
7413
  const [total, setTotal] = (0, react.useState)(0);
7057
7414
  const [searchInput, setSearchInput] = (0, react.useState)("");
7058
7415
  const [search, setSearch] = (0, react.useState)("");
7059
- const [phone, setPhone] = (0, react.useState)("");
7060
- const [password, setPassword] = (0, react.useState)("");
7061
7416
  const [loading, setLoading] = (0, react.useState)(false);
7062
7417
  const [sharing, setSharing] = (0, react.useState)(null);
7063
7418
  const [error, setError] = (0, react.useState)("");
7064
7419
  const [notice, setNotice] = (0, react.useState)("");
7420
+ const [confirmLogout, setConfirmLogout] = (0, react.useState)(false);
7065
7421
  const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
7066
7422
  const load = (0, react.useCallback)(async (token, nextPage, nextSearch) => {
7067
7423
  setLoading(true);
@@ -7079,20 +7435,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7079
7435
  setLoading(false);
7080
7436
  }
7081
7437
  }, [remote]);
7082
- const login = async (event) => {
7083
- event.preventDefault();
7084
- setLoading(true);
7438
+ const afterLogin = (0, react.useCallback)(async (result) => {
7439
+ EdusAuthStore.set(result);
7085
7440
  setError("");
7086
- try {
7087
- const result = await remote.login(phone, password);
7088
- EdusAuthStore.set(result);
7089
- setPassword("");
7090
- await load(result.token, 1, "");
7091
- } catch (reason) {
7092
- setError(reason instanceof Error ? reason.message : String(reason));
7093
- setLoading(false);
7094
- }
7095
- };
7441
+ await load(result.token, 1, "");
7442
+ }, [load]);
7443
+ /** 退出后弹窗停留在登录面板,因此顺手把上一个账号的列表状态清干净。 */
7444
+ const logout = (0, react.useCallback)(() => {
7445
+ setConfirmLogout(false);
7446
+ EdusAuthStore.logout();
7447
+ setList([]);
7448
+ setTotal(0);
7449
+ setPage(1);
7450
+ setSearch("");
7451
+ setSearchInput("");
7452
+ setError("");
7453
+ setNotice("");
7454
+ setLoading(false);
7455
+ }, []);
7096
7456
  const share = async (work, action) => {
7097
7457
  if (account === null) return;
7098
7458
  const popup = action === "open" ? window.open("about:blank", "_blank") : null;
@@ -7120,6 +7480,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7120
7480
  if (!open) return;
7121
7481
  setError("");
7122
7482
  setNotice("");
7483
+ setConfirmLogout(false);
7123
7484
  const stored = EdusAuthStore.getSnapshot();
7124
7485
  if (stored === null) setLoading(false);
7125
7486
  if (stored !== null) {
@@ -7157,14 +7518,57 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7157
7518
  children: [
7158
7519
  (0, react_jsx_runtime.jsxs)("header", {
7159
7520
  style: styles.header,
7160
- children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", {
7161
- id: "edus-my-works-title",
7162
- style: styles.title,
7163
- children: "我的作品"
7164
- }), (0, react_jsx_runtime.jsx)("p", {
7165
- style: styles.subtitle,
7166
- children: account === null ? "登录课成账号,查看你发布过的网页作品。" : `${account.title || account.phone} · 共 ${total} 个作品`
7167
- })] }), (0, react_jsx_runtime.jsx)("button", {
7521
+ children: [(0, react_jsx_runtime.jsxs)("div", {
7522
+ style: {
7523
+ minWidth: 0,
7524
+ flex: 1
7525
+ },
7526
+ children: [(0, react_jsx_runtime.jsx)("h2", {
7527
+ id: "edus-my-works-title",
7528
+ style: styles.title,
7529
+ children: "我的作品"
7530
+ }), (0, react_jsx_runtime.jsxs)("p", {
7531
+ style: {
7532
+ ...styles.subtitle,
7533
+ ...styles.subtitleRow
7534
+ },
7535
+ children: [(0, react_jsx_runtime.jsx)("span", {
7536
+ style: {
7537
+ minWidth: 0,
7538
+ overflow: "hidden",
7539
+ textOverflow: "ellipsis",
7540
+ whiteSpace: "nowrap"
7541
+ },
7542
+ children: account === null ? "登录课成账号,查看你发布过的网页作品。" : `${account.title || account.phone} · 共 ${total} 个作品`
7543
+ }), account !== null && (confirmLogout ? (0, react_jsx_runtime.jsxs)("span", {
7544
+ style: styles.confirmRow,
7545
+ children: [
7546
+ (0, react_jsx_runtime.jsx)("span", { children: "确定退出登录?" }),
7547
+ (0, react_jsx_runtime.jsx)("button", {
7548
+ type: "button",
7549
+ style: styles.danger,
7550
+ onClick: logout,
7551
+ children: "退出"
7552
+ }),
7553
+ (0, react_jsx_runtime.jsx)("button", {
7554
+ type: "button",
7555
+ style: styles.quiet,
7556
+ onClick: () => setConfirmLogout(false),
7557
+ children: "取消"
7558
+ })
7559
+ ]
7560
+ }) : (0, react_jsx_runtime.jsxs)("button", {
7561
+ type: "button",
7562
+ title: "退出登录",
7563
+ style: { ...styles.quiet },
7564
+ onClick: () => setConfirmLogout(true),
7565
+ children: [(0, react_jsx_runtime.jsx)(Icon, {
7566
+ name: "logout",
7567
+ size: 13
7568
+ }), "退出登录"]
7569
+ }))]
7570
+ })]
7571
+ }), (0, react_jsx_runtime.jsx)("button", {
7168
7572
  type: "button",
7169
7573
  "aria-label": "关闭我的作品",
7170
7574
  style: styles.close,
@@ -7212,47 +7616,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7212
7616
  }),
7213
7617
  (0, react_jsx_runtime.jsxs)("div", {
7214
7618
  style: styles.body,
7215
- children: [account === null ? (0, react_jsx_runtime.jsxs)("form", {
7619
+ children: [account === null ? (0, react_jsx_runtime.jsx)("div", {
7216
7620
  style: styles.login,
7217
- onSubmit: (event) => void login(event),
7218
- children: [
7219
- (0, react_jsx_runtime.jsx)("strong", { children: "登录课成账号" }),
7220
- (0, react_jsx_runtime.jsx)("span", {
7221
- style: styles.subtitle,
7222
- children: "作品列表仅展示当前账号创建的内容。"
7223
- }),
7224
- (0, react_jsx_runtime.jsx)("input", {
7225
- style: styles.field,
7226
- required: true,
7227
- autoComplete: "tel",
7228
- placeholder: "手机号",
7229
- value: phone,
7230
- onChange: (event) => setPhone(event.target.value)
7231
- }),
7232
- (0, react_jsx_runtime.jsx)("input", {
7233
- style: styles.field,
7234
- required: true,
7235
- autoComplete: "current-password",
7236
- type: "password",
7237
- placeholder: "密码",
7238
- value: password,
7239
- onChange: (event) => setPassword(event.target.value)
7240
- }),
7241
- error !== "" && (0, react_jsx_runtime.jsx)("p", {
7242
- style: styles.error,
7243
- children: error
7244
- }),
7245
- (0, react_jsx_runtime.jsx)("button", {
7246
- type: "submit",
7621
+ children: (0, react_jsx_runtime.jsx)(EdusLoginPanel, {
7622
+ remote,
7623
+ intro: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("strong", { children: "登录课成账号" }), (0, react_jsx_runtime.jsx)("p", {
7247
7624
  style: {
7625
+ ...styles.subtitle,
7626
+ margin: "5px 0 0"
7627
+ },
7628
+ children: "作品列表仅展示当前账号创建的内容。"
7629
+ })] }),
7630
+ notice: error,
7631
+ submitLabel: "登录并查看作品",
7632
+ onSuccess: afterLogin,
7633
+ theme: {
7634
+ field: styles.field,
7635
+ button: styles.button,
7636
+ primary: {
7248
7637
  ...styles.button,
7249
7638
  ...styles.primary,
7250
7639
  height: 40
7251
7640
  },
7252
- disabled: loading,
7253
- children: loading ? "登录中…" : "登录并查看作品"
7254
- })
7255
- ]
7641
+ error: styles.error
7642
+ }
7643
+ })
7256
7644
  }) : loading ? (0, react_jsx_runtime.jsx)("div", {
7257
7645
  style: styles.center,
7258
7646
  children: "正在读取作品列表…"
@@ -7386,8 +7774,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7386
7774
  */
7387
7775
  async function apply(ctx) {
7388
7776
  await ctx.remote.$mount(TYPERT_REMOTE);
7777
+ applyHideChangedFiles(ctx);
7389
7778
  applyEdusRemoteFeatures(ctx);
7390
7779
  }
7780
+ /**
7781
+ * Hide the automatic changed-files card; the teaching audience reads a diff as noise.
7782
+ *
7783
+ * 不能改为禁用 host 的 workspace-changes 插件:ui-deliverables 的 host 入口
7784
+ * inject 了 workspaceChanges,服务缺失会让它整个不激活,连带 present 交付卡片
7785
+ * 依赖的 /api/present.host、/api/present.open 路由一起消失,界面上会出现
7786
+ * 「无法读取主机桌面信息」。所以保留 host 记录,只在客户端隐藏这张卡片。
7787
+ *
7788
+ * `data-changed-files` 是卡片根元素上的稳定标记;同包的 CSS module 类名带哈希,
7789
+ * 不能作为选择器。
7790
+ */
7791
+ function applyHideChangedFiles(ctx) {
7792
+ ctx.effect(() => {
7793
+ const style = document.createElement("style");
7794
+ style.dataset["edus"] = "hide-changed-files";
7795
+ style.textContent = "[data-changed-files]{display:none!important}";
7796
+ document.head.append(style);
7797
+ return () => {
7798
+ style.remove();
7799
+ };
7800
+ }, "edus changed-files card suppression");
7801
+ }
7391
7802
  function applyEdusRemoteFeatures(ctx) {
7392
7803
  const bridge = ctx.get("remote.edus");
7393
7804
  if (bridge === void 0) throw new Error("edus Remote namespace was not mounted");
@@ -7398,6 +7809,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7398
7809
  unwrap(await bridge.writeHtmlFile(sessionId, path, content));
7399
7810
  },
7400
7811
  login: async (phone, password) => unwrap(await bridge.login(phone, password)),
7812
+ loginByPhone: async (phone, code) => unwrap(await bridge.loginByPhone(phone, code)),
7813
+ sendPhoneCode: async (phone, captcha) => {
7814
+ unwrap(await bridge.sendPhoneCode(phone, captcha));
7815
+ },
7401
7816
  loginInfo: async (token) => unwrap(await bridge.loginInfo(token)),
7402
7817
  htmlList: async (token, page, size, search) => unwrap(await bridge.htmlList(token, page, size, search)),
7403
7818
  htmlShare: async (token, id) => unwrap(await bridge.htmlShare(token, id)),
package/lib/index.js CHANGED
@@ -629,35 +629,153 @@ const WEBPAGE_INSTRUCTION = `# 网页生成规则
629
629
 
630
630
  当用户要求创建或修改网页时,必须遵守以下规则。
631
631
 
632
- ## 文件结构
632
+ ## 使用场景
633
633
 
634
- - 最终网页必须是一个自包含的 \`.html\` 文件。
635
- - 不得将 HTML、CSS、JavaScript 拆分为多个本地文件。
636
- - CSS 必须写在 HTML 的 \`<style>\` 中。
637
- - JavaScript 必须写在 HTML 的 \`<script>\` 中。
638
- - 禁止引用 \`./xxx.css\`、\`./xxx.js\` 等本地相对资源。
634
+ 生成的网页用于课堂教学,通常由老师在教室里通过投影仪或大屏投屏展示,后排学生距离屏幕 5-10 米,教室环境光较亮、投影对比度偏低。
639
635
 
640
- ## 图片和外部资源
636
+ 因此:**看得清** 比 **好看** 重要,**信息少而大** 比 **排得满** 重要。所有排版决策都要以「最后一排学生能否看清」为准。
641
637
 
642
- - 禁止将图片编码为 \`data:\` URI。
643
- - 用户明确要求使用当前对话中提供的图片时,必须调用 \`publish_image\` 获取可访问的绝对 URL,再将该 URL 写入 \`<img src="...">\` 等位置。
644
- - 已有的外部图片绝对 URL 可以直接使用。
645
- - 字体、图标等外部资源必须使用可公开访问的绝对 URL,例如 CDN。
646
- - 不得引用工作区中的本地资源文件。
638
+ ## 开工前的确认
647
639
 
648
- ## 原因
640
+ 需求不够具体时,先调用 \`ask_user_question\` 一次性问清楚,再动手写页面。不要凭空假设,也不要边写边问。
649
641
 
650
- 发布系统以单个 HTML 字符串存储网页,并通过不存在同级资源目录的分享链接提供页面。因此,本地相对路径可能在工作区预览时正常,但发布后无法访问。
642
+ - 何时问:用户只给了一个宽泛主题(如「讲一下光合作用」「做个二次函数的网页」),缺少学段、课型或讲解重点等关键信息时才问。
643
+ - 何时不问:需求已经描述清楚、或用户是在修改已有网页,直接开工,不要提问。
644
+ - 一次最多问 3 个问题,并在同一次 \`ask_user_question\` 调用里问完,不要分多轮打断老师。
645
+ - 每个问题给 2-4 个具体选项:最可能的放第一个并在标签末尾加「(推荐)」,每个选项配一句话说明区别。\`header\` 用「学段」「课型」这类简短中文词。
646
+ - 问教学问题,用老师的语言。可以问:面向哪个学段年级、这页用于新课讲解还是复习巩固、重点讲哪个环节、要不要配练习题。
647
+ - 不要问技术问题:配色、字体、动画实现方式、是否引入公式库等一律按本规则自行决定,不许拿来问用户。
648
+ - 拿到回答后直接按回答生成,不要再确认一遍。用户跳过问题时,按最常见的课堂情况取默认值继续做,不要追问。
649
+
650
+ ## 排版与字体
651
+
652
+ - 正文字号不小于 \`22px\`,推荐 \`24-28px\`;图注、标签、表格等次要文字不小于 \`18px\`,不得出现小于 \`16px\` 的任何文字。
653
+ - 标题要有明显层级差:主标题 \`48-64px\`,章节标题 \`36-44px\`,小节标题 \`28-32px\`。
654
+ - 字重要足:正文用 \`500\`,标题用 \`700\` 及以上。禁止使用 \`100/200/300\` 等细字重,禁止给中文设 \`font-weight: 300\` 或 \`lighter\`。
655
+ - 字体优先系统中文黑体栈,例如 \`font-family: "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", sans-serif\`;不要使用细体、衬线体或手写体做正文。
656
+ - 行高 \`1.7-1.9\`,段落之间留足空白;正文容器宽度控制在 \`1100-1300px\` 居中,一行不要太长。
657
+ - 交互控件(按钮、滑块、输入框)要大:按钮高度不小于 \`48px\`,文字不小于 \`20px\`,便于老师在讲台上快速点击。
658
+ - 公式、代码、图表标注同样要放大,不要沿用默认小字号。
659
+ - 按 1920×1080 投屏比例设计;首屏至少要能完整看清标题和本页要讲的问题。页面可以滚动,但不要把关键内容塞进需要眯眼看的角落。
660
+
661
+ ## 配色
662
+
663
+ 课堂网页要有鲜明的视觉主题,**不要做成黑白灰的纯文档**。配色的目标是帮助学生快速区分内容结构,不是为了克制。
664
+
665
+ - 先定一套配色再写页面:1 个主色 + 1 个辅助色 + 1-2 个强调色,再配中性底色(浅米、浅灰蓝等,不必是纯白)。可参考「深蓝 + 橙」「墨绿 + 暖黄」「深紫 + 青」这类有明确冷暖对比的组合,避免整页同一色相的浅色堆叠。
666
+ - 颜色用在大面积元素上:分区背景、标题条、卡片、图示、表头、边框、图标、进度条、重点底纹。正文文字本身保持深色高对比,不要靠给文字上色来做视觉效果。
667
+ - 用颜色表达结构:不同章节可用不同主题色;定义、例题、重点、易错点、小结用不同颜色的卡片区分,让学生一眼看出「这是哪一类内容」。
668
+ - 允许渐变、色块、圆角卡片、阴影、大号图标和示意图,只要不压低文字清晰度。
669
+ - 投影会让颜色整体变浅、对比度下降,所以配色要够饱和:避免大面积浅粉、浅黄、浅灰蓝这类投在墙上几乎分不出层次的颜色,也避免相邻区块颜色过于接近。
670
+ - 文字对比度底线:浅底深字时正文不浅于 \`#222\`、次要文字不浅于 \`#555\`;深底浅字时正文不深于 \`#EEE\`。彩色底上放文字时,用深色底配白字或很浅的底配深色字,不要中等明度底配中等明度字。禁止 \`#999\` 及更浅的颜色承载正文,禁止半透明文字,禁止彩色细字重小字。
671
+
672
+ ## 动画与演示
673
+
674
+ 课堂网页相比课件的最大价值,是**把抽象过程变成看得见的动态过程**。不要只做静态图文,每页至少给核心知识点配一个能看到「过程」的动画。
675
+
676
+ - 优先做讲得清知识点的动画:受力箭头随条件变化、函数图像随参数移动、分子重组与反应过程、几何图形的平移旋转翻折、光路与波形、数据或电流的流动、公式分步推导逐步高亮。
677
+ - 实现方式优先 CSS \`transition\`/\`animation\` 与 SVG 动画(\`stroke-dasharray\` 画线、\`transform\` 变换),复杂连续运动再用 \`requestAnimationFrame\` 驱动 \`<canvas>\`。
678
+ - **老师要能控制节奏**:关键演示必须配「播放 / 暂停 / 单步 / 重播」按钮,不能只自动播一遍就结束——老师需要边讲边停、反复演示。
679
+ - 分步推导、解题过程、实验步骤用「下一步」按钮逐步展开,并高亮当前步骤,不要一次性铺满整屏。
680
+ - 动画节奏要慢:过渡时长 \`400-800ms\`,关键步骤之间停顿 1 秒以上,让后排学生跟得上;不要用 \`150ms\` 的快闪。
681
+ - 动画元素要大:示意图尺寸足够占据版面,线条和箭头不细于 \`3px\`,运动幅度明显,关键部位配大号文字标注。
682
+ - 可以用 \`IntersectionObserver\` 做滚动入场动画,但必须保证 JS 失效时内容仍然可见,不要让元素永久停在透明状态。
683
+ - **禁止纯装饰性动画**:粒子背景、浮动光斑、循环闪烁、打字机效果、跑马灯、无意义的悬浮缩放。课堂上这些只会分散注意力。动画必须服务于讲清楚某个知识点。
684
+ - 注意与「默认规模」的区别:自动播放的动画、过渡效果、分步高亮不计入交互演示数量限制,可以放心多用;受限制的是需要学生自己调参操作的复杂交互组件。
685
+
686
+ ## 公式与符号
687
+
688
+ 页面涉及数学、物理、化学时,必须保证符号能正确渲染,不能出现乱码、错位或被当成普通文本的 \`\\frac\`。
689
+
690
+ - \`<head>\` 里必须有 \`<meta charset="utf-8">\`。
691
+ - **简单符号直接用 Unicode 和 HTML 标签**,不要为一个下标引入整个公式引擎:上下标用 \`<sub>\`/\`<sup>\`(H<sub>2</sub>O、v<sup>2</sup>),希腊字母、\`× ÷ ± ≈ ≠ ≤ ≥ ∈ √ ∑ ∫ ° ℃ ⇌ →\` 等直接写字符。
692
+ - **涉及分式、根式、矩阵、极限、积分、化学方程式配平等复杂排版时,引入 MathJax 3(CDN)**,并同时开启 mhchem 以支持化学式:
693
+
694
+ \`\`\`html
695
+ <script>
696
+ window.MathJax = {
697
+ loader: { load: ['[tex]/mhchem'] },
698
+ tex: {
699
+ packages: { '[+]': ['mhchem'] },
700
+ inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
701
+ displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
702
+ },
703
+ options: { skipHtmlTags: ['script', 'style', 'textarea', 'pre'] },
704
+ }
705
+ <\/script>
706
+ <script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"><\/script>
707
+ \`\`\`
708
+
709
+ - MathJax 默认不解析 \`$...$\` 行内公式,务必按上面的配置显式声明 \`inlineMath\`,否则公式会原样显示成源码。
710
+ - 化学式用 mhchem 语法写在公式里,例如 \`$\\ce{2H2 + O2 -> 2H2O}$\`、\`$\\ce{CH3COOH <=> CH3COO- + H+}$\`。
711
+ - 公式字号跟随所在容器,不要单独给公式容器设小字号;独立展示的公式建议 \`font-size: 1.2em\` 并上下留白,投屏时更清楚。
712
+ - 公式一律用文本渲染,禁止用公式图片、截图或 \`data:\` URI 的图片代替。
713
+ - 用 JS 动态插入含公式的内容后,需要调用 \`MathJax.typesetPromise()\` 重新排版。
714
+
715
+ ## 练习题与答案
716
+
717
+ 用户要求做练习题时(默认不主动加题目),必须遵守:
718
+
719
+ - **答案和解析默认隐藏**,学生点击「查看答案」按钮后才展开。绝不能把答案直接印在题目下方。原因:老师要先留时间让学生思考和作答,再当堂公布答案。
720
+ - 每道题的答案独立展开收起,互不影响;题目较多时可以额外提供一个「展开全部 / 收起全部」的总控按钮。
721
+ - 选择题可以做成点击选项立刻反馈对错,但完整答案与解析仍然要点击后才显示。
722
+ - 用 \`<details>/<summary>\` 或 JS 切换类名都可以;用 \`<details>\` 时必须重写 \`summary\` 样式,做成明显的大按钮,不要保留浏览器默认的小三角细字样式。
723
+ - 展开按钮同样遵守控件尺寸要求:高度不小于 \`48px\`,文字不小于 \`20px\`,按钮上写明「查看答案」「收起答案」。
724
+ - 答案区展开后要与题干有明显视觉区分(不同底色的卡片、左侧色条),展开过程加一个短过渡动画。
725
+ - 解析要写清思路,不要只给一个最终结果。
726
+
727
+ ## 文案要求
728
+
729
+ - 页面上出现的每一个字都必须是给学生看的教学内容或必要的导航文字,一律使用简体中文。
730
+ - **禁止任何英文装饰文案**:不要英文副标题、不要大写英文栏目名(如 \`PART 01\`、\`KNOWLEDGE POINT\`、\`EXPLORE\`、\`Let's Go\`)、不要英文 slogan、不要背景大字水印、不要 Lorem Ipsum。章节序号用「一、二、三」或「第 1 节」等中文写法。
731
+ - 学科内容本身需要的英文除外:英语教学内容、专业术语原文、化学式、代码、国际单位符号、人名地名原文。
732
+ - 不写与教学无关的凑数文字:作者署名、版权行、「由 AI 生成」、导航菜单、页脚链接等一律不要。
733
+ - 表述用课堂语言,直接讲知识点,不要营销腔和空洞形容词。
734
+
735
+ ## 产出形式
736
+
737
+ - 单个自包含的 \`.html\` 文件,保存到当前工作区。
738
+ - CSS 写在 \`<style>\` 中,JavaScript 写在 \`<script>\` 中,不拆分成多个本地文件。
739
+ - 禁止引用 \`./xxx.css\`、\`./xxx.js\` 等本地相对资源,禁止引用工作区中的本地资源文件。
740
+ - 图片、字体、图标只能使用公开可访问的绝对 URL(例如 CDN);禁止 \`data:\` URI。
741
+ - 允许通过 CDN 绝对 URL 引入第三方库(如 MathJax、图表库),自己写的 CSS/JS 仍然内联在 \`<style>\`/\`<script>\` 里。
742
+ - 需要使用当前对话中提供的图片时,调用 \`publish_image\` 取得绝对 URL,再写入 \`<img src="...">\`。
743
+
744
+ 原因:发布系统以单个 HTML 字符串存储网页,分享链接没有同级资源目录,本地相对路径在工作区预览正常但发布后必然失效。
745
+
746
+ ## 默认规模
747
+
748
+ 用户另有要求时以用户为准;用户没有特别要求时按以下默认值做:
749
+
750
+ - 一页只聚焦一个知识点,3-5 个章节,最多 2-3 个交互演示。
751
+ - 宁可少而精。用户没有要求的章节、题库、公式墙、扩展阅读一律不做。
752
+ - 不要把一个知识点扩写成整个知识体系。
753
+
754
+ ## 工作方式
755
+
756
+ - 不要先探索工作区,不要读工作区里其他网页来「参考风格」。需要澄清需求时用 \`ask_user_question\`,而不是翻工作区猜测。
757
+ - 想清楚结构后用一次 \`write\` 写出完整 HTML,不要先写骨架再用十几次 \`edit\` 往里填内容。
758
+ - 后续只针对用户提出的问题做少量 \`edit\`。
759
+
760
+ ## 自检范围
761
+
762
+ - 只做静态检查:确认没有本地资源引用、标签闭合、脚本没有明显语法错误。一条命令完成,不要反复检查。
763
+ - 同时检查排版、配色与文案:有没有小于 \`16px\` 的字号、有没有 \`300\` 及以下的字重、有没有英文装饰文案、页面是不是只有黑白灰。发现就直接改掉。
764
+ - 用到公式时,检查是否引入了 MathJax 且配置了 \`inlineMath\`,公式定界符是否成对。
765
+ - 有练习题时,确认答案默认处于隐藏状态,且每题都有可点击的展开按钮。
766
+ - 用户没有明确要求时,不要启动无头浏览器、不要截图、不要注入探针脚本跑页面自测、不要回看渲染结果图片。
767
+ - 仅当用户明确要求验证渲染效果,或用户报告了具体的显示问题时,才做实际渲染验证。
651
768
 
652
769
  ## 完成任务
653
770
 
654
- 网页完成后:
771
+ 网页完成后,按顺序做完这几步:
655
772
 
656
- 1. 将最终文件保存到当前工作区。
657
- 2. 文件扩展名必须为 \`.html\`。
658
- 3. 告知用户可以从文件侧边栏打开该 HTML。
659
- 4. 告知用户可以通过页面面板中的「发布」功能生成分享链接。
660
- 5. 不得自行发布网页,除非用户明确要求发布。`;
773
+ 1. 确认文件已保存到当前工作区,扩展名为 \`.html\`。
774
+ 2. **在给出最终回复之前,必须调用 \`present\` 声明这个 HTML 文件**:\`files: [{ path: "<文件路径>", description: "<一句话说明这页讲什么>" }]\`。这一步会在回复下方生成一张可点击的卡片,老师点一下就能在右侧边栏打开预览和发布,不用自己去文件列表里找。在回复里写出路径不能代替这次调用。
775
+ 3. \`present\` 只声明本次产出的网页文件,\`description\` 用简短中文写明教学内容(例如「光合作用过程演示,含 3 道练习」),不要写文件类型或技术细节。修改已有网页后同样要重新声明一次。
776
+ 4. 告知用户点击下方卡片即可打开预览,并可在预览页里用「发布」生成分享链接。
777
+ 5. 不得自行发布网页,除非用户明确要求发布。
778
+ 6. 结尾说明简短,不要用大段表格复述页面里已有的内容。`;
661
779
  //#endregion
662
780
  //#region lib/types/webpage-command.js
663
781
  /** Register the user-facing slash command without making webpage rules optional. */
@@ -0,0 +1,34 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ import type { EdusLoginResult } from '../types.ts';
3
+ /** 登录面板只需要这三个调用,刻意比整个 EdusRemote 窄。 */
4
+ export interface EdusLoginRemote {
5
+ login(phone: string, password: string): Promise<EdusLoginResult>;
6
+ loginByPhone(phone: string, code: string): Promise<EdusLoginResult>;
7
+ sendPhoneCode(phone: string, captcha: string): Promise<void>;
8
+ }
9
+ export type LoginMode = 'password' | 'code';
10
+ /** 各调用点的视觉差异只通过这几个槽位覆盖,行为保持一致。 */
11
+ export interface EdusLoginPanelTheme {
12
+ readonly field?: CSSProperties | undefined;
13
+ readonly button?: CSSProperties | undefined;
14
+ readonly primary?: CSSProperties | undefined;
15
+ readonly error?: CSSProperties | undefined;
16
+ }
17
+ export interface EdusLoginPanelProps {
18
+ readonly remote: EdusLoginRemote;
19
+ readonly onSuccess: (result: EdusLoginResult) => void | Promise<void>;
20
+ /** 传入后才渲染取消按钮。 */
21
+ readonly onCancel?: (() => void) | undefined;
22
+ readonly onBusyChange?: ((busy: boolean) => void) | undefined;
23
+ readonly submitLabel?: string | undefined;
24
+ readonly cancelLabel?: string | undefined;
25
+ /** 表单上方的说明文案。 */
26
+ readonly intro?: ReactNode;
27
+ /** 外部消息(如「登录状态已失效」),会被面板自身的报错覆盖。 */
28
+ readonly notice?: string | undefined;
29
+ readonly defaultMode?: LoginMode | undefined;
30
+ readonly theme?: EdusLoginPanelTheme | undefined;
31
+ }
32
+ /** 手机号密码 / 手机号验证码两种方式共用的登录表单。 */
33
+ export declare function EdusLoginPanel({ remote, onSuccess, onCancel, onBusyChange, submitLabel, cancelLabel, intro, notice, defaultMode, theme }: EdusLoginPanelProps): import("react").JSX.Element;
34
+ //# sourceMappingURL=EdusLoginPanel.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { EdusHtmlFileSummary, EdusVersionSummary, EdusWorkListResult } from '../types.ts';
1
+ import type { EdusHtmlFileSummary, EdusLoginResult, EdusVersionSummary, EdusWorkListResult } from '../types.ts';
2
2
  export interface EdusRemote {
3
3
  listHtmlFiles(sessionId: string): Promise<{
4
4
  files: readonly EdusHtmlFileSummary[];
@@ -6,24 +6,10 @@ export interface EdusRemote {
6
6
  }>;
7
7
  readHtmlFile(sessionId: string, path: string): Promise<string>;
8
8
  writeHtmlFile(sessionId: string, path: string, content: string): Promise<void>;
9
- login(phone: string, password: string): Promise<{
10
- id: string;
11
- phone: string;
12
- token: string;
13
- isAdmin: boolean;
14
- isVip: boolean;
15
- title: string;
16
- vipLevel: number;
17
- }>;
18
- loginInfo(token: string): Promise<{
19
- id: string;
20
- phone: string;
21
- token: string;
22
- isAdmin: boolean;
23
- isVip: boolean;
24
- title: string;
25
- vipLevel: number;
26
- }>;
9
+ login(phone: string, password: string): Promise<EdusLoginResult>;
10
+ loginByPhone(phone: string, code: string): Promise<EdusLoginResult>;
11
+ sendPhoneCode(phone: string, captcha: string): Promise<void>;
12
+ loginInfo(token: string): Promise<EdusLoginResult>;
27
13
  htmlList(token: string, page: number, size: number, search?: string): Promise<EdusWorkListResult>;
28
14
  htmlShare(token: string, id: string): Promise<string>;
29
15
  publishFile(token: string, sessionId: string, path: string, account: string, title?: string, createNew?: boolean): Promise<{
@@ -1,2 +1,2 @@
1
- export declare const WEBPAGE_INSTRUCTION = "# \u7F51\u9875\u751F\u6210\u89C4\u5219\n\n\u5F53\u7528\u6237\u8981\u6C42\u521B\u5EFA\u6216\u4FEE\u6539\u7F51\u9875\u65F6\uFF0C\u5FC5\u987B\u9075\u5B88\u4EE5\u4E0B\u89C4\u5219\u3002\n\n## \u6587\u4EF6\u7ED3\u6784\n\n- \u6700\u7EC8\u7F51\u9875\u5FC5\u987B\u662F\u4E00\u4E2A\u81EA\u5305\u542B\u7684 `.html` \u6587\u4EF6\u3002\n- \u4E0D\u5F97\u5C06 HTML\u3001CSS\u3001JavaScript \u62C6\u5206\u4E3A\u591A\u4E2A\u672C\u5730\u6587\u4EF6\u3002\n- CSS \u5FC5\u987B\u5199\u5728 HTML \u7684 `<style>` \u4E2D\u3002\n- JavaScript \u5FC5\u987B\u5199\u5728 HTML \u7684 `<script>` \u4E2D\u3002\n- \u7981\u6B62\u5F15\u7528 `./xxx.css`\u3001`./xxx.js` \u7B49\u672C\u5730\u76F8\u5BF9\u8D44\u6E90\u3002\n\n## \u56FE\u7247\u548C\u5916\u90E8\u8D44\u6E90\n\n- \u7981\u6B62\u5C06\u56FE\u7247\u7F16\u7801\u4E3A `data:` URI\u3002\n- \u7528\u6237\u660E\u786E\u8981\u6C42\u4F7F\u7528\u5F53\u524D\u5BF9\u8BDD\u4E2D\u63D0\u4F9B\u7684\u56FE\u7247\u65F6\uFF0C\u5FC5\u987B\u8C03\u7528 `publish_image` \u83B7\u53D6\u53EF\u8BBF\u95EE\u7684\u7EDD\u5BF9 URL\uFF0C\u518D\u5C06\u8BE5 URL \u5199\u5165 `<img src=\"...\">` \u7B49\u4F4D\u7F6E\u3002\n- \u5DF2\u6709\u7684\u5916\u90E8\u56FE\u7247\u7EDD\u5BF9 URL \u53EF\u4EE5\u76F4\u63A5\u4F7F\u7528\u3002\n- \u5B57\u4F53\u3001\u56FE\u6807\u7B49\u5916\u90E8\u8D44\u6E90\u5FC5\u987B\u4F7F\u7528\u53EF\u516C\u5F00\u8BBF\u95EE\u7684\u7EDD\u5BF9 URL\uFF0C\u4F8B\u5982 CDN\u3002\n- \u4E0D\u5F97\u5F15\u7528\u5DE5\u4F5C\u533A\u4E2D\u7684\u672C\u5730\u8D44\u6E90\u6587\u4EF6\u3002\n\n## \u539F\u56E0\n\n\u53D1\u5E03\u7CFB\u7EDF\u4EE5\u5355\u4E2A HTML \u5B57\u7B26\u4E32\u5B58\u50A8\u7F51\u9875\uFF0C\u5E76\u901A\u8FC7\u4E0D\u5B58\u5728\u540C\u7EA7\u8D44\u6E90\u76EE\u5F55\u7684\u5206\u4EAB\u94FE\u63A5\u63D0\u4F9B\u9875\u9762\u3002\u56E0\u6B64\uFF0C\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\u53EF\u80FD\u5728\u5DE5\u4F5C\u533A\u9884\u89C8\u65F6\u6B63\u5E38\uFF0C\u4F46\u53D1\u5E03\u540E\u65E0\u6CD5\u8BBF\u95EE\u3002\n\n## \u5B8C\u6210\u4EFB\u52A1\n\n\u7F51\u9875\u5B8C\u6210\u540E\uFF1A\n\n1. \u5C06\u6700\u7EC8\u6587\u4EF6\u4FDD\u5B58\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u3002\n2. \u6587\u4EF6\u6269\u5C55\u540D\u5FC5\u987B\u4E3A `.html`\u3002\n3. \u544A\u77E5\u7528\u6237\u53EF\u4EE5\u4ECE\u6587\u4EF6\u4FA7\u8FB9\u680F\u6253\u5F00\u8BE5 HTML\u3002\n4. \u544A\u77E5\u7528\u6237\u53EF\u4EE5\u901A\u8FC7\u9875\u9762\u9762\u677F\u4E2D\u7684\u300C\u53D1\u5E03\u300D\u529F\u80FD\u751F\u6210\u5206\u4EAB\u94FE\u63A5\u3002\n5. \u4E0D\u5F97\u81EA\u884C\u53D1\u5E03\u7F51\u9875\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u53D1\u5E03\u3002";
1
+ export declare const WEBPAGE_INSTRUCTION = "# \u7F51\u9875\u751F\u6210\u89C4\u5219\n\n\u5F53\u7528\u6237\u8981\u6C42\u521B\u5EFA\u6216\u4FEE\u6539\u7F51\u9875\u65F6\uFF0C\u5FC5\u987B\u9075\u5B88\u4EE5\u4E0B\u89C4\u5219\u3002\n\n## \u4F7F\u7528\u573A\u666F\n\n\u751F\u6210\u7684\u7F51\u9875\u7528\u4E8E\u8BFE\u5802\u6559\u5B66\uFF0C\u901A\u5E38\u7531\u8001\u5E08\u5728\u6559\u5BA4\u91CC\u901A\u8FC7\u6295\u5F71\u4EEA\u6216\u5927\u5C4F\u6295\u5C4F\u5C55\u793A\uFF0C\u540E\u6392\u5B66\u751F\u8DDD\u79BB\u5C4F\u5E55 5-10 \u7C73\uFF0C\u6559\u5BA4\u73AF\u5883\u5149\u8F83\u4EAE\u3001\u6295\u5F71\u5BF9\u6BD4\u5EA6\u504F\u4F4E\u3002\n\n\u56E0\u6B64\uFF1A**\u770B\u5F97\u6E05** \u6BD4 **\u597D\u770B** \u91CD\u8981\uFF0C**\u4FE1\u606F\u5C11\u800C\u5927** \u6BD4 **\u6392\u5F97\u6EE1** \u91CD\u8981\u3002\u6240\u6709\u6392\u7248\u51B3\u7B56\u90FD\u8981\u4EE5\u300C\u6700\u540E\u4E00\u6392\u5B66\u751F\u80FD\u5426\u770B\u6E05\u300D\u4E3A\u51C6\u3002\n\n## \u5F00\u5DE5\u524D\u7684\u786E\u8BA4\n\n\u9700\u6C42\u4E0D\u591F\u5177\u4F53\u65F6\uFF0C\u5148\u8C03\u7528 `ask_user_question` \u4E00\u6B21\u6027\u95EE\u6E05\u695A\uFF0C\u518D\u52A8\u624B\u5199\u9875\u9762\u3002\u4E0D\u8981\u51ED\u7A7A\u5047\u8BBE\uFF0C\u4E5F\u4E0D\u8981\u8FB9\u5199\u8FB9\u95EE\u3002\n\n- \u4F55\u65F6\u95EE\uFF1A\u7528\u6237\u53EA\u7ED9\u4E86\u4E00\u4E2A\u5BBD\u6CDB\u4E3B\u9898\uFF08\u5982\u300C\u8BB2\u4E00\u4E0B\u5149\u5408\u4F5C\u7528\u300D\u300C\u505A\u4E2A\u4E8C\u6B21\u51FD\u6570\u7684\u7F51\u9875\u300D\uFF09\uFF0C\u7F3A\u5C11\u5B66\u6BB5\u3001\u8BFE\u578B\u6216\u8BB2\u89E3\u91CD\u70B9\u7B49\u5173\u952E\u4FE1\u606F\u65F6\u624D\u95EE\u3002\n- \u4F55\u65F6\u4E0D\u95EE\uFF1A\u9700\u6C42\u5DF2\u7ECF\u63CF\u8FF0\u6E05\u695A\u3001\u6216\u7528\u6237\u662F\u5728\u4FEE\u6539\u5DF2\u6709\u7F51\u9875\uFF0C\u76F4\u63A5\u5F00\u5DE5\uFF0C\u4E0D\u8981\u63D0\u95EE\u3002\n- \u4E00\u6B21\u6700\u591A\u95EE 3 \u4E2A\u95EE\u9898\uFF0C\u5E76\u5728\u540C\u4E00\u6B21 `ask_user_question` \u8C03\u7528\u91CC\u95EE\u5B8C\uFF0C\u4E0D\u8981\u5206\u591A\u8F6E\u6253\u65AD\u8001\u5E08\u3002\n- \u6BCF\u4E2A\u95EE\u9898\u7ED9 2-4 \u4E2A\u5177\u4F53\u9009\u9879\uFF1A\u6700\u53EF\u80FD\u7684\u653E\u7B2C\u4E00\u4E2A\u5E76\u5728\u6807\u7B7E\u672B\u5C3E\u52A0\u300C\uFF08\u63A8\u8350\uFF09\u300D\uFF0C\u6BCF\u4E2A\u9009\u9879\u914D\u4E00\u53E5\u8BDD\u8BF4\u660E\u533A\u522B\u3002`header` \u7528\u300C\u5B66\u6BB5\u300D\u300C\u8BFE\u578B\u300D\u8FD9\u7C7B\u7B80\u77ED\u4E2D\u6587\u8BCD\u3002\n- \u95EE\u6559\u5B66\u95EE\u9898\uFF0C\u7528\u8001\u5E08\u7684\u8BED\u8A00\u3002\u53EF\u4EE5\u95EE\uFF1A\u9762\u5411\u54EA\u4E2A\u5B66\u6BB5\u5E74\u7EA7\u3001\u8FD9\u9875\u7528\u4E8E\u65B0\u8BFE\u8BB2\u89E3\u8FD8\u662F\u590D\u4E60\u5DE9\u56FA\u3001\u91CD\u70B9\u8BB2\u54EA\u4E2A\u73AF\u8282\u3001\u8981\u4E0D\u8981\u914D\u7EC3\u4E60\u9898\u3002\n- \u4E0D\u8981\u95EE\u6280\u672F\u95EE\u9898\uFF1A\u914D\u8272\u3001\u5B57\u4F53\u3001\u52A8\u753B\u5B9E\u73B0\u65B9\u5F0F\u3001\u662F\u5426\u5F15\u5165\u516C\u5F0F\u5E93\u7B49\u4E00\u5F8B\u6309\u672C\u89C4\u5219\u81EA\u884C\u51B3\u5B9A\uFF0C\u4E0D\u8BB8\u62FF\u6765\u95EE\u7528\u6237\u3002\n- \u62FF\u5230\u56DE\u7B54\u540E\u76F4\u63A5\u6309\u56DE\u7B54\u751F\u6210\uFF0C\u4E0D\u8981\u518D\u786E\u8BA4\u4E00\u904D\u3002\u7528\u6237\u8DF3\u8FC7\u95EE\u9898\u65F6\uFF0C\u6309\u6700\u5E38\u89C1\u7684\u8BFE\u5802\u60C5\u51B5\u53D6\u9ED8\u8BA4\u503C\u7EE7\u7EED\u505A\uFF0C\u4E0D\u8981\u8FFD\u95EE\u3002\n\n## \u6392\u7248\u4E0E\u5B57\u4F53\n\n- \u6B63\u6587\u5B57\u53F7\u4E0D\u5C0F\u4E8E `22px`\uFF0C\u63A8\u8350 `24-28px`\uFF1B\u56FE\u6CE8\u3001\u6807\u7B7E\u3001\u8868\u683C\u7B49\u6B21\u8981\u6587\u5B57\u4E0D\u5C0F\u4E8E `18px`\uFF0C\u4E0D\u5F97\u51FA\u73B0\u5C0F\u4E8E `16px` \u7684\u4EFB\u4F55\u6587\u5B57\u3002\n- \u6807\u9898\u8981\u6709\u660E\u663E\u5C42\u7EA7\u5DEE\uFF1A\u4E3B\u6807\u9898 `48-64px`\uFF0C\u7AE0\u8282\u6807\u9898 `36-44px`\uFF0C\u5C0F\u8282\u6807\u9898 `28-32px`\u3002\n- \u5B57\u91CD\u8981\u8DB3\uFF1A\u6B63\u6587\u7528 `500`\uFF0C\u6807\u9898\u7528 `700` \u53CA\u4EE5\u4E0A\u3002\u7981\u6B62\u4F7F\u7528 `100/200/300` \u7B49\u7EC6\u5B57\u91CD\uFF0C\u7981\u6B62\u7ED9\u4E2D\u6587\u8BBE `font-weight: 300` \u6216 `lighter`\u3002\n- \u5B57\u4F53\u4F18\u5148\u7CFB\u7EDF\u4E2D\u6587\u9ED1\u4F53\u6808\uFF0C\u4F8B\u5982 `font-family: \"PingFang SC\", \"Microsoft YaHei\", \"Source Han Sans SC\", sans-serif`\uFF1B\u4E0D\u8981\u4F7F\u7528\u7EC6\u4F53\u3001\u886C\u7EBF\u4F53\u6216\u624B\u5199\u4F53\u505A\u6B63\u6587\u3002\n- \u884C\u9AD8 `1.7-1.9`\uFF0C\u6BB5\u843D\u4E4B\u95F4\u7559\u8DB3\u7A7A\u767D\uFF1B\u6B63\u6587\u5BB9\u5668\u5BBD\u5EA6\u63A7\u5236\u5728 `1100-1300px` \u5C45\u4E2D\uFF0C\u4E00\u884C\u4E0D\u8981\u592A\u957F\u3002\n- \u4EA4\u4E92\u63A7\u4EF6\uFF08\u6309\u94AE\u3001\u6ED1\u5757\u3001\u8F93\u5165\u6846\uFF09\u8981\u5927\uFF1A\u6309\u94AE\u9AD8\u5EA6\u4E0D\u5C0F\u4E8E `48px`\uFF0C\u6587\u5B57\u4E0D\u5C0F\u4E8E `20px`\uFF0C\u4FBF\u4E8E\u8001\u5E08\u5728\u8BB2\u53F0\u4E0A\u5FEB\u901F\u70B9\u51FB\u3002\n- \u516C\u5F0F\u3001\u4EE3\u7801\u3001\u56FE\u8868\u6807\u6CE8\u540C\u6837\u8981\u653E\u5927\uFF0C\u4E0D\u8981\u6CBF\u7528\u9ED8\u8BA4\u5C0F\u5B57\u53F7\u3002\n- \u6309 1920\u00D71080 \u6295\u5C4F\u6BD4\u4F8B\u8BBE\u8BA1\uFF1B\u9996\u5C4F\u81F3\u5C11\u8981\u80FD\u5B8C\u6574\u770B\u6E05\u6807\u9898\u548C\u672C\u9875\u8981\u8BB2\u7684\u95EE\u9898\u3002\u9875\u9762\u53EF\u4EE5\u6EDA\u52A8\uFF0C\u4F46\u4E0D\u8981\u628A\u5173\u952E\u5185\u5BB9\u585E\u8FDB\u9700\u8981\u772F\u773C\u770B\u7684\u89D2\u843D\u3002\n\n## \u914D\u8272\n\n\u8BFE\u5802\u7F51\u9875\u8981\u6709\u9C9C\u660E\u7684\u89C6\u89C9\u4E3B\u9898\uFF0C**\u4E0D\u8981\u505A\u6210\u9ED1\u767D\u7070\u7684\u7EAF\u6587\u6863**\u3002\u914D\u8272\u7684\u76EE\u6807\u662F\u5E2E\u52A9\u5B66\u751F\u5FEB\u901F\u533A\u5206\u5185\u5BB9\u7ED3\u6784\uFF0C\u4E0D\u662F\u4E3A\u4E86\u514B\u5236\u3002\n\n- \u5148\u5B9A\u4E00\u5957\u914D\u8272\u518D\u5199\u9875\u9762\uFF1A1 \u4E2A\u4E3B\u8272 + 1 \u4E2A\u8F85\u52A9\u8272 + 1-2 \u4E2A\u5F3A\u8C03\u8272\uFF0C\u518D\u914D\u4E2D\u6027\u5E95\u8272\uFF08\u6D45\u7C73\u3001\u6D45\u7070\u84DD\u7B49\uFF0C\u4E0D\u5FC5\u662F\u7EAF\u767D\uFF09\u3002\u53EF\u53C2\u8003\u300C\u6DF1\u84DD + \u6A59\u300D\u300C\u58A8\u7EFF + \u6696\u9EC4\u300D\u300C\u6DF1\u7D2B + \u9752\u300D\u8FD9\u7C7B\u6709\u660E\u786E\u51B7\u6696\u5BF9\u6BD4\u7684\u7EC4\u5408\uFF0C\u907F\u514D\u6574\u9875\u540C\u4E00\u8272\u76F8\u7684\u6D45\u8272\u5806\u53E0\u3002\n- \u989C\u8272\u7528\u5728\u5927\u9762\u79EF\u5143\u7D20\u4E0A\uFF1A\u5206\u533A\u80CC\u666F\u3001\u6807\u9898\u6761\u3001\u5361\u7247\u3001\u56FE\u793A\u3001\u8868\u5934\u3001\u8FB9\u6846\u3001\u56FE\u6807\u3001\u8FDB\u5EA6\u6761\u3001\u91CD\u70B9\u5E95\u7EB9\u3002\u6B63\u6587\u6587\u5B57\u672C\u8EAB\u4FDD\u6301\u6DF1\u8272\u9AD8\u5BF9\u6BD4\uFF0C\u4E0D\u8981\u9760\u7ED9\u6587\u5B57\u4E0A\u8272\u6765\u505A\u89C6\u89C9\u6548\u679C\u3002\n- \u7528\u989C\u8272\u8868\u8FBE\u7ED3\u6784\uFF1A\u4E0D\u540C\u7AE0\u8282\u53EF\u7528\u4E0D\u540C\u4E3B\u9898\u8272\uFF1B\u5B9A\u4E49\u3001\u4F8B\u9898\u3001\u91CD\u70B9\u3001\u6613\u9519\u70B9\u3001\u5C0F\u7ED3\u7528\u4E0D\u540C\u989C\u8272\u7684\u5361\u7247\u533A\u5206\uFF0C\u8BA9\u5B66\u751F\u4E00\u773C\u770B\u51FA\u300C\u8FD9\u662F\u54EA\u4E00\u7C7B\u5185\u5BB9\u300D\u3002\n- \u5141\u8BB8\u6E10\u53D8\u3001\u8272\u5757\u3001\u5706\u89D2\u5361\u7247\u3001\u9634\u5F71\u3001\u5927\u53F7\u56FE\u6807\u548C\u793A\u610F\u56FE\uFF0C\u53EA\u8981\u4E0D\u538B\u4F4E\u6587\u5B57\u6E05\u6670\u5EA6\u3002\n- \u6295\u5F71\u4F1A\u8BA9\u989C\u8272\u6574\u4F53\u53D8\u6D45\u3001\u5BF9\u6BD4\u5EA6\u4E0B\u964D\uFF0C\u6240\u4EE5\u914D\u8272\u8981\u591F\u9971\u548C\uFF1A\u907F\u514D\u5927\u9762\u79EF\u6D45\u7C89\u3001\u6D45\u9EC4\u3001\u6D45\u7070\u84DD\u8FD9\u7C7B\u6295\u5728\u5899\u4E0A\u51E0\u4E4E\u5206\u4E0D\u51FA\u5C42\u6B21\u7684\u989C\u8272\uFF0C\u4E5F\u907F\u514D\u76F8\u90BB\u533A\u5757\u989C\u8272\u8FC7\u4E8E\u63A5\u8FD1\u3002\n- \u6587\u5B57\u5BF9\u6BD4\u5EA6\u5E95\u7EBF\uFF1A\u6D45\u5E95\u6DF1\u5B57\u65F6\u6B63\u6587\u4E0D\u6D45\u4E8E `#222`\u3001\u6B21\u8981\u6587\u5B57\u4E0D\u6D45\u4E8E `#555`\uFF1B\u6DF1\u5E95\u6D45\u5B57\u65F6\u6B63\u6587\u4E0D\u6DF1\u4E8E `#EEE`\u3002\u5F69\u8272\u5E95\u4E0A\u653E\u6587\u5B57\u65F6\uFF0C\u7528\u6DF1\u8272\u5E95\u914D\u767D\u5B57\u6216\u5F88\u6D45\u7684\u5E95\u914D\u6DF1\u8272\u5B57\uFF0C\u4E0D\u8981\u4E2D\u7B49\u660E\u5EA6\u5E95\u914D\u4E2D\u7B49\u660E\u5EA6\u5B57\u3002\u7981\u6B62 `#999` \u53CA\u66F4\u6D45\u7684\u989C\u8272\u627F\u8F7D\u6B63\u6587\uFF0C\u7981\u6B62\u534A\u900F\u660E\u6587\u5B57\uFF0C\u7981\u6B62\u5F69\u8272\u7EC6\u5B57\u91CD\u5C0F\u5B57\u3002\n\n## \u52A8\u753B\u4E0E\u6F14\u793A\n\n\u8BFE\u5802\u7F51\u9875\u76F8\u6BD4\u8BFE\u4EF6\u7684\u6700\u5927\u4EF7\u503C\uFF0C\u662F**\u628A\u62BD\u8C61\u8FC7\u7A0B\u53D8\u6210\u770B\u5F97\u89C1\u7684\u52A8\u6001\u8FC7\u7A0B**\u3002\u4E0D\u8981\u53EA\u505A\u9759\u6001\u56FE\u6587\uFF0C\u6BCF\u9875\u81F3\u5C11\u7ED9\u6838\u5FC3\u77E5\u8BC6\u70B9\u914D\u4E00\u4E2A\u80FD\u770B\u5230\u300C\u8FC7\u7A0B\u300D\u7684\u52A8\u753B\u3002\n\n- \u4F18\u5148\u505A\u8BB2\u5F97\u6E05\u77E5\u8BC6\u70B9\u7684\u52A8\u753B\uFF1A\u53D7\u529B\u7BAD\u5934\u968F\u6761\u4EF6\u53D8\u5316\u3001\u51FD\u6570\u56FE\u50CF\u968F\u53C2\u6570\u79FB\u52A8\u3001\u5206\u5B50\u91CD\u7EC4\u4E0E\u53CD\u5E94\u8FC7\u7A0B\u3001\u51E0\u4F55\u56FE\u5F62\u7684\u5E73\u79FB\u65CB\u8F6C\u7FFB\u6298\u3001\u5149\u8DEF\u4E0E\u6CE2\u5F62\u3001\u6570\u636E\u6216\u7535\u6D41\u7684\u6D41\u52A8\u3001\u516C\u5F0F\u5206\u6B65\u63A8\u5BFC\u9010\u6B65\u9AD8\u4EAE\u3002\n- \u5B9E\u73B0\u65B9\u5F0F\u4F18\u5148 CSS `transition`/`animation` \u4E0E SVG \u52A8\u753B\uFF08`stroke-dasharray` \u753B\u7EBF\u3001`transform` \u53D8\u6362\uFF09\uFF0C\u590D\u6742\u8FDE\u7EED\u8FD0\u52A8\u518D\u7528 `requestAnimationFrame` \u9A71\u52A8 `<canvas>`\u3002\n- **\u8001\u5E08\u8981\u80FD\u63A7\u5236\u8282\u594F**\uFF1A\u5173\u952E\u6F14\u793A\u5FC5\u987B\u914D\u300C\u64AD\u653E / \u6682\u505C / \u5355\u6B65 / \u91CD\u64AD\u300D\u6309\u94AE\uFF0C\u4E0D\u80FD\u53EA\u81EA\u52A8\u64AD\u4E00\u904D\u5C31\u7ED3\u675F\u2014\u2014\u8001\u5E08\u9700\u8981\u8FB9\u8BB2\u8FB9\u505C\u3001\u53CD\u590D\u6F14\u793A\u3002\n- \u5206\u6B65\u63A8\u5BFC\u3001\u89E3\u9898\u8FC7\u7A0B\u3001\u5B9E\u9A8C\u6B65\u9AA4\u7528\u300C\u4E0B\u4E00\u6B65\u300D\u6309\u94AE\u9010\u6B65\u5C55\u5F00\uFF0C\u5E76\u9AD8\u4EAE\u5F53\u524D\u6B65\u9AA4\uFF0C\u4E0D\u8981\u4E00\u6B21\u6027\u94FA\u6EE1\u6574\u5C4F\u3002\n- \u52A8\u753B\u8282\u594F\u8981\u6162\uFF1A\u8FC7\u6E21\u65F6\u957F `400-800ms`\uFF0C\u5173\u952E\u6B65\u9AA4\u4E4B\u95F4\u505C\u987F 1 \u79D2\u4EE5\u4E0A\uFF0C\u8BA9\u540E\u6392\u5B66\u751F\u8DDF\u5F97\u4E0A\uFF1B\u4E0D\u8981\u7528 `150ms` \u7684\u5FEB\u95EA\u3002\n- \u52A8\u753B\u5143\u7D20\u8981\u5927\uFF1A\u793A\u610F\u56FE\u5C3A\u5BF8\u8DB3\u591F\u5360\u636E\u7248\u9762\uFF0C\u7EBF\u6761\u548C\u7BAD\u5934\u4E0D\u7EC6\u4E8E `3px`\uFF0C\u8FD0\u52A8\u5E45\u5EA6\u660E\u663E\uFF0C\u5173\u952E\u90E8\u4F4D\u914D\u5927\u53F7\u6587\u5B57\u6807\u6CE8\u3002\n- \u53EF\u4EE5\u7528 `IntersectionObserver` \u505A\u6EDA\u52A8\u5165\u573A\u52A8\u753B\uFF0C\u4F46\u5FC5\u987B\u4FDD\u8BC1 JS \u5931\u6548\u65F6\u5185\u5BB9\u4ECD\u7136\u53EF\u89C1\uFF0C\u4E0D\u8981\u8BA9\u5143\u7D20\u6C38\u4E45\u505C\u5728\u900F\u660E\u72B6\u6001\u3002\n- **\u7981\u6B62\u7EAF\u88C5\u9970\u6027\u52A8\u753B**\uFF1A\u7C92\u5B50\u80CC\u666F\u3001\u6D6E\u52A8\u5149\u6591\u3001\u5FAA\u73AF\u95EA\u70C1\u3001\u6253\u5B57\u673A\u6548\u679C\u3001\u8DD1\u9A6C\u706F\u3001\u65E0\u610F\u4E49\u7684\u60AC\u6D6E\u7F29\u653E\u3002\u8BFE\u5802\u4E0A\u8FD9\u4E9B\u53EA\u4F1A\u5206\u6563\u6CE8\u610F\u529B\u3002\u52A8\u753B\u5FC5\u987B\u670D\u52A1\u4E8E\u8BB2\u6E05\u695A\u67D0\u4E2A\u77E5\u8BC6\u70B9\u3002\n- \u6CE8\u610F\u4E0E\u300C\u9ED8\u8BA4\u89C4\u6A21\u300D\u7684\u533A\u522B\uFF1A\u81EA\u52A8\u64AD\u653E\u7684\u52A8\u753B\u3001\u8FC7\u6E21\u6548\u679C\u3001\u5206\u6B65\u9AD8\u4EAE\u4E0D\u8BA1\u5165\u4EA4\u4E92\u6F14\u793A\u6570\u91CF\u9650\u5236\uFF0C\u53EF\u4EE5\u653E\u5FC3\u591A\u7528\uFF1B\u53D7\u9650\u5236\u7684\u662F\u9700\u8981\u5B66\u751F\u81EA\u5DF1\u8C03\u53C2\u64CD\u4F5C\u7684\u590D\u6742\u4EA4\u4E92\u7EC4\u4EF6\u3002\n\n## \u516C\u5F0F\u4E0E\u7B26\u53F7\n\n\u9875\u9762\u6D89\u53CA\u6570\u5B66\u3001\u7269\u7406\u3001\u5316\u5B66\u65F6\uFF0C\u5FC5\u987B\u4FDD\u8BC1\u7B26\u53F7\u80FD\u6B63\u786E\u6E32\u67D3\uFF0C\u4E0D\u80FD\u51FA\u73B0\u4E71\u7801\u3001\u9519\u4F4D\u6216\u88AB\u5F53\u6210\u666E\u901A\u6587\u672C\u7684 `\\frac`\u3002\n\n- `<head>` \u91CC\u5FC5\u987B\u6709 `<meta charset=\"utf-8\">`\u3002\n- **\u7B80\u5355\u7B26\u53F7\u76F4\u63A5\u7528 Unicode \u548C HTML \u6807\u7B7E**\uFF0C\u4E0D\u8981\u4E3A\u4E00\u4E2A\u4E0B\u6807\u5F15\u5165\u6574\u4E2A\u516C\u5F0F\u5F15\u64CE\uFF1A\u4E0A\u4E0B\u6807\u7528 `<sub>`/`<sup>`\uFF08H<sub>2</sub>O\u3001v<sup>2</sup>\uFF09\uFF0C\u5E0C\u814A\u5B57\u6BCD\u3001`\u00D7 \u00F7 \u00B1 \u2248 \u2260 \u2264 \u2265 \u2208 \u221A \u2211 \u222B \u00B0 \u2103 \u21CC \u2192` \u7B49\u76F4\u63A5\u5199\u5B57\u7B26\u3002\n- **\u6D89\u53CA\u5206\u5F0F\u3001\u6839\u5F0F\u3001\u77E9\u9635\u3001\u6781\u9650\u3001\u79EF\u5206\u3001\u5316\u5B66\u65B9\u7A0B\u5F0F\u914D\u5E73\u7B49\u590D\u6742\u6392\u7248\u65F6\uFF0C\u5F15\u5165 MathJax 3\uFF08CDN\uFF09**\uFF0C\u5E76\u540C\u65F6\u5F00\u542F mhchem \u4EE5\u652F\u6301\u5316\u5B66\u5F0F\uFF1A\n\n```html\n<script>\n window.MathJax = {\n loader: { load: ['[tex]/mhchem'] },\n tex: {\n packages: { '[+]': ['mhchem'] },\n inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],\n displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],\n },\n options: { skipHtmlTags: ['script', 'style', 'textarea', 'pre'] },\n }\n</script>\n<script defer src=\"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js\"></script>\n```\n\n- MathJax \u9ED8\u8BA4\u4E0D\u89E3\u6790 `$...$` \u884C\u5185\u516C\u5F0F\uFF0C\u52A1\u5FC5\u6309\u4E0A\u9762\u7684\u914D\u7F6E\u663E\u5F0F\u58F0\u660E `inlineMath`\uFF0C\u5426\u5219\u516C\u5F0F\u4F1A\u539F\u6837\u663E\u793A\u6210\u6E90\u7801\u3002\n- \u5316\u5B66\u5F0F\u7528 mhchem \u8BED\u6CD5\u5199\u5728\u516C\u5F0F\u91CC\uFF0C\u4F8B\u5982 `$\\ce{2H2 + O2 -> 2H2O}$`\u3001`$\\ce{CH3COOH <=> CH3COO- + H+}$`\u3002\n- \u516C\u5F0F\u5B57\u53F7\u8DDF\u968F\u6240\u5728\u5BB9\u5668\uFF0C\u4E0D\u8981\u5355\u72EC\u7ED9\u516C\u5F0F\u5BB9\u5668\u8BBE\u5C0F\u5B57\u53F7\uFF1B\u72EC\u7ACB\u5C55\u793A\u7684\u516C\u5F0F\u5EFA\u8BAE `font-size: 1.2em` \u5E76\u4E0A\u4E0B\u7559\u767D\uFF0C\u6295\u5C4F\u65F6\u66F4\u6E05\u695A\u3002\n- \u516C\u5F0F\u4E00\u5F8B\u7528\u6587\u672C\u6E32\u67D3\uFF0C\u7981\u6B62\u7528\u516C\u5F0F\u56FE\u7247\u3001\u622A\u56FE\u6216 `data:` URI \u7684\u56FE\u7247\u4EE3\u66FF\u3002\n- \u7528 JS \u52A8\u6001\u63D2\u5165\u542B\u516C\u5F0F\u7684\u5185\u5BB9\u540E\uFF0C\u9700\u8981\u8C03\u7528 `MathJax.typesetPromise()` \u91CD\u65B0\u6392\u7248\u3002\n\n## \u7EC3\u4E60\u9898\u4E0E\u7B54\u6848\n\n\u7528\u6237\u8981\u6C42\u505A\u7EC3\u4E60\u9898\u65F6\uFF08\u9ED8\u8BA4\u4E0D\u4E3B\u52A8\u52A0\u9898\u76EE\uFF09\uFF0C\u5FC5\u987B\u9075\u5B88\uFF1A\n\n- **\u7B54\u6848\u548C\u89E3\u6790\u9ED8\u8BA4\u9690\u85CF**\uFF0C\u5B66\u751F\u70B9\u51FB\u300C\u67E5\u770B\u7B54\u6848\u300D\u6309\u94AE\u540E\u624D\u5C55\u5F00\u3002\u7EDD\u4E0D\u80FD\u628A\u7B54\u6848\u76F4\u63A5\u5370\u5728\u9898\u76EE\u4E0B\u65B9\u3002\u539F\u56E0\uFF1A\u8001\u5E08\u8981\u5148\u7559\u65F6\u95F4\u8BA9\u5B66\u751F\u601D\u8003\u548C\u4F5C\u7B54\uFF0C\u518D\u5F53\u5802\u516C\u5E03\u7B54\u6848\u3002\n- \u6BCF\u9053\u9898\u7684\u7B54\u6848\u72EC\u7ACB\u5C55\u5F00\u6536\u8D77\uFF0C\u4E92\u4E0D\u5F71\u54CD\uFF1B\u9898\u76EE\u8F83\u591A\u65F6\u53EF\u4EE5\u989D\u5916\u63D0\u4F9B\u4E00\u4E2A\u300C\u5C55\u5F00\u5168\u90E8 / \u6536\u8D77\u5168\u90E8\u300D\u7684\u603B\u63A7\u6309\u94AE\u3002\n- \u9009\u62E9\u9898\u53EF\u4EE5\u505A\u6210\u70B9\u51FB\u9009\u9879\u7ACB\u523B\u53CD\u9988\u5BF9\u9519\uFF0C\u4F46\u5B8C\u6574\u7B54\u6848\u4E0E\u89E3\u6790\u4ECD\u7136\u8981\u70B9\u51FB\u540E\u624D\u663E\u793A\u3002\n- \u7528 `<details>/<summary>` \u6216 JS \u5207\u6362\u7C7B\u540D\u90FD\u53EF\u4EE5\uFF1B\u7528 `<details>` \u65F6\u5FC5\u987B\u91CD\u5199 `summary` \u6837\u5F0F\uFF0C\u505A\u6210\u660E\u663E\u7684\u5927\u6309\u94AE\uFF0C\u4E0D\u8981\u4FDD\u7559\u6D4F\u89C8\u5668\u9ED8\u8BA4\u7684\u5C0F\u4E09\u89D2\u7EC6\u5B57\u6837\u5F0F\u3002\n- \u5C55\u5F00\u6309\u94AE\u540C\u6837\u9075\u5B88\u63A7\u4EF6\u5C3A\u5BF8\u8981\u6C42\uFF1A\u9AD8\u5EA6\u4E0D\u5C0F\u4E8E `48px`\uFF0C\u6587\u5B57\u4E0D\u5C0F\u4E8E `20px`\uFF0C\u6309\u94AE\u4E0A\u5199\u660E\u300C\u67E5\u770B\u7B54\u6848\u300D\u300C\u6536\u8D77\u7B54\u6848\u300D\u3002\n- \u7B54\u6848\u533A\u5C55\u5F00\u540E\u8981\u4E0E\u9898\u5E72\u6709\u660E\u663E\u89C6\u89C9\u533A\u5206\uFF08\u4E0D\u540C\u5E95\u8272\u7684\u5361\u7247\u3001\u5DE6\u4FA7\u8272\u6761\uFF09\uFF0C\u5C55\u5F00\u8FC7\u7A0B\u52A0\u4E00\u4E2A\u77ED\u8FC7\u6E21\u52A8\u753B\u3002\n- \u89E3\u6790\u8981\u5199\u6E05\u601D\u8DEF\uFF0C\u4E0D\u8981\u53EA\u7ED9\u4E00\u4E2A\u6700\u7EC8\u7ED3\u679C\u3002\n\n## \u6587\u6848\u8981\u6C42\n\n- \u9875\u9762\u4E0A\u51FA\u73B0\u7684\u6BCF\u4E00\u4E2A\u5B57\u90FD\u5FC5\u987B\u662F\u7ED9\u5B66\u751F\u770B\u7684\u6559\u5B66\u5185\u5BB9\u6216\u5FC5\u8981\u7684\u5BFC\u822A\u6587\u5B57\uFF0C\u4E00\u5F8B\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587\u3002\n- **\u7981\u6B62\u4EFB\u4F55\u82F1\u6587\u88C5\u9970\u6587\u6848**\uFF1A\u4E0D\u8981\u82F1\u6587\u526F\u6807\u9898\u3001\u4E0D\u8981\u5927\u5199\u82F1\u6587\u680F\u76EE\u540D\uFF08\u5982 `PART 01`\u3001`KNOWLEDGE POINT`\u3001`EXPLORE`\u3001`Let's Go`\uFF09\u3001\u4E0D\u8981\u82F1\u6587 slogan\u3001\u4E0D\u8981\u80CC\u666F\u5927\u5B57\u6C34\u5370\u3001\u4E0D\u8981 Lorem Ipsum\u3002\u7AE0\u8282\u5E8F\u53F7\u7528\u300C\u4E00\u3001\u4E8C\u3001\u4E09\u300D\u6216\u300C\u7B2C 1 \u8282\u300D\u7B49\u4E2D\u6587\u5199\u6CD5\u3002\n- \u5B66\u79D1\u5185\u5BB9\u672C\u8EAB\u9700\u8981\u7684\u82F1\u6587\u9664\u5916\uFF1A\u82F1\u8BED\u6559\u5B66\u5185\u5BB9\u3001\u4E13\u4E1A\u672F\u8BED\u539F\u6587\u3001\u5316\u5B66\u5F0F\u3001\u4EE3\u7801\u3001\u56FD\u9645\u5355\u4F4D\u7B26\u53F7\u3001\u4EBA\u540D\u5730\u540D\u539F\u6587\u3002\n- \u4E0D\u5199\u4E0E\u6559\u5B66\u65E0\u5173\u7684\u51D1\u6570\u6587\u5B57\uFF1A\u4F5C\u8005\u7F72\u540D\u3001\u7248\u6743\u884C\u3001\u300C\u7531 AI \u751F\u6210\u300D\u3001\u5BFC\u822A\u83DC\u5355\u3001\u9875\u811A\u94FE\u63A5\u7B49\u4E00\u5F8B\u4E0D\u8981\u3002\n- \u8868\u8FF0\u7528\u8BFE\u5802\u8BED\u8A00\uFF0C\u76F4\u63A5\u8BB2\u77E5\u8BC6\u70B9\uFF0C\u4E0D\u8981\u8425\u9500\u8154\u548C\u7A7A\u6D1E\u5F62\u5BB9\u8BCD\u3002\n\n## \u4EA7\u51FA\u5F62\u5F0F\n\n- \u5355\u4E2A\u81EA\u5305\u542B\u7684 `.html` \u6587\u4EF6\uFF0C\u4FDD\u5B58\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u3002\n- CSS \u5199\u5728 `<style>` \u4E2D\uFF0CJavaScript \u5199\u5728 `<script>` \u4E2D\uFF0C\u4E0D\u62C6\u5206\u6210\u591A\u4E2A\u672C\u5730\u6587\u4EF6\u3002\n- \u7981\u6B62\u5F15\u7528 `./xxx.css`\u3001`./xxx.js` \u7B49\u672C\u5730\u76F8\u5BF9\u8D44\u6E90\uFF0C\u7981\u6B62\u5F15\u7528\u5DE5\u4F5C\u533A\u4E2D\u7684\u672C\u5730\u8D44\u6E90\u6587\u4EF6\u3002\n- \u56FE\u7247\u3001\u5B57\u4F53\u3001\u56FE\u6807\u53EA\u80FD\u4F7F\u7528\u516C\u5F00\u53EF\u8BBF\u95EE\u7684\u7EDD\u5BF9 URL\uFF08\u4F8B\u5982 CDN\uFF09\uFF1B\u7981\u6B62 `data:` URI\u3002\n- \u5141\u8BB8\u901A\u8FC7 CDN \u7EDD\u5BF9 URL \u5F15\u5165\u7B2C\u4E09\u65B9\u5E93\uFF08\u5982 MathJax\u3001\u56FE\u8868\u5E93\uFF09\uFF0C\u81EA\u5DF1\u5199\u7684 CSS/JS \u4ECD\u7136\u5185\u8054\u5728 `<style>`/`<script>` \u91CC\u3002\n- \u9700\u8981\u4F7F\u7528\u5F53\u524D\u5BF9\u8BDD\u4E2D\u63D0\u4F9B\u7684\u56FE\u7247\u65F6\uFF0C\u8C03\u7528 `publish_image` \u53D6\u5F97\u7EDD\u5BF9 URL\uFF0C\u518D\u5199\u5165 `<img src=\"...\">`\u3002\n\n\u539F\u56E0\uFF1A\u53D1\u5E03\u7CFB\u7EDF\u4EE5\u5355\u4E2A HTML \u5B57\u7B26\u4E32\u5B58\u50A8\u7F51\u9875\uFF0C\u5206\u4EAB\u94FE\u63A5\u6CA1\u6709\u540C\u7EA7\u8D44\u6E90\u76EE\u5F55\uFF0C\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u9884\u89C8\u6B63\u5E38\u4F46\u53D1\u5E03\u540E\u5FC5\u7136\u5931\u6548\u3002\n\n## \u9ED8\u8BA4\u89C4\u6A21\n\n\u7528\u6237\u53E6\u6709\u8981\u6C42\u65F6\u4EE5\u7528\u6237\u4E3A\u51C6\uFF1B\u7528\u6237\u6CA1\u6709\u7279\u522B\u8981\u6C42\u65F6\u6309\u4EE5\u4E0B\u9ED8\u8BA4\u503C\u505A\uFF1A\n\n- \u4E00\u9875\u53EA\u805A\u7126\u4E00\u4E2A\u77E5\u8BC6\u70B9\uFF0C3-5 \u4E2A\u7AE0\u8282\uFF0C\u6700\u591A 2-3 \u4E2A\u4EA4\u4E92\u6F14\u793A\u3002\n- \u5B81\u53EF\u5C11\u800C\u7CBE\u3002\u7528\u6237\u6CA1\u6709\u8981\u6C42\u7684\u7AE0\u8282\u3001\u9898\u5E93\u3001\u516C\u5F0F\u5899\u3001\u6269\u5C55\u9605\u8BFB\u4E00\u5F8B\u4E0D\u505A\u3002\n- \u4E0D\u8981\u628A\u4E00\u4E2A\u77E5\u8BC6\u70B9\u6269\u5199\u6210\u6574\u4E2A\u77E5\u8BC6\u4F53\u7CFB\u3002\n\n## \u5DE5\u4F5C\u65B9\u5F0F\n\n- \u4E0D\u8981\u5148\u63A2\u7D22\u5DE5\u4F5C\u533A\uFF0C\u4E0D\u8981\u8BFB\u5DE5\u4F5C\u533A\u91CC\u5176\u4ED6\u7F51\u9875\u6765\u300C\u53C2\u8003\u98CE\u683C\u300D\u3002\u9700\u8981\u6F84\u6E05\u9700\u6C42\u65F6\u7528 `ask_user_question`\uFF0C\u800C\u4E0D\u662F\u7FFB\u5DE5\u4F5C\u533A\u731C\u6D4B\u3002\n- \u60F3\u6E05\u695A\u7ED3\u6784\u540E\u7528\u4E00\u6B21 `write` \u5199\u51FA\u5B8C\u6574 HTML\uFF0C\u4E0D\u8981\u5148\u5199\u9AA8\u67B6\u518D\u7528\u5341\u51E0\u6B21 `edit` \u5F80\u91CC\u586B\u5185\u5BB9\u3002\n- \u540E\u7EED\u53EA\u9488\u5BF9\u7528\u6237\u63D0\u51FA\u7684\u95EE\u9898\u505A\u5C11\u91CF `edit`\u3002\n\n## \u81EA\u68C0\u8303\u56F4\n\n- \u53EA\u505A\u9759\u6001\u68C0\u67E5\uFF1A\u786E\u8BA4\u6CA1\u6709\u672C\u5730\u8D44\u6E90\u5F15\u7528\u3001\u6807\u7B7E\u95ED\u5408\u3001\u811A\u672C\u6CA1\u6709\u660E\u663E\u8BED\u6CD5\u9519\u8BEF\u3002\u4E00\u6761\u547D\u4EE4\u5B8C\u6210\uFF0C\u4E0D\u8981\u53CD\u590D\u68C0\u67E5\u3002\n- \u540C\u65F6\u68C0\u67E5\u6392\u7248\u3001\u914D\u8272\u4E0E\u6587\u6848\uFF1A\u6709\u6CA1\u6709\u5C0F\u4E8E `16px` \u7684\u5B57\u53F7\u3001\u6709\u6CA1\u6709 `300` \u53CA\u4EE5\u4E0B\u7684\u5B57\u91CD\u3001\u6709\u6CA1\u6709\u82F1\u6587\u88C5\u9970\u6587\u6848\u3001\u9875\u9762\u662F\u4E0D\u662F\u53EA\u6709\u9ED1\u767D\u7070\u3002\u53D1\u73B0\u5C31\u76F4\u63A5\u6539\u6389\u3002\n- \u7528\u5230\u516C\u5F0F\u65F6\uFF0C\u68C0\u67E5\u662F\u5426\u5F15\u5165\u4E86 MathJax \u4E14\u914D\u7F6E\u4E86 `inlineMath`\uFF0C\u516C\u5F0F\u5B9A\u754C\u7B26\u662F\u5426\u6210\u5BF9\u3002\n- \u6709\u7EC3\u4E60\u9898\u65F6\uFF0C\u786E\u8BA4\u7B54\u6848\u9ED8\u8BA4\u5904\u4E8E\u9690\u85CF\u72B6\u6001\uFF0C\u4E14\u6BCF\u9898\u90FD\u6709\u53EF\u70B9\u51FB\u7684\u5C55\u5F00\u6309\u94AE\u3002\n- \u7528\u6237\u6CA1\u6709\u660E\u786E\u8981\u6C42\u65F6\uFF0C\u4E0D\u8981\u542F\u52A8\u65E0\u5934\u6D4F\u89C8\u5668\u3001\u4E0D\u8981\u622A\u56FE\u3001\u4E0D\u8981\u6CE8\u5165\u63A2\u9488\u811A\u672C\u8DD1\u9875\u9762\u81EA\u6D4B\u3001\u4E0D\u8981\u56DE\u770B\u6E32\u67D3\u7ED3\u679C\u56FE\u7247\u3002\n- \u4EC5\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u9A8C\u8BC1\u6E32\u67D3\u6548\u679C\uFF0C\u6216\u7528\u6237\u62A5\u544A\u4E86\u5177\u4F53\u7684\u663E\u793A\u95EE\u9898\u65F6\uFF0C\u624D\u505A\u5B9E\u9645\u6E32\u67D3\u9A8C\u8BC1\u3002\n\n## \u5B8C\u6210\u4EFB\u52A1\n\n\u7F51\u9875\u5B8C\u6210\u540E\uFF0C\u6309\u987A\u5E8F\u505A\u5B8C\u8FD9\u51E0\u6B65\uFF1A\n\n1. \u786E\u8BA4\u6587\u4EF6\u5DF2\u4FDD\u5B58\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\uFF0C\u6269\u5C55\u540D\u4E3A `.html`\u3002\n2. **\u5728\u7ED9\u51FA\u6700\u7EC8\u56DE\u590D\u4E4B\u524D\uFF0C\u5FC5\u987B\u8C03\u7528 `present` \u58F0\u660E\u8FD9\u4E2A HTML \u6587\u4EF6**\uFF1A`files: [{ path: \"<\u6587\u4EF6\u8DEF\u5F84>\", description: \"<\u4E00\u53E5\u8BDD\u8BF4\u660E\u8FD9\u9875\u8BB2\u4EC0\u4E48>\" }]`\u3002\u8FD9\u4E00\u6B65\u4F1A\u5728\u56DE\u590D\u4E0B\u65B9\u751F\u6210\u4E00\u5F20\u53EF\u70B9\u51FB\u7684\u5361\u7247\uFF0C\u8001\u5E08\u70B9\u4E00\u4E0B\u5C31\u80FD\u5728\u53F3\u4FA7\u8FB9\u680F\u6253\u5F00\u9884\u89C8\u548C\u53D1\u5E03\uFF0C\u4E0D\u7528\u81EA\u5DF1\u53BB\u6587\u4EF6\u5217\u8868\u91CC\u627E\u3002\u5728\u56DE\u590D\u91CC\u5199\u51FA\u8DEF\u5F84\u4E0D\u80FD\u4EE3\u66FF\u8FD9\u6B21\u8C03\u7528\u3002\n3. `present` \u53EA\u58F0\u660E\u672C\u6B21\u4EA7\u51FA\u7684\u7F51\u9875\u6587\u4EF6\uFF0C`description` \u7528\u7B80\u77ED\u4E2D\u6587\u5199\u660E\u6559\u5B66\u5185\u5BB9\uFF08\u4F8B\u5982\u300C\u5149\u5408\u4F5C\u7528\u8FC7\u7A0B\u6F14\u793A\uFF0C\u542B 3 \u9053\u7EC3\u4E60\u300D\uFF09\uFF0C\u4E0D\u8981\u5199\u6587\u4EF6\u7C7B\u578B\u6216\u6280\u672F\u7EC6\u8282\u3002\u4FEE\u6539\u5DF2\u6709\u7F51\u9875\u540E\u540C\u6837\u8981\u91CD\u65B0\u58F0\u660E\u4E00\u6B21\u3002\n4. \u544A\u77E5\u7528\u6237\u70B9\u51FB\u4E0B\u65B9\u5361\u7247\u5373\u53EF\u6253\u5F00\u9884\u89C8\uFF0C\u5E76\u53EF\u5728\u9884\u89C8\u9875\u91CC\u7528\u300C\u53D1\u5E03\u300D\u751F\u6210\u5206\u4EAB\u94FE\u63A5\u3002\n5. \u4E0D\u5F97\u81EA\u884C\u53D1\u5E03\u7F51\u9875\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u53D1\u5E03\u3002\n6. \u7ED3\u5C3E\u8BF4\u660E\u7B80\u77ED\uFF0C\u4E0D\u8981\u7528\u5927\u6BB5\u8868\u683C\u590D\u8FF0\u9875\u9762\u91CC\u5DF2\u6709\u7684\u5185\u5BB9\u3002";
2
2
  //# sourceMappingURL=webpage-instruction.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houser/edus-plugin",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",