cotutor 0.3.0 → 0.4.1
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/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +65 -18
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +13 -3
- package/cards/photo/card.css +10 -3
- package/cards/photo/card.md +2 -2
- package/cards/tutor/card.css +13 -0
- package/cards/tutor/card.md +2 -1
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
- package/dist/app/activity-eval.js +3 -2
- package/dist/app/cli/activities.d.ts +8 -1
- package/dist/app/cli/activities.js +42 -4
- package/dist/app/cli/cert.d.ts +11 -0
- package/dist/app/cli/cert.js +36 -9
- package/dist/app/cli/chalk/browser.d.ts +2 -2
- package/dist/app/cli/chalk/browser.js +22 -7
- package/dist/app/cli/chalk/main.js +3 -2
- package/dist/app/cli/doctor.d.ts +11 -5
- package/dist/app/cli/doctor.js +156 -181
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/init.js +8 -5
- package/dist/app/cli/main.js +379 -27
- package/dist/app/cli/migrate.d.ts +2 -11
- package/dist/app/cli/migrate.js +1 -59
- package/dist/app/cli/port-holder.d.ts +16 -0
- package/dist/app/cli/port-holder.js +34 -0
- package/dist/app/cli/port.d.ts +37 -0
- package/dist/app/cli/port.js +116 -0
- package/dist/app/cli/sandbox.d.ts +25 -0
- package/dist/app/cli/sandbox.js +166 -0
- package/dist/app/cli/serve-log.d.ts +13 -0
- package/dist/app/cli/serve-log.js +90 -0
- package/dist/app/cli/serve.d.ts +18 -4
- package/dist/app/cli/serve.js +78 -8
- package/dist/app/cli/service-windows.d.ts +50 -0
- package/dist/app/cli/service-windows.js +117 -0
- package/dist/app/cli/service.d.ts +152 -0
- package/dist/app/cli/service.js +443 -0
- package/dist/app/cli/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +13 -13
- package/dist/app/cli/skills.d.ts +5 -1
- package/dist/app/cli/skills.js +14 -4
- package/dist/app/cli/tray-icons.d.ts +11 -0
- package/dist/app/cli/tray-icons.js +87 -0
- package/dist/app/cli/tray-ids.d.ts +6 -0
- package/dist/app/cli/tray-ids.js +6 -0
- package/dist/app/cli/tray-mac.d.ts +8 -0
- package/dist/app/cli/tray-mac.js +140 -0
- package/dist/app/cli/tray-run.d.ts +1 -0
- package/dist/app/cli/tray-run.js +42 -0
- package/dist/app/cli/tray-windows.d.ts +5 -0
- package/dist/app/cli/tray-windows.js +166 -0
- package/dist/app/cli/tray.d.ts +236 -0
- package/dist/app/cli/tray.js +670 -0
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +11 -6
- package/dist/app/dictation/route.js +3 -2
- package/dist/app/http/app.d.ts +5 -10
- package/dist/app/http/app.js +47 -98
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-tutor-files.js +1 -1
- package/dist/app/http/parent-home.d.ts +14 -0
- package/dist/app/http/parent-home.js +69 -6
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/raw-view.js +3 -3
- package/dist/app/http/voice-preview.d.ts +4 -0
- package/dist/app/http/voice-preview.js +34 -0
- package/dist/app/machine/bailian.d.ts +0 -2
- package/dist/app/machine/bailian.js +5 -6
- package/dist/app/machine/update.d.ts +25 -0
- package/dist/app/machine/update.js +88 -0
- package/dist/app/mock/mock-live.js +1 -1
- package/dist/app/mock/mock.d.ts +4 -2
- package/dist/app/mock/mock.js +20 -12
- package/dist/app/replay.js +4 -4
- package/dist/app/skills/home-doc.js +1 -1
- package/dist/app/workspace.d.ts +9 -4
- package/dist/app/workspace.js +33 -10
- package/dist/app/ws/route.d.ts +8 -2
- package/dist/app/ws/route.js +7 -3
- package/dist/board/parse.d.ts +0 -2
- package/dist/board/parse.js +0 -6
- package/dist/board/presentation.d.ts +9 -15
- package/dist/board/presentation.js +8 -10
- package/dist/brain/api-tools.js +2 -1
- package/dist/brain/api.d.ts +12 -17
- package/dist/brain/api.js +16 -33
- package/dist/brain/index.d.ts +23 -71
- package/dist/brain/index.js +6 -69
- package/dist/brain/log.d.ts +5 -0
- package/dist/brain/log.js +22 -0
- package/dist/brain/runtime.d.ts +30 -0
- package/dist/brain/runtime.js +23 -0
- package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
- package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
- package/dist/brain/stream.d.ts +5 -5
- package/dist/brain/stream.js +7 -7
- package/dist/brain/transcript.d.ts +3 -3
- package/dist/brain/transcript.js +6 -6
- package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
- package/dist/brain/types.js +14 -0
- package/dist/chalk/glyphs/skeleton.d.ts +3 -3
- package/dist/chalk/glyphs/skeleton.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/memory/diary.js +3 -3
- package/dist/realtime/bailian.d.ts +1 -1
- package/dist/realtime/bailian.js +3 -2
- package/dist/realtime/preview.js +1 -1
- package/dist/realtime/voices.d.ts +2 -0
- package/dist/realtime/voices.js +57 -57
- package/dist/runtime/activities.d.ts +13 -1
- package/dist/runtime/activities.js +28 -2
- package/dist/runtime/activity-state.js +2 -2
- package/dist/runtime/agent-file.d.ts +3 -3
- package/dist/runtime/agent-file.js +3 -3
- package/dist/runtime/context/tutor-rules.d.ts +2 -2
- package/dist/runtime/context/tutor-rules.js +3 -3
- package/dist/runtime/home/home.d.ts +12 -1
- package/dist/runtime/home/home.js +8 -1
- package/dist/runtime/home/store.d.ts +1 -1
- package/dist/runtime/home/store.js +36 -21
- package/dist/runtime/journal/mix.d.ts +15 -6
- package/dist/runtime/journal/mix.js +60 -19
- package/dist/runtime/journal/recorder.d.ts +58 -13
- package/dist/runtime/journal/recorder.js +106 -16
- package/dist/runtime/journal/store.d.ts +4 -4
- package/dist/runtime/journal/store.js +6 -6
- package/dist/runtime/kid-input.js +2 -3
- package/dist/runtime/live/call.d.ts +12 -0
- package/dist/runtime/live/call.js +51 -18
- package/dist/runtime/live/turns.js +1 -1
- package/dist/runtime/runner.d.ts +9 -55
- package/dist/runtime/runner.js +52 -173
- package/dist/scenes/paint/painter.js +4 -3
- package/dist/schema/config.d.ts +43 -22
- package/dist/schema/config.js +45 -47
- package/dist/schema/conversation.d.ts +6 -0
- package/dist/schema/home.d.ts +11 -1
- package/dist/schema/home.js +7 -2
- package/dist/schema/workspace.d.ts +27 -0
- package/dist/schema/workspace.js +28 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/qianwen.d.ts +14 -10
- package/dist/shared/qianwen.js +26 -22
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/shared/upgrade.d.ts +9 -0
- package/dist/shared/upgrade.js +23 -0
- package/dist/ui/dev/dev-page.js +25 -252
- package/dist/ui/dev/dev-theme.d.ts +1 -1
- package/dist/ui/dev/dev-theme.js +1 -1
- package/dist/ui/dev/voice-gallery-page.js +1 -1
- package/dist/ui/kid/board-mark.d.ts +2 -2
- package/dist/ui/kid/board-mark.js +2 -2
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-page.js +68 -103
- package/dist/ui/kid/voice-ui.d.ts +1 -1
- package/dist/ui/kid/voice-ui.js +1 -1
- package/dist/ui/kid/word-emoji.js +5 -3
- package/dist/ui/parent/parent-home-page.js +337 -7
- package/dist/ui/parent/parent-page.js +6 -2
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
- package/dist/ui/{dev → parent}/qr-page.js +19 -14
- package/dist/ui/shared/brand.d.ts +3 -1
- package/dist/ui/shared/brand.js +3 -1
- package/grades/grade-2/README.md +1 -1
- package/grades/grade-2/VALIDATION.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/package.json +6 -2
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
- package/skills/cotutor-board/references/photo.md +2 -2
- package/skills/cotutor-home/SKILL.md +1 -0
- package/skills/cotutor-home/references/tutor.md +2 -1
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
- package/skills/cotutor-tutor/SKILL.md +5 -13
- package/dist/brain/cli/adapters.d.ts +0 -60
- package/dist/brain/cli/adapters.js +0 -91
- package/dist/brain/cli/qwen.d.ts +0 -11
- package/dist/brain/cli/qwen.js +0 -141
- package/dist/brain/cli/run-plan.d.ts +0 -59
- package/dist/brain/cli/run-plan.js +0 -71
- package/dist/brain/cli/run.d.ts +0 -35
- package/dist/brain/cli/run.js +0 -256
- package/dist/brain/cli/types.js +0 -3
- package/dist/brain/cli/warm.d.ts +0 -53
- package/dist/brain/cli/warm.js +0 -103
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { faviconLinks } from "../shared/favicon.js";
|
|
11
11
|
import { BRAND_CSS, BRAND_FOOTER, BRAND_JS, titleLogo } from "../shared/brand.js";
|
|
12
12
|
import { pageDomJs } from "../shared/page-dom.js";
|
|
13
|
+
import { upgradeSteps } from "../../shared/upgrade.js";
|
|
13
14
|
export const PARENT_HOME_PAGE = String.raw `<!doctype html>
|
|
14
15
|
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
15
16
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
@@ -30,6 +31,9 @@ ${faviconLinks()}
|
|
|
30
31
|
#ct-mark { display:flex; }
|
|
31
32
|
${BRAND_CSS}
|
|
32
33
|
.top small { color:var(--mute); font-size:13px; }
|
|
34
|
+
/* 去工作台:次要的小链接,不和首页 / 记录并列 */
|
|
35
|
+
.devl { flex:none; display:inline-flex; align-items:center; gap:4px; height:32px; padding:0 10px; border:1px solid #e2dfd6; border-radius:10px; background:#fff; color:var(--dim); font-size:13px; text-decoration:none; }
|
|
36
|
+
.top .devl { margin-left:auto; }
|
|
33
37
|
.top .seg { order:3; flex:1 0 100%; }
|
|
34
38
|
.seg { display:flex; padding:3px; border-radius:12px; background:#e9e6de; }
|
|
35
39
|
.seg > * { flex:1; height:36px; border:0; border-radius:9px; background:transparent; font-size:15px; color:var(--dim); display:flex; align-items:center; justify-content:center; text-decoration:none; cursor:pointer; }
|
|
@@ -104,6 +108,100 @@ ${BRAND_CSS}
|
|
|
104
108
|
#toast { position:fixed; left:50%; bottom:calc(env(safe-area-inset-bottom) + 24px); transform:translateX(-50%); max-width:min(560px,calc(100% - 32px)); padding:12px 18px; border-radius:16px; background:#2b2b2bee; color:#fff; font-size:15px; display:none; z-index:20; white-space:pre-line; }
|
|
105
109
|
#toast.on { display:block; }
|
|
106
110
|
.main { display:flex; flex-direction:column; gap:18px; }
|
|
111
|
+
/* 孩子的平板怎么打开:二维码(服务端现画);宽屏在右栏最上面,窄屏在预览上面 */
|
|
112
|
+
#qrtop:empty { display:none; }
|
|
113
|
+
.qrrow { display:flex; flex-direction:column; gap:10px; align-items:center; text-align:center; }
|
|
114
|
+
.qrimg { flex:none; width:220px; height:220px; max-width:100%; border-radius:8px; box-shadow:0 0 0 1px var(--line); overflow:hidden; background:#fff; }
|
|
115
|
+
.qrimg svg { display:block; width:100%; height:100%; }
|
|
116
|
+
.qrurl { font:13px/1.4 ui-monospace,Menlo,monospace; background:#faf9f4; border-radius:8px; padding:8px 10px; overflow-wrap:anywhere; }
|
|
117
|
+
.qrwarn { font-size:13px; color:var(--accent); line-height:1.5; }
|
|
118
|
+
.gh .x { height:auto; padding:2px 0; font-size:13px; }
|
|
119
|
+
/* 样子与声音:头像一格格点;两种声音各一行(现在用的),「换一个」开弹窗;宽屏在右栏,窄屏整屏 */
|
|
120
|
+
.lk { display:flex; flex-direction:column; gap:16px; }
|
|
121
|
+
.lk .nav b { font-size:16px; text-align:center; }
|
|
122
|
+
.lksaved { min-width:56px; text-align:right; font-size:13px; color:var(--ok); }
|
|
123
|
+
.lkbig { align-self:center; width:104px; height:104px; border-radius:50%; overflow:hidden; background:#fff3e8; box-shadow:0 0 0 4px #fff, 0 6px 18px #2b2b2b1f; display:grid; place-items:center; font-size:44px; font-weight:700; color:var(--accent); }
|
|
124
|
+
.lkbig img { width:100%; height:100%; object-fit:cover; }
|
|
125
|
+
.lkgrid { background:var(--card); border-radius:14px; padding:12px; display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:8px; }
|
|
126
|
+
.lkopt { display:flex; flex-direction:column; align-items:center; gap:3px; min-height:44px; padding:8px 0 6px; border:2px solid transparent; border-radius:14px; background:transparent; cursor:pointer; }
|
|
127
|
+
.lkopt.on { border-color:var(--ink); background:#fff; }
|
|
128
|
+
.lkf { width:52px; height:52px; border-radius:50%; overflow:hidden; background:#faf9f4; display:grid; place-items:center; font-size:26px; font-weight:700; color:var(--dim); }
|
|
129
|
+
.lkf img { width:100%; height:100%; object-fit:cover; }
|
|
130
|
+
.lkopt small { font-size:11px; color:var(--mute); line-height:1.3; }
|
|
131
|
+
/* 一个声音:圆里「女 / 男」,底色也分(不只靠颜色);标签:普通话 / 中文灰,方言橙,英文青,童声紫,场景描边 */
|
|
132
|
+
.vface { flex:none; width:44px; height:44px; border-radius:50%; display:grid; place-items:center; font-size:16px; font-weight:700; background:#efece5; color:#5f5b54; }
|
|
133
|
+
.vface.g-f { background:#fbe4e8; color:#9e2f4a; }
|
|
134
|
+
.vface.g-m { background:#dfe9f6; color:#285a96; }
|
|
135
|
+
.vtx { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; text-align:left; }
|
|
136
|
+
.vnm { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
|
|
137
|
+
.vnm b { font-size:16px; font-weight:600; }
|
|
138
|
+
.vnm small, .vtr { font-size:13px; color:#5f5b54; }
|
|
139
|
+
.vtags { display:flex; gap:5px; flex-wrap:wrap; }
|
|
140
|
+
.vt { font-size:12px; line-height:20px; padding:0 7px; border-radius:6px; border:1px solid transparent; }
|
|
141
|
+
.t-lang { background:#f1eee7; color:#5f5b54; }
|
|
142
|
+
.t-dia { background:#fdecd3; color:#8a4b0b; }
|
|
143
|
+
.t-en { background:#dcefee; color:#0f5f5c; }
|
|
144
|
+
.t-kid { background:#ece6f8; color:#5b3fa0; }
|
|
145
|
+
.t-line { background:#fff; color:#5f5b54; border-color:#e3dfd6; }
|
|
146
|
+
.t-warn { background:#fff; color:#8a4b0b; border-color:#e2b877; }
|
|
147
|
+
.vplay { flex:none; width:44px; height:44px; border-radius:50%; border:0; background:#f3efe6; color:var(--ink); display:grid; place-items:center; cursor:pointer; }
|
|
148
|
+
.vplay.on { background:var(--accent); color:#fff; }
|
|
149
|
+
.vcur { padding:12px 12px 12px 14px; display:flex; flex-direction:column; gap:8px; }
|
|
150
|
+
.vcur + .vcur { border-top:1px solid var(--line); }
|
|
151
|
+
.vcur > .lbl { font-size:13px; color:#5f5b54; }
|
|
152
|
+
.vrow2 { display:flex; align-items:center; gap:12px; }
|
|
153
|
+
.vchg { align-self:flex-end; display:inline-flex; align-items:center; gap:2px; min-height:44px; padding:0 10px 0 16px; border:0; border-radius:22px; background:#f3efe6; color:var(--ink); font-size:15px; cursor:pointer; }
|
|
154
|
+
.vchg span { display:flex; }
|
|
155
|
+
.vchg svg { width:16px; height:16px; }
|
|
156
|
+
/* 选声音的弹窗:宽屏居中,窄屏从底下升起占满 */
|
|
157
|
+
.vp { position:fixed; inset:0; z-index:30; background:rgba(30,28,25,.46); display:flex; align-items:center; justify-content:center; padding:24px; }
|
|
158
|
+
.vpbox { width:100%; max-width:840px; height:min(788px, 100%); background:#fff; border-radius:20px; box-shadow:0 24px 60px rgba(20,18,15,.28); display:flex; flex-direction:column; overflow:hidden; outline:none; }
|
|
159
|
+
.vphd { padding:18px 16px 0 24px; display:flex; align-items:flex-start; gap:12px; }
|
|
160
|
+
.vphd .tx { flex:1; min-width:0; display:flex; flex-direction:column; }
|
|
161
|
+
.vphd b { font-size:20px; font-weight:600; }
|
|
162
|
+
.vphd small { font-size:13px; color:#5f5b54; }
|
|
163
|
+
.vpx { flex:none; width:44px; height:44px; border-radius:50%; border:0; background:#f3efe6; color:var(--ink); display:grid; place-items:center; cursor:pointer; }
|
|
164
|
+
.vpctl { padding:14px 24px 12px; display:flex; flex-direction:column; gap:10px; border-bottom:1px solid var(--line); }
|
|
165
|
+
.vpctl .fld { padding:10px 14px; }
|
|
166
|
+
#vp-f { display:flex; flex-direction:column; gap:8px; }
|
|
167
|
+
.vpg { display:flex; align-items:flex-start; gap:10px; }
|
|
168
|
+
.vpg .lbl { flex:none; width:36px; padding-top:6px; font-size:13px; color:#5f5b54; }
|
|
169
|
+
.vpg .chips { flex:1; min-width:0; }
|
|
170
|
+
.chips { display:flex; flex-wrap:wrap; gap:6px; }
|
|
171
|
+
.chip { display:inline-flex; align-items:center; gap:4px; min-height:34px; padding:0 12px; border:1px solid #e3dfd6; border-radius:17px; background:#fff; color:var(--ink); font-size:14px; cursor:pointer; }
|
|
172
|
+
.chip small { font-size:12px; color:#6b675f; }
|
|
173
|
+
.chip.on { background:var(--ink); border-color:var(--ink); color:#fff; }
|
|
174
|
+
.chip.on small { color:#e8e4dc; }
|
|
175
|
+
.chip.more { color:var(--accent); border-style:dashed; }
|
|
176
|
+
.vplist { flex:1; min-height:0; overflow-y:auto; padding:12px 24px 16px; background:#faf9f6; -webkit-overflow-scrolling:touch; }
|
|
177
|
+
.vpgrid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; }
|
|
178
|
+
.vcard { display:flex; align-items:flex-start; gap:8px; padding:12px 10px 12px 12px; border-radius:14px; background:#fff; border:2px solid #ece8df; }
|
|
179
|
+
.vcard.on { background:#fffaf3; border-color:var(--accent); }
|
|
180
|
+
.vcard .vpick { flex:1; min-width:0; display:flex; align-items:flex-start; gap:12px; border:0; background:transparent; padding:0; font:inherit; color:inherit; cursor:pointer; }
|
|
181
|
+
.vpnone { padding:60px 0; display:flex; flex-direction:column; align-items:center; gap:10px; color:#5f5b54; }
|
|
182
|
+
.vpft { padding:12px 16px 12px 24px; border-top:1px solid var(--line); display:flex; align-items:center; gap:10px; }
|
|
183
|
+
#vp-st { flex:1; min-width:0; font-size:13px; color:#5f5b54; }
|
|
184
|
+
.vpno { min-height:44px; padding:0 18px; border:0; border-radius:22px; background:#f3efe6; color:var(--ink); font-size:15px; cursor:pointer; }
|
|
185
|
+
.vpgo { min-height:44px; padding:0 22px; border:0; border-radius:22px; background:var(--accent); color:#fff; font-size:15px; font-weight:600; cursor:pointer; }
|
|
186
|
+
.vpgo:disabled { background:#ece8df; color:#6b675f; cursor:default; }
|
|
187
|
+
@media (max-width:700px) {
|
|
188
|
+
.vp { padding:40px 0 0; align-items:stretch; }
|
|
189
|
+
.vpbox { max-width:none; height:100%; border-radius:20px 20px 0 0; }
|
|
190
|
+
.vphd { padding:14px 8px 0 16px; }
|
|
191
|
+
.vphd b { font-size:18px; }
|
|
192
|
+
.vpctl { padding:10px 16px; }
|
|
193
|
+
#vp-f { gap:6px; }
|
|
194
|
+
.vpg { align-items:center; gap:8px; }
|
|
195
|
+
.vpg .lbl { padding-top:0; width:28px; }
|
|
196
|
+
.vpg .chips { flex-wrap:nowrap; overflow-x:auto; margin-right:-16px; padding:0 16px 2px 0; }
|
|
197
|
+
.chip { flex:none; }
|
|
198
|
+
.vplist { padding:10px 12px 14px; }
|
|
199
|
+
.vpgrid { grid-template-columns:minmax(0, 1fr); gap:8px; }
|
|
200
|
+
.vpft { flex-wrap:wrap; padding:10px 16px calc(14px + env(safe-area-inset-bottom)); }
|
|
201
|
+
#vp-st { flex:1 0 100%; text-align:center; font-size:12px; }
|
|
202
|
+
.vpno { flex:1; }
|
|
203
|
+
.vpgo { flex:2; min-height:48px; }
|
|
204
|
+
}
|
|
107
205
|
/* 百炼没配:孩子那边打不开,这块放在右栏最上面 */
|
|
108
206
|
.grp.warn { background:#fff3e8; }
|
|
109
207
|
.grp.warn .it, .grp.warn .box { background:transparent; }
|
|
@@ -115,12 +213,19 @@ ${BRAND_CSS}
|
|
|
115
213
|
.top { align-items:center; justify-content:flex-start; }
|
|
116
214
|
.top .seg { order:0; flex:0 0 260px; }
|
|
117
215
|
.top small { margin-left:auto; }
|
|
216
|
+
.top .devl { margin-left:0; height:34px; padding:0 12px; font-size:14px; }
|
|
118
217
|
.main { display:grid; grid-template-columns:minmax(0,1fr) 380px; gap:22px; align-items:start; }
|
|
119
218
|
.gh { padding:0 6px 6px; }
|
|
120
219
|
}
|
|
220
|
+
.upd { position:relative; display:flex; flex-direction:column; gap:6px; margin:0 0 12px; padding:12px 14px; border-radius:14px; background:var(--card); box-shadow:0 0 0 1px var(--line); font-size:15px; }
|
|
221
|
+
.upd .sub { color:var(--dim); font-size:14px; padding-right:64px; }
|
|
222
|
+
.upd pre { margin:2px 0 0; padding:10px 12px; border-radius:10px; background:var(--paper); font:13px/1.6 ui-monospace,Menlo,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; user-select:all; -webkit-user-select:all; }
|
|
223
|
+
.upd button { position:absolute; top:6px; right:6px; min-height:44px; padding:0 10px; border:0; background:transparent; color:var(--accent); font-size:15px; cursor:pointer; }
|
|
121
224
|
</style></head><body>
|
|
122
225
|
<div class="wrap" id="main">
|
|
123
|
-
<div class="top"><span class="ttl"><span id="ct-mark" class="ct-draw ct-hover">${titleLogo(true)}</span><b id="title">家长</b></span><nav class="seg"><a class="on" href="/parent/home">首页</a><a href="/parent">记录</a></nav><small id="pub"></small></div>
|
|
226
|
+
<div class="top"><span class="ttl"><span id="ct-mark" class="ct-draw ct-hover">${titleLogo(true)}</span><b id="title">家长</b></span><nav class="seg"><a class="on" href="/parent/home">首页</a><a href="/parent">记录</a><a href="/parent/settings">设置</a></nav><small id="pub"></small><a class="devl" href="/dev" target="_blank" title="工作台:细调老师文件、activity(新窗口)">工作台<svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 2h6v6"/><path d="M10 2 3 9"/></svg></a></div>
|
|
227
|
+
<div id="updatebar"></div>
|
|
228
|
+
<div id="qrtop"></div>
|
|
124
229
|
<div class="main">
|
|
125
230
|
<section id="pv" aria-label="孩子看到的首页">
|
|
126
231
|
<div class="pvh"><span>孩子看到的首页 · 点牌上的按钮改它,点后面那张换老师</span><span class="seg" id="pvdev"><button type="button" data-d="tablet">iPad</button><button type="button" data-d="phone">手机</button></span></div>
|
|
@@ -144,6 +249,8 @@ ${pageDomJs(3200)}
|
|
|
144
249
|
const AGAIN = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.3-5.7"></path><path d="M20 4v5h-5"></path></svg>';
|
|
145
250
|
// 学科色服务端定好随老师下发(src/ui/shared/tutor-color.ts)
|
|
146
251
|
const avatar = (t) => { const el = h('span', { class: 'av', style: 'color:' + t.color }); el.innerHTML = '<svg viewBox="0 0 100 100" aria-hidden="true"><circle cx="50" cy="50" r="44"></circle></svg>'; const f = h('span', { class: 'face' }); if (t.avatar && /\.(png|jpe?g|webp|svg)$/i.test(t.avatar)) f.append(h('img', { src: '/api/kid/avatar/' + t.name, alt: '' })); else f.textContent = t.avatar || (t.subject || t.display || '?').slice(0, 1); el.append(f); return el; };
|
|
252
|
+
// 方砖在「每天都在」里的那句:点进去老师怎么带(参数都空着,老师先请孩子拍材料)
|
|
253
|
+
const TILE_NOTE = { preview: '孩子拍新课的那页,老师带着先读一读、问一问', check: '作业写完拍给老师,一次看一处、改一处', review: '学过的拿出来,挑一点再练一练' };
|
|
147
254
|
const sw = (on, label, click, disabled) => h('button', { type: 'button', class: 'sw', role: 'switch', 'aria-checked': on ? 'true' : 'false', 'aria-label': label, disabled: disabled || null, on: { click } });
|
|
148
255
|
const md = (d) => Number(d.slice(5, 7)) + '/' + Number(d.slice(8, 10));
|
|
149
256
|
// 留多久:只今天 = 今天;到周日 = 这周日(今天是周日就是今天);一直 = 不写
|
|
@@ -183,6 +290,7 @@ ${pageDomJs(3200)}
|
|
|
183
290
|
if (!t) return;
|
|
184
291
|
if (t.name !== cur) choose(t.name, true);
|
|
185
292
|
if (ev.act === 'button') { if (ev.id === 'new') toast('「新话题」是应用自己加的,一直在第一个,这里改不了'); else if (typeof ev.id === 'number') pick(ev.id); }
|
|
293
|
+
else if (ev.act === 'tile') { const el = $('#daily-tile-' + ev.id); if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
|
|
186
294
|
else if (ev.act === 'extra') { if (ev.kind === 'dictation') editWords(t); else { const el = $('#daily-talk'); if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' }); } }
|
|
187
295
|
};
|
|
188
296
|
for (const b of document.querySelectorAll('#pvdev button')) b.addEventListener('click', () => { dev = picked = b.dataset.d; try { localStorage.setItem('parent-home-dev', dev); } catch (e) {} fit(); });
|
|
@@ -209,7 +317,7 @@ ${pageDomJs(3200)}
|
|
|
209
317
|
const st = BL.st;
|
|
210
318
|
if (!st) { blEl.replaceChildren(); return; }
|
|
211
319
|
const rows = [];
|
|
212
|
-
if (st.ready) rows.push(h('div', { class: 'it' }, h('span', { class: 'tx' }, h('b', {}, '配好了'), h('small', {}, (st.keyTail ? 'key …' + st.keyTail + ' · 业务空间 ' + st.workspaceId : '测试用的假百炼')
|
|
320
|
+
if (st.ready) rows.push(h('div', { class: 'it' }, h('span', { class: 'tx' }, h('b', {}, '配好了'), h('small', {}, (st.keyTail ? 'key …' + st.keyTail + ' · 业务空间 ' + st.workspaceId : '测试用的假百炼'))),
|
|
213
321
|
h('button', { type: 'button', class: 'x', disabled: BL.busy || null, on: { click: blTest } }, BL.busy && !BL.open ? '在试…' : '试连'),
|
|
214
322
|
st.canEdit ? h('button', { type: 'button', class: 'x', on: { click: () => { BL.open = !BL.open; BL.msg = ''; blDraw(); } } }, BL.open ? '收起' : '换一把') : null));
|
|
215
323
|
else rows.push(h('div', { class: 'box' }, h('b', {}, '还没配,孩子那边打不开'), h('div', { class: 'note', style: 'padding:0' }, '老师开口和想讲稿都走百炼,要百炼「按量付费」的 API key(控制台「密钥管理」里创建)和业务空间 id(控制台的业务空间详情里看);Token Plan 的 sk-sp- 不行。存在这台电脑上,不进孩子的文件夹。')));
|
|
@@ -222,9 +330,65 @@ ${pageDomJs(3200)}
|
|
|
222
330
|
blEl.replaceChildren(h('div', { class: 'gh' }, h('span', {}, '百炼 · 老师开口用的')), h('div', { class: 'grp' + (st.ready ? '' : ' warn') }, ...rows));
|
|
223
331
|
};
|
|
224
332
|
|
|
333
|
+
// ---- 扫码打开:孩子的平板一张(孩子端)、家长自己的手机一张(家长端);服务端现画,地址同 /parent/qr。还没有孩子的设备连过就开着,连过收起;家长开合过就记住 ----
|
|
334
|
+
const QR = { d: null, ip: false, open: null, to: 'kid' };
|
|
335
|
+
try { const v = localStorage.getItem('parent-home-qr'); if (v === 'open' || v === 'closed') QR.open = v === 'open'; } catch (e) {}
|
|
336
|
+
const qrEl = h('div', { id: 'kidqr' });
|
|
337
|
+
const qrToggle = (open) => { QR.open = open; try { localStorage.setItem('parent-home-qr', open ? 'open' : 'closed'); } catch (e) {} qrDraw(); };
|
|
338
|
+
const qrWhen = (iso) => { const t = new Date(iso); if (Number.isNaN(t.getTime())) return ''; const day = t.getFullYear() + '-' + String(t.getMonth() + 1).padStart(2, '0') + '-' + String(t.getDate()).padStart(2, '0'); return (D && day === D.today ? '今天 ' : md(day) + ' ') + t.toTimeString().slice(0, 5); };
|
|
339
|
+
const QR_ICON = '<svg width="30" height="30" viewBox="0 0 30 30" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3" y="3" width="9" height="9" rx="1.5"></rect><rect x="18" y="3" width="9" height="9" rx="1.5"></rect><rect x="3" y="18" width="9" height="9" rx="1.5"></rect><path d="M18 18h3v3h-3zM24 24h3v3h-3zM24 18h3M18 24v3" stroke-linecap="round"></path></svg>';
|
|
340
|
+
const host = (u) => u.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
341
|
+
const qrDraw = () => {
|
|
342
|
+
const d = QR.d;
|
|
343
|
+
if (!d || !d.ready) { qrEl.replaceChildren(); return; }
|
|
344
|
+
const open = QR.open === null ? d.devices === 0 : QR.open;
|
|
345
|
+
const seen = d.devices ? '孩子的平板 ' + d.devices + ' 台连过 · 上次' + qrWhen(d.last) : '还没有平板连过';
|
|
346
|
+
if (!open) {
|
|
347
|
+
qrEl.replaceChildren(h('div', { class: 'grp' }, h('button', { type: 'button', class: 'it', 'aria-expanded': 'false', on: { click: () => qrToggle(true) } },
|
|
348
|
+
h('span', { style: 'flex:none;display:flex', html: QR_ICON }), h('span', { class: 'tx' }, h('b', {}, '扫码打开'), h('small', {}, seen)), h('span', { class: 'lnk', style: 'font-size:14px' }, '展开'))));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const kid = QR.to === 'kid', c = kid ? d : (d.parent || d);
|
|
352
|
+
const useIp = QR.ip && c.ip;
|
|
353
|
+
const seg = h('div', { class: 'seg', role: 'group', 'aria-label': '给谁扫' }, ...[['kid', '孩子的平板'], ['parent', '我的手机']].map(([k, l]) =>
|
|
354
|
+
h('button', { type: 'button', class: QR.to === k ? 'on' : '', 'aria-pressed': QR.to === k ? 'true' : 'false', on: { click: () => { QR.to = k; qrDraw(); } } }, l)));
|
|
355
|
+
const print = '/parent/qr?' + new URLSearchParams(Object.assign({}, useIp ? { via: 'ip' } : {}, kid ? {} : { to: 'parent' })).toString();
|
|
356
|
+
qrEl.replaceChildren(
|
|
357
|
+
h('div', { class: 'gh' }, h('span', {}, '扫码打开'), h('button', { type: 'button', class: 'x', 'aria-expanded': 'true', on: { click: () => qrToggle(false) } }, '收起')),
|
|
358
|
+
h('div', { class: 'grp' }, h('div', { class: 'box' }, seg,
|
|
359
|
+
h('div', { class: 'qrrow' }, h('div', { class: 'qrimg', html: (useIp ? c.svgIp : c.svg) || '' }),
|
|
360
|
+
h('b', { style: 'font-weight:600' }, kid ? '用孩子平板的相机扫一下' : '用你自己的手机扫一下'),
|
|
361
|
+
h('span', { class: 'lbl', style: 'line-height:1.5' }, kid ? '打开的就是孩子的首页。在 Safari 里「添加到主屏幕」,以后点图标就进来。' : '打开的是家长端:看记录、排首页。也可以添加到主屏幕。')),
|
|
362
|
+
h('div', { class: 'qrurl' }, useIp ? c.ip : c.url),
|
|
363
|
+
d.https ? null : h('div', { class: 'qrwarn' }, d.notes[0] || ''),
|
|
364
|
+
h('div', { class: 'row2', style: 'font-size:14px' },
|
|
365
|
+
c.ip ? h('button', { type: 'button', class: 'x', style: 'height:auto;padding:0;font-size:14px;text-align:left', on: { click: () => { QR.ip = !QR.ip; qrDraw(); } } }, useIp ? '换回 ' + host(c.url) : '扫了打不开?换成 ' + host(c.ip)) : h('span'),
|
|
366
|
+
h('span', { style: 'display:flex;gap:14px;flex:none' },
|
|
367
|
+
h('a', { class: 'lnk kidl', style: 'font-size:14px', href: '/', target: '_blank' }, '孩子端 ↗'),
|
|
368
|
+
h('a', { class: 'lnk', style: 'font-size:14px', href: print, target: '_blank' }, '打印 ›'))))),
|
|
369
|
+
h('div', { class: 'note' }, d.https ? (d.notes[0] || '') : '', d.devices ? '' : (d.https ? ' ' : '') + '还没有平板连进来过,所以这块先开着;连过以后自动收起。'));
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// 有新版本(服务每天问一次 npm,machine/update.ts):只在家长端说,孩子端不说;点「知道了」这个版本不再提,读不了 localStorage 就每次都说
|
|
373
|
+
const UPGRADE_STEPS = ${JSON.stringify(upgradeSteps())};
|
|
374
|
+
const showUpdate = async () => {
|
|
375
|
+
const box = $('#updatebar');
|
|
376
|
+
const hh = await api('GET', '/api/health').catch(() => null);
|
|
377
|
+
const latest = hh && hh.latest;
|
|
378
|
+
let seen = null;
|
|
379
|
+
try { seen = localStorage.getItem('cotutor.updateSeen'); } catch {}
|
|
380
|
+
if (!latest || seen === latest) { box.replaceChildren(); return; }
|
|
381
|
+
box.replaceChildren(h('div', { class: 'upd' },
|
|
382
|
+
h('b', {}, '有新版本 ' + latest),
|
|
383
|
+
h('span', { class: 'sub' }, '现在是 ' + hh.version + '。挑孩子不用的时候,在这台电脑的终端里依次运行:'),
|
|
384
|
+
h('pre', {}, UPGRADE_STEPS.map((s) => h('div', {}, s))),
|
|
385
|
+
h('button', { type: 'button', on: { click: () => { try { localStorage.setItem('cotutor.updateSeen', latest); } catch {} box.replaceChildren(); } } }, '知道了')));
|
|
386
|
+
};
|
|
225
387
|
const load = async () => {
|
|
388
|
+
showUpdate();
|
|
226
389
|
try { D = await api('GET', '/api/parent/home'); } catch (e) { toast('读不到:' + e.message); return; }
|
|
227
390
|
BL.st = await api('GET', '/api/parent/bailian').catch(() => null);
|
|
391
|
+
QR.d = await api('GET', '/api/parent/qr').catch(() => null);
|
|
228
392
|
document.title = D.title + ' · 家长';
|
|
229
393
|
$('#title').textContent = D.title;
|
|
230
394
|
$('#pub').textContent = D.home ? md(D.home.publishedAt.slice(0, 10)) + ' ' + new Date(D.home.publishedAt).toTimeString().slice(0, 5) + ' 发布 · 改了马上到孩子的首页' : '还没发布过';
|
|
@@ -239,12 +403,12 @@ ${pageDomJs(3200)}
|
|
|
239
403
|
};
|
|
240
404
|
const tutorOf = () => D.tutors.find((t) => t.name === cur);
|
|
241
405
|
/** 看哪位老师;fromPreview = 预览里换过去的,不用再告诉它 */
|
|
242
|
-
const choose = (name, fromPreview) => { if (name === cur) return; cur = name; sel = null; dirty = false; draft = { edit: null, add: null }; render(); if (!fromPreview) pvSync(); };
|
|
406
|
+
const choose = (name, fromPreview) => { if (name === cur) return; cur = name; sel = null; lookStop(); LOOK.open = null; dirty = false; draft = { edit: null, add: null }; render(); if (!fromPreview) pvSync(); };
|
|
243
407
|
/** 改第 k 颗(-1 = 加一个):宽屏在右栏,窄屏整屏的表单 */
|
|
244
408
|
const pick = (k) => {
|
|
245
409
|
const t = tutorOf(); if (!t) return;
|
|
246
410
|
if (!WIDE.matches) { editButton(t, k); return; }
|
|
247
|
-
sel = k; dirty = false; draft = { edit: null, add: null }; render();
|
|
411
|
+
sel = k; lookStop(); LOOK.open = null; dirty = false; draft = { edit: null, add: null }; render();
|
|
248
412
|
setTimeout(() => { const el = $('#b-label'); if (el) el.focus(); }, 50);
|
|
249
413
|
};
|
|
250
414
|
const setExtras = (t, kind, on) => {
|
|
@@ -316,6 +480,8 @@ ${pageDomJs(3200)}
|
|
|
316
480
|
// 每天都在
|
|
317
481
|
const has = (k) => t.extras.includes(k);
|
|
318
482
|
const daily = [];
|
|
483
|
+
// 方砖:预习、检查作业、复习,老师家里有那个 activity 文件的才列;关掉就是孩子牌上不出那一块
|
|
484
|
+
for (const x of t.tiles || []) daily.push(h('div', { class: 'it', id: 'daily-tile-' + x.id }, h('span', { class: 'tx' }, h('b', {}, x.label), h('small', {}, x.usable ? TILE_NOTE[x.id] : '「' + x.label + '」那份 activity 文件写坏了,孩子那边先不出(工作台看)')), sw(x.on, x.label, () => save('PATCH', '/api/parent/settings', { tutors: { [t.name]: { tiles: { [x.id]: !x.on } } } }))));
|
|
319
485
|
daily.push(h('div', { class: 'it' }, h('span', { class: 'tx' }, h('b', {}, '听写'), h('small', {}, '拍照听写一直在;设了这周的词,孩子点「听写」直接写')), sw(has('dictation'), '听写', () => setExtras(t, 'dictation', !has('dictation')))));
|
|
320
486
|
if (has('dictation') || t.dictation) daily.push(h('button', { type: 'button', class: 'it', on: { click: () => editWords(t) } }, h('span', { class: 'tx' }, h('b', {}, '这周的词'), h('small', {}, t.dictation && t.dictation.length ? t.dictation.length + ' 个词:' + t.dictation.map((w) => w.chars).join('、') : '还没设')), h('span', { class: 'chev', html: CHEV })));
|
|
321
487
|
daily.push(h('div', { class: 'it', id: 'daily-talk' }, h('span', { class: 'tx' }, h('b', {}, '口语课'), h('small', {}, D.talk.enabled ? '孩子自己拍今天学的那页,先跟读再打电话' : '口语课没开(cotutor.json 的 talk.enabled)')), sw(has('talk'), '口语课', () => setExtras(t, 'talk', !has('talk')), !D.talk.enabled && !has('talk'))));
|
|
@@ -328,14 +494,17 @@ ${pageDomJs(3200)}
|
|
|
328
494
|
h('div', { class: 'row2' }, h('a', { class: 'lnk', href: '/parent' }, '看回放 ›'), h('button', { type: 'button', class: 'step', style: 'border:0;background:transparent;color:var(--accent);cursor:pointer', on: { click: () => save('PATCH', '/api/parent/settings', { talk: { note: note.value } }, '存好了') } }, '存给老师的话'))));
|
|
329
495
|
}
|
|
330
496
|
const extras = h('div', {}, h('div', { class: 'gh' }, h('span', {}, '每天都在 · ' + t.display)), h('div', { class: 'grp' }, ...daily));
|
|
331
|
-
const self = h('div', {}, h('div', { class: 'gh' }, h('span', {}, '这位老师')), h('div', { class: 'grp' },
|
|
497
|
+
const self = h('div', {}, h('div', { class: 'gh' }, h('span', {}, '这位老师 · ' + t.display)), h('div', { class: 'grp' },
|
|
498
|
+
h('button', { type: 'button', class: 'it', on: { click: () => openLook(t) } }, avatar(t), h('span', { class: 'tx' }, h('b', {}, '样子与声音'), h('small', {}, '说话:' + t.voices.mouth + ' · 点读:' + (t.voices.card || '没配'))), h('span', { class: 'chev', html: CHEV })),
|
|
332
499
|
h('div', { class: 'it' }, h('span', { class: 'tx' }, h('b', {}, '在孩子的首页上')), sw(!t.hidden, '在首页上', () => save('PATCH', '/api/parent/settings', { tutors: { [t.name]: { hidden: !t.hidden } } })))));
|
|
333
|
-
const pane = !wide ? null : sel === null || (sel >= 0 && !t.buttons[sel])
|
|
500
|
+
const pane = !wide ? null : LOOK.open === t.name ? lookPanel() : sel === null || (sel >= 0 && !t.buttons[sel])
|
|
334
501
|
? h('div', { class: 'grp hint' }, '点左边牌上的按钮,在这里改它的字和讲法;', h('br'), '改的时候牌上跟着变,点「发布」才到孩子那边。')
|
|
335
502
|
: inlineForm(t, sel);
|
|
336
503
|
blDraw();
|
|
504
|
+
qrDraw();
|
|
337
505
|
const blTop = BL.st && !BL.st.ready;
|
|
338
|
-
$('#
|
|
506
|
+
if (wide) $('#qrtop').replaceChildren(); else $('#qrtop').replaceChildren(qrEl);
|
|
507
|
+
$('#body').replaceChildren(h('div', {}, blTop ? blEl : null, wide ? qrEl : null, pane, extras, self, blTop ? null : blEl));
|
|
339
508
|
};
|
|
340
509
|
|
|
341
510
|
// ---- 改一个按钮的表单(k = -1 是新加的开场按钮):窄屏整屏盖上,宽屏在右栏;预览里那颗跟着变 ----
|
|
@@ -382,6 +551,167 @@ ${pageDomJs(3200)}
|
|
|
382
551
|
};
|
|
383
552
|
const openSheet = (...kids) => { $('#sheet-in').replaceChildren(...kids); $('#sheet').classList.add('on'); window.scrollTo(0, 0); };
|
|
384
553
|
const closeSheet = () => $('#sheet').classList.remove('on');
|
|
554
|
+
|
|
555
|
+
// ---- 样子与声音:头像点了就存;两种声音各一行(现在用的那个、试听),「换一个」开弹窗挑(src/app/http/parent-look.ts)。宽屏在右栏,窄屏整屏 ----
|
|
556
|
+
const LOOK = { open: null, d: null, playing: null, audio: null, saved: false };
|
|
557
|
+
const lookUrl = (name) => '/api/parent/tutors/' + encodeURIComponent(name) + '/look';
|
|
558
|
+
const LK_PLAY = '<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M4 2.5v9l7.5-4.5z" fill="currentColor"></path></svg>';
|
|
559
|
+
const LK_PAUSE = '<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><rect x="3" y="2.5" width="3" height="9" rx="1" fill="currentColor"></rect><rect x="8" y="2.5" width="3" height="9" rx="1" fill="currentColor"></rect></svg>';
|
|
560
|
+
const LK_X = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9"></path></svg>';
|
|
561
|
+
function lookStop() { if (LOOK.audio) { LOOK.audio.pause(); URL.revokeObjectURL(LOOK.audio.src); } LOOK.audio = null; LOOK.playing = null; }
|
|
562
|
+
const lookDraw = () => { const el = $('#look'); if (el) el.replaceWith(lookPanel()); if (VP.kind) vpPaint(); };
|
|
563
|
+
const openLook = async (t) => {
|
|
564
|
+
lookStop(); vpClose(); LOOK.open = t.name; LOOK.d = null; LOOK.saved = false;
|
|
565
|
+
if (WIDE.matches) { sel = null; render(); } else openSheet(lookPanel());
|
|
566
|
+
try { LOOK.d = await api('GET', lookUrl(t.name)); } catch (e) { toast('读不到:' + e.message); }
|
|
567
|
+
lookDraw();
|
|
568
|
+
};
|
|
569
|
+
const closeLook = () => { lookStop(); vpClose(); LOOK.open = null; if (WIDE.matches) render(); else closeSheet(); };
|
|
570
|
+
let savedTimer = 0;
|
|
571
|
+
const lookSave = async (patch) => {
|
|
572
|
+
try { LOOK.d = await api('PUT', lookUrl(LOOK.open), patch); } catch (e) { toast('没存上:' + e.message); return false; }
|
|
573
|
+
LOOK.saved = true; lookDraw(); clearTimeout(savedTimer); savedTimer = setTimeout(() => { LOOK.saved = false; lookDraw(); }, 1600);
|
|
574
|
+
// 头像变了预览里的牌、先后那排跟着变
|
|
575
|
+
await load(); pvSync(true);
|
|
576
|
+
return true;
|
|
577
|
+
};
|
|
578
|
+
// 试听:服务端念它定的那句(有缓存);同时只放一个,再点一下停
|
|
579
|
+
const lookPlay = async (kind, voice) => {
|
|
580
|
+
const key = kind + ' ' + voice;
|
|
581
|
+
if (LOOK.playing === key) { lookStop(); lookDraw(); return; }
|
|
582
|
+
lookStop(); LOOK.playing = key; lookDraw();
|
|
583
|
+
try {
|
|
584
|
+
const r = await fetch(lookUrl(LOOK.open) + '/preview?kind=' + kind + '&voice=' + encodeURIComponent(voice), { cache: 'no-store' });
|
|
585
|
+
if (!r.ok) { const j = await r.json().catch(() => null); throw new Error((j && j.message) || 'HTTP ' + r.status); }
|
|
586
|
+
const blob = await r.blob();
|
|
587
|
+
if (LOOK.playing !== key) return;
|
|
588
|
+
const a = new Audio(URL.createObjectURL(blob)); LOOK.audio = a;
|
|
589
|
+
a.onended = () => { if (LOOK.playing === key) { lookStop(); lookDraw(); } };
|
|
590
|
+
await a.play();
|
|
591
|
+
} catch (e) { if (LOOK.playing === key) { lookStop(); lookDraw(); } toast('试听不了:' + e.message); }
|
|
592
|
+
};
|
|
593
|
+
const lookFace = (o) => o.img ? h('img', { src: o.img, alt: '' }) : (o.text || '?');
|
|
594
|
+
|
|
595
|
+
// 一个声音长什么样:圆里一个「女 / 男」(底色也分),名字、年纪,标签(语言口音、童声、场景、提醒、现在用的 / 出厂),特点
|
|
596
|
+
const ageOf = (a) => (a <= 12 ? '童声' : a <= 19 ? '少年' : a <= 35 ? '青年' : '成熟');
|
|
597
|
+
const vFace = (v) => h('span', { class: 'vface' + (v.gender === '女' ? ' g-f' : v.gender === '男' ? ' g-m' : '') }, v.gender || '?');
|
|
598
|
+
const vTags = (kind, v, current) => {
|
|
599
|
+
const out = [];
|
|
600
|
+
const plain = kind === 'mouth' ? v.lang === '普通话' : v.lang === '中文';
|
|
601
|
+
out.push(h('span', { class: 'vt ' + (plain ? 't-lang' : kind === 'mouth' ? 't-dia' : 't-en') }, v.lang));
|
|
602
|
+
if (v.age && v.age <= 12) out.push(h('span', { class: 'vt t-kid' }, '童声'));
|
|
603
|
+
if (v.scene) out.push(h('span', { class: 'vt t-line' }, v.scene));
|
|
604
|
+
if (v.warn) out.push(h('span', { class: 'vt t-warn' }, v.warn));
|
|
605
|
+
if (v.voice === current) out.push(h('span', { class: 'vt t-line' }, '现在用的'));
|
|
606
|
+
else if (v.shipped) out.push(h('span', { class: 'vt t-line' }, '出厂'));
|
|
607
|
+
return out;
|
|
608
|
+
};
|
|
609
|
+
const vPlayBtn = (kind, v) => { const playing = LOOK.playing === kind + ' ' + v.voice; return h('button', { type: 'button', class: 'vplay' + (playing ? ' on' : ''), 'aria-label': (playing ? '停 ' : '试听 ') + v.name, html: playing ? LK_PAUSE : LK_PLAY, on: { click: (e) => { e.stopPropagation(); if (VP.kind) { VP.sel = v.voice; } lookPlay(kind, v.voice); } } }); };
|
|
610
|
+
const vBody = (kind, v, current) => h('span', { class: 'vtx' },
|
|
611
|
+
h('span', { class: 'vnm' }, h('b', {}, v.name), v.age ? h('small', {}, v.age + ' 岁') : null),
|
|
612
|
+
h('span', { class: 'vtags' }, ...vTags(kind, v, current)),
|
|
613
|
+
v.trait ? h('small', { class: 'vtr' }, v.trait) : null);
|
|
614
|
+
|
|
615
|
+
// ---- 选声音的弹窗:宽屏居中,窄屏从底下升起;搜、筛(每个选项后面是筛出来几个,跟着别的条件变)、试听、选中,按「用」才存 ----
|
|
616
|
+
const VP_STEP = 60;
|
|
617
|
+
const VP = { kind: null, sel: null, q: '', f: {}, n: VP_STEP, more: false };
|
|
618
|
+
const VP_DIMS = {
|
|
619
|
+
mouth: [['g', '性别', (v) => v.gender], ['lang', '口音', (v) => v.lang]],
|
|
620
|
+
card: [['g', '性别', (v) => v.gender], ['lang', '语言', (v) => v.lang], ['age', '年纪', (v) => (v.age ? ageOf(v.age) : '')], ['scene', '场景', (v) => v.scene || '']],
|
|
621
|
+
};
|
|
622
|
+
const VP_ORDER = { g: ['女', '男'], age: ['童声', '少年', '青年', '成熟'] };
|
|
623
|
+
const SCENE_SHOWN = 7;
|
|
624
|
+
const vpPass = (v, skip) => {
|
|
625
|
+
const q = VP.q.trim().toLowerCase();
|
|
626
|
+
if (q && ![v.name, v.trait, v.scene || '', v.lang, v.voice].some((x) => x.toLowerCase().includes(q))) return false;
|
|
627
|
+
return VP_DIMS[VP.kind].every(([k, , of]) => k === skip || !VP.f[k] || of(v) === VP.f[k]);
|
|
628
|
+
};
|
|
629
|
+
const vpOpen = (kind) => {
|
|
630
|
+
if (!LOOK.d) return;
|
|
631
|
+
VP.kind = kind; VP.sel = LOOK.d[kind].current; VP.q = ''; VP.f = {}; VP.n = VP_STEP; VP.more = false;
|
|
632
|
+
const t = D && D.tutors.find((x) => x.name === LOOK.open);
|
|
633
|
+
const q = h('input', { class: 'fld', type: 'search', placeholder: kind === 'mouth' ? '搜名字或特点,比如 温柔、沉稳' : '搜名字或特点,比如 温柔、沉稳、播音', 'aria-label': '搜声音', on: { input: (e) => { VP.q = e.target.value; VP.n = VP_STEP; vpPaint(); } } });
|
|
634
|
+
const box = h('div', { class: 'vpbox', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'vp-t', tabindex: '-1' },
|
|
635
|
+
h('div', { class: 'vphd' },
|
|
636
|
+
h('div', { class: 'tx' }, h('b', { id: 'vp-t' }, (t ? t.display : '') + ' · ' + (kind === 'mouth' ? '说话的声音' : '点读的声音')),
|
|
637
|
+
h('small', {}, (kind === 'mouth' ? '孩子听到老师开口就是它' : '点读卡、单词卡、听写念词都用它') + ' · 一共 ' + LOOK.d[kind].options.length + ' 个')),
|
|
638
|
+
h('button', { type: 'button', class: 'vpx', 'aria-label': '关掉', html: LK_X, on: { click: vpClose } })),
|
|
639
|
+
h('div', { class: 'vpctl' }, q, h('div', { id: 'vp-f' })),
|
|
640
|
+
h('div', { class: 'vplist', id: 'vp-list' }),
|
|
641
|
+
h('div', { class: 'vpft' }, h('span', { id: 'vp-st' }), h('button', { type: 'button', class: 'vpno', on: { click: vpClose } }, '取消'), h('button', { type: 'button', id: 'vp-use', class: 'vpgo', on: { click: vpUse } })));
|
|
642
|
+
const el = h('div', { id: 'vp', class: 'vp', on: { click: (e) => { if (e.target === el) vpClose(); }, keydown: (e) => { if (e.key === 'Escape') vpClose(); } } }, box);
|
|
643
|
+
document.body.append(el); document.body.style.overflow = 'hidden';
|
|
644
|
+
vpPaint(); box.focus();
|
|
645
|
+
};
|
|
646
|
+
function vpClose() {
|
|
647
|
+
if (!VP.kind) return;
|
|
648
|
+
if (LOOK.playing && LOOK.playing.startsWith(VP.kind + ' ')) lookStop();
|
|
649
|
+
VP.kind = null; const el = $('#vp'); if (el) el.remove(); document.body.style.overflow = '';
|
|
650
|
+
const p = $('#look'); if (p) lookDraw();
|
|
651
|
+
}
|
|
652
|
+
async function vpUse() {
|
|
653
|
+
const kind = VP.kind, voice = VP.sel;
|
|
654
|
+
if (!kind || !voice || voice === LOOK.d[kind].current) return;
|
|
655
|
+
if (await lookSave(kind === 'mouth' ? { mouth: voice } : { voice })) { vpClose(); toast(kind === 'mouth' ? '存好了,下次开口就换' : '存好了,配过音的卡下次点开才换'); }
|
|
656
|
+
}
|
|
657
|
+
function vpPaint() {
|
|
658
|
+
const kind = VP.kind; if (!kind || !LOOK.d) return;
|
|
659
|
+
const block = LOOK.d[kind], all = block.options;
|
|
660
|
+
// 筛选:一组一排,选项后面是「别的条件不变、换成它」筛出来几个
|
|
661
|
+
const groups = VP_DIMS[kind].map(([k, label, of]) => {
|
|
662
|
+
const vals = [];
|
|
663
|
+
all.forEach((v) => { const x = of(v); if (x && !vals.includes(x)) vals.push(x); });
|
|
664
|
+
const n = (x) => all.filter((v) => vpPass(v, k) && (!x || of(v) === x)).length;
|
|
665
|
+
let list = VP_ORDER[k] ? VP_ORDER[k].filter((x) => vals.includes(x)) : vals.slice().sort((a, b) => all.filter((v) => of(v) === b).length - all.filter((v) => of(v) === a).length);
|
|
666
|
+
if (k === 'lang' && kind === 'mouth') list = ['普通话'].concat(list.filter((x) => x !== '普通话'));
|
|
667
|
+
const cut = k === 'scene' && !VP.more && list.length > SCENE_SHOWN + 1;
|
|
668
|
+
const shown = cut ? list.slice(0, SCENE_SHOWN).concat(VP.f.scene && list.indexOf(VP.f.scene) >= SCENE_SHOWN ? [VP.f.scene] : []) : list;
|
|
669
|
+
const chips = [''].concat(shown).map((x) => { const on = (VP.f[k] || '') === x; return h('button', { type: 'button', class: 'chip' + (on ? ' on' : ''), 'aria-pressed': on ? 'true' : 'false', on: { click: () => { VP.f[k] = x; VP.n = VP_STEP; vpPaint(); } } }, x || '全部', h('small', {}, String(n(x)))); });
|
|
670
|
+
if (cut) chips.push(h('button', { type: 'button', class: 'chip more', on: { click: () => { VP.more = true; vpPaint(); } } }, '更多 ' + (list.length - SCENE_SHOWN) + ' 种'));
|
|
671
|
+
return h('div', { class: 'vpg' }, h('span', { class: 'lbl' }, label), h('div', { class: 'chips' }, ...chips));
|
|
672
|
+
});
|
|
673
|
+
$('#vp-f').replaceChildren(...groups);
|
|
674
|
+
const hits = all.filter((v) => vpPass(v, ''));
|
|
675
|
+
const shown = hits.slice(0, VP.n);
|
|
676
|
+
const cards = shown.map((v) => {
|
|
677
|
+
const on = v.voice === VP.sel;
|
|
678
|
+
return h('div', { class: 'vcard' + (on ? ' on' : '') },
|
|
679
|
+
h('button', { type: 'button', class: 'vpick', role: 'radio', 'aria-checked': on ? 'true' : 'false', on: { click: () => { VP.sel = v.voice; vpPaint(); } } }, vFace(v), vBody(kind, v, block.current)),
|
|
680
|
+
vPlayBtn(kind, v));
|
|
681
|
+
});
|
|
682
|
+
const left = hits.length - shown.length;
|
|
683
|
+
const list = $('#vp-list'), top = list.scrollTop;
|
|
684
|
+
list.replaceChildren(
|
|
685
|
+
hits.length ? h('div', { class: 'vpgrid', role: 'radiogroup', 'aria-label': kind === 'mouth' ? '说话的声音' : '点读的声音' }, ...cards)
|
|
686
|
+
: h('div', { class: 'vpnone' }, '没有这样的声音', h('button', { type: 'button', class: 'vpno', on: { click: () => { VP.q = ''; VP.f = {}; const i = $('#vp .vpctl input'); if (i) i.value = ''; vpPaint(); } } }, '清掉筛选')),
|
|
687
|
+
left > 0 ? h('button', { type: 'button', class: 'addb', style: 'width:100%;margin-top:12px', on: { click: () => { VP.n += VP_STEP; vpPaint(); } } }, '再看 ' + Math.min(VP_STEP, left) + ' 个(还有 ' + left + ' 个)') : null);
|
|
688
|
+
list.scrollTop = top;
|
|
689
|
+
const picked = all.find((v) => v.voice === VP.sel), same = VP.sel === block.current;
|
|
690
|
+
$('#vp-st').textContent = '筛出 ' + hits.length + ' 个 · 试听念服务端定的一句';
|
|
691
|
+
const go = $('#vp-use'); go.textContent = same ? '现在就是它' : '用「' + (picked ? picked.name : '') + '」'; go.disabled = same;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function lookPanel() {
|
|
695
|
+
const t = D && D.tutors.find((x) => x.name === LOOK.open), d = LOOK.d;
|
|
696
|
+
const head = h('div', { class: 'nav' }, h('button', { type: 'button', on: { click: closeLook } }, '‹ 返回'), h('b', {}, (t ? t.display : '') + ' · 样子与声音'), h('span', { class: 'lksaved', 'aria-live': 'polite' }, LOOK.saved ? '存好了' : ''));
|
|
697
|
+
if (!t || !d) return h('div', { id: 'look', class: 'lk' }, head, h('div', { class: 'grp hint' }, '在读…'));
|
|
698
|
+
const curA = d.avatar.options.find((o) => o.value === d.avatar.current);
|
|
699
|
+
const big = h('div', { class: 'lkbig' }, curA ? lookFace(curA) : avatar(t));
|
|
700
|
+
const grid = h('div', { class: 'lkgrid' }, ...d.avatar.options.map((o) => h('button', { type: 'button', class: 'lkopt' + (o.value === d.avatar.current ? ' on' : ''), 'aria-pressed': o.value === d.avatar.current ? 'true' : 'false', 'aria-label': o.label || o.text, on: { click: () => { if (o.value !== d.avatar.current) lookSave({ avatar: o.value }); } } },
|
|
701
|
+
h('span', { class: 'lkf' }, lookFace(o)), o.label ? h('small', {}, o.label) : null)));
|
|
702
|
+
const cur = (kind, label) => {
|
|
703
|
+
const block = d[kind], v = block.options.find((o) => o.voice === block.current);
|
|
704
|
+
return h('div', { class: 'vcur', id: 'lk-' + kind },
|
|
705
|
+
h('div', { class: 'lbl' }, label),
|
|
706
|
+
v ? h('div', { class: 'vrow2' }, vFace(v), vBody(kind, v, null), vPlayBtn(kind, v)) : h('div', { class: 'lbl' }, '还没配'),
|
|
707
|
+
h('button', { type: 'button', class: 'vchg', on: { click: () => vpOpen(kind) } }, '换一个 · ' + block.options.length + ' 个可选', h('span', { html: CHEV })));
|
|
708
|
+
};
|
|
709
|
+
return h('div', { id: 'look', class: 'lk' }, head, big,
|
|
710
|
+
h('div', {}, h('div', { class: 'gh' }, h('span', {}, '头像 · 孩子的首页、老师页上都是这张')), grid),
|
|
711
|
+
h('div', {}, h('div', { class: 'gh' }, h('span', {}, '声音')),
|
|
712
|
+
h('div', { class: 'grp' }, cur('mouth', '说话的声音 · 老师开口说话'), cur('card', '点读的声音 · 点读卡、单词卡、听写念词')),
|
|
713
|
+
h('div', { class: 'note' }, '改了说话的声音,下次开口就换;已经配好音的卡,下次点开才换。')));
|
|
714
|
+
}
|
|
385
715
|
const editButton = (t, k) => {
|
|
386
716
|
const f = buttonForm(t, k, () => closeSheet());
|
|
387
717
|
openSheet(h('div', { class: 'nav' }, h('button', { type: 'button', on: { click: () => { dirty = false; draft = { edit: null, add: null }; closeSheet(); pvSync(); } } }, '取消'), h('b', {}, f.title), f.go), ...f.fields, f.del ? h('button', { type: 'button', class: 'danger', on: { click: () => f.del.click() } }, '删掉这个按钮') : null);
|
|
@@ -73,7 +73,9 @@ ${BRAND_CSS}
|
|
|
73
73
|
#phead { display:flex; flex-wrap:wrap; align-items:center; gap:12px 20px; }
|
|
74
74
|
#pnokey { display:block; padding:12px 16px; border-radius:14px; background:#fff3e8; color:#b4531f; font-size:15px; text-decoration:none; }
|
|
75
75
|
#pnokey[hidden] { display:none; }
|
|
76
|
-
#phead #ttl { flex:1 0
|
|
76
|
+
#phead #ttl { flex:1 1 0; }
|
|
77
|
+
/* 去工作台:次要的小链接,不和首页 / 记录并列 */
|
|
78
|
+
.devl { flex:none; display:inline-flex; align-items:center; gap:4px; height:32px; padding:0 10px; border:1px solid #e2dfd6; border-radius:10px; background:#fff; color:var(--dim); font-size:13px; text-decoration:none; }
|
|
77
79
|
#phead .ptabs { flex:1 0 100%; }
|
|
78
80
|
/* 七天日期条:每天下面学科色的点 = 那天有动静的老师 */
|
|
79
81
|
#pweek { display:flex; align-items:center; gap:2px; padding:4px; border-radius:16px; background:var(--card); border:1px solid var(--line); }
|
|
@@ -172,6 +174,7 @@ ${BRAND_CSS}
|
|
|
172
174
|
#home { max-width:1180px; }
|
|
173
175
|
#phead #ttl { flex:0 0 auto; }
|
|
174
176
|
#phead .ptabs { flex:0 0 260px; }
|
|
177
|
+
#phead .devl { order:1; margin-left:auto; height:34px; padding:0 12px; font-size:14px; }
|
|
175
178
|
#pcols { display:grid; grid-template-columns:minmax(0,640px) minmax(0,1fr); gap:20px; align-items:start; }
|
|
176
179
|
#pday { position:sticky; top:16px; max-height:calc(100vh - 32px); overflow:auto; }
|
|
177
180
|
}
|
|
@@ -186,7 +189,8 @@ ${REEL_DOCK_CSS}
|
|
|
186
189
|
<div id="home">
|
|
187
190
|
<div id="phead">
|
|
188
191
|
<div id="ttl"><span id="ct-mark" class="ct-draw ct-hover">${titleLogo(true)}</span><h1 id="title">${title}</h1></div>
|
|
189
|
-
<
|
|
192
|
+
<a class="devl" href="/dev" target="_blank" title="工作台:细调老师文件、activity(新窗口)">工作台<svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 2h6v6"/><path d="M10 2 3 9"/></svg></a>
|
|
193
|
+
<nav id="ptabs" class="ptabs"><a href="/parent/home">首页</a><a class="on" href="/parent">记录</a><a href="/parent/settings">设置</a></nav>
|
|
190
194
|
</div>
|
|
191
195
|
<a id="pnokey" href="/parent/home" hidden>孩子那边现在打不开:还没配百炼的 key。去「首页」配好就能用 ›</a>
|
|
192
196
|
<div id="pweek"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PARENT_SETTINGS_PAGE: string;
|