cotutor 0.3.0 → 0.4.0
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 +36 -13
- 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 +5 -0
- package/dist/app/cli/cert.js +20 -7
- 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 +6 -5
- package/dist/app/cli/doctor.js +107 -171
- 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 +347 -24
- 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 +138 -0
- package/dist/app/cli/service.js +429 -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 +33 -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 +205 -0
- package/dist/app/cli/tray.js +618 -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 +45 -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/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/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 +315 -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 +2 -1
- 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
|
@@ -30,6 +30,9 @@ ${faviconLinks()}
|
|
|
30
30
|
#ct-mark { display:flex; }
|
|
31
31
|
${BRAND_CSS}
|
|
32
32
|
.top small { color:var(--mute); font-size:13px; }
|
|
33
|
+
/* 去工作台:次要的小链接,不和首页 / 记录并列 */
|
|
34
|
+
.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; }
|
|
35
|
+
.top .devl { margin-left:auto; }
|
|
33
36
|
.top .seg { order:3; flex:1 0 100%; }
|
|
34
37
|
.seg { display:flex; padding:3px; border-radius:12px; background:#e9e6de; }
|
|
35
38
|
.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 +107,100 @@ ${BRAND_CSS}
|
|
|
104
107
|
#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
108
|
#toast.on { display:block; }
|
|
106
109
|
.main { display:flex; flex-direction:column; gap:18px; }
|
|
110
|
+
/* 孩子的平板怎么打开:二维码(服务端现画);宽屏在右栏最上面,窄屏在预览上面 */
|
|
111
|
+
#qrtop:empty { display:none; }
|
|
112
|
+
.qrrow { display:flex; flex-direction:column; gap:10px; align-items:center; text-align:center; }
|
|
113
|
+
.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; }
|
|
114
|
+
.qrimg svg { display:block; width:100%; height:100%; }
|
|
115
|
+
.qrurl { font:13px/1.4 ui-monospace,Menlo,monospace; background:#faf9f4; border-radius:8px; padding:8px 10px; overflow-wrap:anywhere; }
|
|
116
|
+
.qrwarn { font-size:13px; color:var(--accent); line-height:1.5; }
|
|
117
|
+
.gh .x { height:auto; padding:2px 0; font-size:13px; }
|
|
118
|
+
/* 样子与声音:头像一格格点;两种声音各一行(现在用的),「换一个」开弹窗;宽屏在右栏,窄屏整屏 */
|
|
119
|
+
.lk { display:flex; flex-direction:column; gap:16px; }
|
|
120
|
+
.lk .nav b { font-size:16px; text-align:center; }
|
|
121
|
+
.lksaved { min-width:56px; text-align:right; font-size:13px; color:var(--ok); }
|
|
122
|
+
.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); }
|
|
123
|
+
.lkbig img { width:100%; height:100%; object-fit:cover; }
|
|
124
|
+
.lkgrid { background:var(--card); border-radius:14px; padding:12px; display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:8px; }
|
|
125
|
+
.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; }
|
|
126
|
+
.lkopt.on { border-color:var(--ink); background:#fff; }
|
|
127
|
+
.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); }
|
|
128
|
+
.lkf img { width:100%; height:100%; object-fit:cover; }
|
|
129
|
+
.lkopt small { font-size:11px; color:var(--mute); line-height:1.3; }
|
|
130
|
+
/* 一个声音:圆里「女 / 男」,底色也分(不只靠颜色);标签:普通话 / 中文灰,方言橙,英文青,童声紫,场景描边 */
|
|
131
|
+
.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; }
|
|
132
|
+
.vface.g-f { background:#fbe4e8; color:#9e2f4a; }
|
|
133
|
+
.vface.g-m { background:#dfe9f6; color:#285a96; }
|
|
134
|
+
.vtx { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; text-align:left; }
|
|
135
|
+
.vnm { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
|
|
136
|
+
.vnm b { font-size:16px; font-weight:600; }
|
|
137
|
+
.vnm small, .vtr { font-size:13px; color:#5f5b54; }
|
|
138
|
+
.vtags { display:flex; gap:5px; flex-wrap:wrap; }
|
|
139
|
+
.vt { font-size:12px; line-height:20px; padding:0 7px; border-radius:6px; border:1px solid transparent; }
|
|
140
|
+
.t-lang { background:#f1eee7; color:#5f5b54; }
|
|
141
|
+
.t-dia { background:#fdecd3; color:#8a4b0b; }
|
|
142
|
+
.t-en { background:#dcefee; color:#0f5f5c; }
|
|
143
|
+
.t-kid { background:#ece6f8; color:#5b3fa0; }
|
|
144
|
+
.t-line { background:#fff; color:#5f5b54; border-color:#e3dfd6; }
|
|
145
|
+
.t-warn { background:#fff; color:#8a4b0b; border-color:#e2b877; }
|
|
146
|
+
.vplay { flex:none; width:44px; height:44px; border-radius:50%; border:0; background:#f3efe6; color:var(--ink); display:grid; place-items:center; cursor:pointer; }
|
|
147
|
+
.vplay.on { background:var(--accent); color:#fff; }
|
|
148
|
+
.vcur { padding:12px 12px 12px 14px; display:flex; flex-direction:column; gap:8px; }
|
|
149
|
+
.vcur + .vcur { border-top:1px solid var(--line); }
|
|
150
|
+
.vcur > .lbl { font-size:13px; color:#5f5b54; }
|
|
151
|
+
.vrow2 { display:flex; align-items:center; gap:12px; }
|
|
152
|
+
.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; }
|
|
153
|
+
.vchg span { display:flex; }
|
|
154
|
+
.vchg svg { width:16px; height:16px; }
|
|
155
|
+
/* 选声音的弹窗:宽屏居中,窄屏从底下升起占满 */
|
|
156
|
+
.vp { position:fixed; inset:0; z-index:30; background:rgba(30,28,25,.46); display:flex; align-items:center; justify-content:center; padding:24px; }
|
|
157
|
+
.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; }
|
|
158
|
+
.vphd { padding:18px 16px 0 24px; display:flex; align-items:flex-start; gap:12px; }
|
|
159
|
+
.vphd .tx { flex:1; min-width:0; display:flex; flex-direction:column; }
|
|
160
|
+
.vphd b { font-size:20px; font-weight:600; }
|
|
161
|
+
.vphd small { font-size:13px; color:#5f5b54; }
|
|
162
|
+
.vpx { flex:none; width:44px; height:44px; border-radius:50%; border:0; background:#f3efe6; color:var(--ink); display:grid; place-items:center; cursor:pointer; }
|
|
163
|
+
.vpctl { padding:14px 24px 12px; display:flex; flex-direction:column; gap:10px; border-bottom:1px solid var(--line); }
|
|
164
|
+
.vpctl .fld { padding:10px 14px; }
|
|
165
|
+
#vp-f { display:flex; flex-direction:column; gap:8px; }
|
|
166
|
+
.vpg { display:flex; align-items:flex-start; gap:10px; }
|
|
167
|
+
.vpg .lbl { flex:none; width:36px; padding-top:6px; font-size:13px; color:#5f5b54; }
|
|
168
|
+
.vpg .chips { flex:1; min-width:0; }
|
|
169
|
+
.chips { display:flex; flex-wrap:wrap; gap:6px; }
|
|
170
|
+
.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; }
|
|
171
|
+
.chip small { font-size:12px; color:#6b675f; }
|
|
172
|
+
.chip.on { background:var(--ink); border-color:var(--ink); color:#fff; }
|
|
173
|
+
.chip.on small { color:#e8e4dc; }
|
|
174
|
+
.chip.more { color:var(--accent); border-style:dashed; }
|
|
175
|
+
.vplist { flex:1; min-height:0; overflow-y:auto; padding:12px 24px 16px; background:#faf9f6; -webkit-overflow-scrolling:touch; }
|
|
176
|
+
.vpgrid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; }
|
|
177
|
+
.vcard { display:flex; align-items:flex-start; gap:8px; padding:12px 10px 12px 12px; border-radius:14px; background:#fff; border:2px solid #ece8df; }
|
|
178
|
+
.vcard.on { background:#fffaf3; border-color:var(--accent); }
|
|
179
|
+
.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; }
|
|
180
|
+
.vpnone { padding:60px 0; display:flex; flex-direction:column; align-items:center; gap:10px; color:#5f5b54; }
|
|
181
|
+
.vpft { padding:12px 16px 12px 24px; border-top:1px solid var(--line); display:flex; align-items:center; gap:10px; }
|
|
182
|
+
#vp-st { flex:1; min-width:0; font-size:13px; color:#5f5b54; }
|
|
183
|
+
.vpno { min-height:44px; padding:0 18px; border:0; border-radius:22px; background:#f3efe6; color:var(--ink); font-size:15px; cursor:pointer; }
|
|
184
|
+
.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; }
|
|
185
|
+
.vpgo:disabled { background:#ece8df; color:#6b675f; cursor:default; }
|
|
186
|
+
@media (max-width:700px) {
|
|
187
|
+
.vp { padding:40px 0 0; align-items:stretch; }
|
|
188
|
+
.vpbox { max-width:none; height:100%; border-radius:20px 20px 0 0; }
|
|
189
|
+
.vphd { padding:14px 8px 0 16px; }
|
|
190
|
+
.vphd b { font-size:18px; }
|
|
191
|
+
.vpctl { padding:10px 16px; }
|
|
192
|
+
#vp-f { gap:6px; }
|
|
193
|
+
.vpg { align-items:center; gap:8px; }
|
|
194
|
+
.vpg .lbl { padding-top:0; width:28px; }
|
|
195
|
+
.vpg .chips { flex-wrap:nowrap; overflow-x:auto; margin-right:-16px; padding:0 16px 2px 0; }
|
|
196
|
+
.chip { flex:none; }
|
|
197
|
+
.vplist { padding:10px 12px 14px; }
|
|
198
|
+
.vpgrid { grid-template-columns:minmax(0, 1fr); gap:8px; }
|
|
199
|
+
.vpft { flex-wrap:wrap; padding:10px 16px calc(14px + env(safe-area-inset-bottom)); }
|
|
200
|
+
#vp-st { flex:1 0 100%; text-align:center; font-size:12px; }
|
|
201
|
+
.vpno { flex:1; }
|
|
202
|
+
.vpgo { flex:2; min-height:48px; }
|
|
203
|
+
}
|
|
107
204
|
/* 百炼没配:孩子那边打不开,这块放在右栏最上面 */
|
|
108
205
|
.grp.warn { background:#fff3e8; }
|
|
109
206
|
.grp.warn .it, .grp.warn .box { background:transparent; }
|
|
@@ -115,12 +212,14 @@ ${BRAND_CSS}
|
|
|
115
212
|
.top { align-items:center; justify-content:flex-start; }
|
|
116
213
|
.top .seg { order:0; flex:0 0 260px; }
|
|
117
214
|
.top small { margin-left:auto; }
|
|
215
|
+
.top .devl { margin-left:0; height:34px; padding:0 12px; font-size:14px; }
|
|
118
216
|
.main { display:grid; grid-template-columns:minmax(0,1fr) 380px; gap:22px; align-items:start; }
|
|
119
217
|
.gh { padding:0 6px 6px; }
|
|
120
218
|
}
|
|
121
219
|
</style></head><body>
|
|
122
220
|
<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>
|
|
221
|
+
<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>
|
|
222
|
+
<div id="qrtop"></div>
|
|
124
223
|
<div class="main">
|
|
125
224
|
<section id="pv" aria-label="孩子看到的首页">
|
|
126
225
|
<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 +243,8 @@ ${pageDomJs(3200)}
|
|
|
144
243
|
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
244
|
// 学科色服务端定好随老师下发(src/ui/shared/tutor-color.ts)
|
|
146
245
|
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; };
|
|
246
|
+
// 方砖在「每天都在」里的那句:点进去老师怎么带(参数都空着,老师先请孩子拍材料)
|
|
247
|
+
const TILE_NOTE = { preview: '孩子拍新课的那页,老师带着先读一读、问一问', check: '作业写完拍给老师,一次看一处、改一处', review: '学过的拿出来,挑一点再练一练' };
|
|
147
248
|
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
249
|
const md = (d) => Number(d.slice(5, 7)) + '/' + Number(d.slice(8, 10));
|
|
149
250
|
// 留多久:只今天 = 今天;到周日 = 这周日(今天是周日就是今天);一直 = 不写
|
|
@@ -183,6 +284,7 @@ ${pageDomJs(3200)}
|
|
|
183
284
|
if (!t) return;
|
|
184
285
|
if (t.name !== cur) choose(t.name, true);
|
|
185
286
|
if (ev.act === 'button') { if (ev.id === 'new') toast('「新话题」是应用自己加的,一直在第一个,这里改不了'); else if (typeof ev.id === 'number') pick(ev.id); }
|
|
287
|
+
else if (ev.act === 'tile') { const el = $('#daily-tile-' + ev.id); if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
|
|
186
288
|
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
289
|
};
|
|
188
290
|
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 +311,7 @@ ${pageDomJs(3200)}
|
|
|
209
311
|
const st = BL.st;
|
|
210
312
|
if (!st) { blEl.replaceChildren(); return; }
|
|
211
313
|
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 : '测试用的假百炼')
|
|
314
|
+
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
315
|
h('button', { type: 'button', class: 'x', disabled: BL.busy || null, on: { click: blTest } }, BL.busy && !BL.open ? '在试…' : '试连'),
|
|
214
316
|
st.canEdit ? h('button', { type: 'button', class: 'x', on: { click: () => { BL.open = !BL.open; BL.msg = ''; blDraw(); } } }, BL.open ? '收起' : '换一把') : null));
|
|
215
317
|
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 +324,49 @@ ${pageDomJs(3200)}
|
|
|
222
324
|
blEl.replaceChildren(h('div', { class: 'gh' }, h('span', {}, '百炼 · 老师开口用的')), h('div', { class: 'grp' + (st.ready ? '' : ' warn') }, ...rows));
|
|
223
325
|
};
|
|
224
326
|
|
|
327
|
+
// ---- 扫码打开:孩子的平板一张(孩子端)、家长自己的手机一张(家长端);服务端现画,地址同 /parent/qr。还没有孩子的设备连过就开着,连过收起;家长开合过就记住 ----
|
|
328
|
+
const QR = { d: null, ip: false, open: null, to: 'kid' };
|
|
329
|
+
try { const v = localStorage.getItem('parent-home-qr'); if (v === 'open' || v === 'closed') QR.open = v === 'open'; } catch (e) {}
|
|
330
|
+
const qrEl = h('div', { id: 'kidqr' });
|
|
331
|
+
const qrToggle = (open) => { QR.open = open; try { localStorage.setItem('parent-home-qr', open ? 'open' : 'closed'); } catch (e) {} qrDraw(); };
|
|
332
|
+
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); };
|
|
333
|
+
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>';
|
|
334
|
+
const host = (u) => u.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
335
|
+
const qrDraw = () => {
|
|
336
|
+
const d = QR.d;
|
|
337
|
+
if (!d || !d.ready) { qrEl.replaceChildren(); return; }
|
|
338
|
+
const open = QR.open === null ? d.devices === 0 : QR.open;
|
|
339
|
+
const seen = d.devices ? '孩子的平板 ' + d.devices + ' 台连过 · 上次' + qrWhen(d.last) : '还没有平板连过';
|
|
340
|
+
if (!open) {
|
|
341
|
+
qrEl.replaceChildren(h('div', { class: 'grp' }, h('button', { type: 'button', class: 'it', 'aria-expanded': 'false', on: { click: () => qrToggle(true) } },
|
|
342
|
+
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' }, '展开'))));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const kid = QR.to === 'kid', c = kid ? d : (d.parent || d);
|
|
346
|
+
const useIp = QR.ip && c.ip;
|
|
347
|
+
const seg = h('div', { class: 'seg', role: 'group', 'aria-label': '给谁扫' }, ...[['kid', '孩子的平板'], ['parent', '我的手机']].map(([k, l]) =>
|
|
348
|
+
h('button', { type: 'button', class: QR.to === k ? 'on' : '', 'aria-pressed': QR.to === k ? 'true' : 'false', on: { click: () => { QR.to = k; qrDraw(); } } }, l)));
|
|
349
|
+
const print = '/parent/qr?' + new URLSearchParams(Object.assign({}, useIp ? { via: 'ip' } : {}, kid ? {} : { to: 'parent' })).toString();
|
|
350
|
+
qrEl.replaceChildren(
|
|
351
|
+
h('div', { class: 'gh' }, h('span', {}, '扫码打开'), h('button', { type: 'button', class: 'x', 'aria-expanded': 'true', on: { click: () => qrToggle(false) } }, '收起')),
|
|
352
|
+
h('div', { class: 'grp' }, h('div', { class: 'box' }, seg,
|
|
353
|
+
h('div', { class: 'qrrow' }, h('div', { class: 'qrimg', html: (useIp ? c.svgIp : c.svg) || '' }),
|
|
354
|
+
h('b', { style: 'font-weight:600' }, kid ? '用孩子平板的相机扫一下' : '用你自己的手机扫一下'),
|
|
355
|
+
h('span', { class: 'lbl', style: 'line-height:1.5' }, kid ? '打开的就是孩子的首页。在 Safari 里「添加到主屏幕」,以后点图标就进来。' : '打开的是家长端:看记录、排首页。也可以添加到主屏幕。')),
|
|
356
|
+
h('div', { class: 'qrurl' }, useIp ? c.ip : c.url),
|
|
357
|
+
d.https ? null : h('div', { class: 'qrwarn' }, d.notes[0] || ''),
|
|
358
|
+
h('div', { class: 'row2', style: 'font-size:14px' },
|
|
359
|
+
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'),
|
|
360
|
+
h('span', { style: 'display:flex;gap:14px;flex:none' },
|
|
361
|
+
h('a', { class: 'lnk kidl', style: 'font-size:14px', href: '/', target: '_blank' }, '孩子端 ↗'),
|
|
362
|
+
h('a', { class: 'lnk', style: 'font-size:14px', href: print, target: '_blank' }, '打印 ›'))))),
|
|
363
|
+
h('div', { class: 'note' }, d.https ? (d.notes[0] || '') : '', d.devices ? '' : (d.https ? ' ' : '') + '还没有平板连进来过,所以这块先开着;连过以后自动收起。'));
|
|
364
|
+
};
|
|
365
|
+
|
|
225
366
|
const load = async () => {
|
|
226
367
|
try { D = await api('GET', '/api/parent/home'); } catch (e) { toast('读不到:' + e.message); return; }
|
|
227
368
|
BL.st = await api('GET', '/api/parent/bailian').catch(() => null);
|
|
369
|
+
QR.d = await api('GET', '/api/parent/qr').catch(() => null);
|
|
228
370
|
document.title = D.title + ' · 家长';
|
|
229
371
|
$('#title').textContent = D.title;
|
|
230
372
|
$('#pub').textContent = D.home ? md(D.home.publishedAt.slice(0, 10)) + ' ' + new Date(D.home.publishedAt).toTimeString().slice(0, 5) + ' 发布 · 改了马上到孩子的首页' : '还没发布过';
|
|
@@ -239,12 +381,12 @@ ${pageDomJs(3200)}
|
|
|
239
381
|
};
|
|
240
382
|
const tutorOf = () => D.tutors.find((t) => t.name === cur);
|
|
241
383
|
/** 看哪位老师;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(); };
|
|
384
|
+
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
385
|
/** 改第 k 颗(-1 = 加一个):宽屏在右栏,窄屏整屏的表单 */
|
|
244
386
|
const pick = (k) => {
|
|
245
387
|
const t = tutorOf(); if (!t) return;
|
|
246
388
|
if (!WIDE.matches) { editButton(t, k); return; }
|
|
247
|
-
sel = k; dirty = false; draft = { edit: null, add: null }; render();
|
|
389
|
+
sel = k; lookStop(); LOOK.open = null; dirty = false; draft = { edit: null, add: null }; render();
|
|
248
390
|
setTimeout(() => { const el = $('#b-label'); if (el) el.focus(); }, 50);
|
|
249
391
|
};
|
|
250
392
|
const setExtras = (t, kind, on) => {
|
|
@@ -316,6 +458,8 @@ ${pageDomJs(3200)}
|
|
|
316
458
|
// 每天都在
|
|
317
459
|
const has = (k) => t.extras.includes(k);
|
|
318
460
|
const daily = [];
|
|
461
|
+
// 方砖:预习、检查作业、复习,老师家里有那个 activity 文件的才列;关掉就是孩子牌上不出那一块
|
|
462
|
+
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
463
|
daily.push(h('div', { class: 'it' }, h('span', { class: 'tx' }, h('b', {}, '听写'), h('small', {}, '拍照听写一直在;设了这周的词,孩子点「听写」直接写')), sw(has('dictation'), '听写', () => setExtras(t, 'dictation', !has('dictation')))));
|
|
320
464
|
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
465
|
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 +472,17 @@ ${pageDomJs(3200)}
|
|
|
328
472
|
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
473
|
}
|
|
330
474
|
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' },
|
|
475
|
+
const self = h('div', {}, h('div', { class: 'gh' }, h('span', {}, '这位老师 · ' + t.display)), h('div', { class: 'grp' },
|
|
476
|
+
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
477
|
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])
|
|
478
|
+
const pane = !wide ? null : LOOK.open === t.name ? lookPanel() : sel === null || (sel >= 0 && !t.buttons[sel])
|
|
334
479
|
? h('div', { class: 'grp hint' }, '点左边牌上的按钮,在这里改它的字和讲法;', h('br'), '改的时候牌上跟着变,点「发布」才到孩子那边。')
|
|
335
480
|
: inlineForm(t, sel);
|
|
336
481
|
blDraw();
|
|
482
|
+
qrDraw();
|
|
337
483
|
const blTop = BL.st && !BL.st.ready;
|
|
338
|
-
$('#
|
|
484
|
+
if (wide) $('#qrtop').replaceChildren(); else $('#qrtop').replaceChildren(qrEl);
|
|
485
|
+
$('#body').replaceChildren(h('div', {}, blTop ? blEl : null, wide ? qrEl : null, pane, extras, self, blTop ? null : blEl));
|
|
339
486
|
};
|
|
340
487
|
|
|
341
488
|
// ---- 改一个按钮的表单(k = -1 是新加的开场按钮):窄屏整屏盖上,宽屏在右栏;预览里那颗跟着变 ----
|
|
@@ -382,6 +529,167 @@ ${pageDomJs(3200)}
|
|
|
382
529
|
};
|
|
383
530
|
const openSheet = (...kids) => { $('#sheet-in').replaceChildren(...kids); $('#sheet').classList.add('on'); window.scrollTo(0, 0); };
|
|
384
531
|
const closeSheet = () => $('#sheet').classList.remove('on');
|
|
532
|
+
|
|
533
|
+
// ---- 样子与声音:头像点了就存;两种声音各一行(现在用的那个、试听),「换一个」开弹窗挑(src/app/http/parent-look.ts)。宽屏在右栏,窄屏整屏 ----
|
|
534
|
+
const LOOK = { open: null, d: null, playing: null, audio: null, saved: false };
|
|
535
|
+
const lookUrl = (name) => '/api/parent/tutors/' + encodeURIComponent(name) + '/look';
|
|
536
|
+
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>';
|
|
537
|
+
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>';
|
|
538
|
+
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>';
|
|
539
|
+
function lookStop() { if (LOOK.audio) { LOOK.audio.pause(); URL.revokeObjectURL(LOOK.audio.src); } LOOK.audio = null; LOOK.playing = null; }
|
|
540
|
+
const lookDraw = () => { const el = $('#look'); if (el) el.replaceWith(lookPanel()); if (VP.kind) vpPaint(); };
|
|
541
|
+
const openLook = async (t) => {
|
|
542
|
+
lookStop(); vpClose(); LOOK.open = t.name; LOOK.d = null; LOOK.saved = false;
|
|
543
|
+
if (WIDE.matches) { sel = null; render(); } else openSheet(lookPanel());
|
|
544
|
+
try { LOOK.d = await api('GET', lookUrl(t.name)); } catch (e) { toast('读不到:' + e.message); }
|
|
545
|
+
lookDraw();
|
|
546
|
+
};
|
|
547
|
+
const closeLook = () => { lookStop(); vpClose(); LOOK.open = null; if (WIDE.matches) render(); else closeSheet(); };
|
|
548
|
+
let savedTimer = 0;
|
|
549
|
+
const lookSave = async (patch) => {
|
|
550
|
+
try { LOOK.d = await api('PUT', lookUrl(LOOK.open), patch); } catch (e) { toast('没存上:' + e.message); return false; }
|
|
551
|
+
LOOK.saved = true; lookDraw(); clearTimeout(savedTimer); savedTimer = setTimeout(() => { LOOK.saved = false; lookDraw(); }, 1600);
|
|
552
|
+
// 头像变了预览里的牌、先后那排跟着变
|
|
553
|
+
await load(); pvSync(true);
|
|
554
|
+
return true;
|
|
555
|
+
};
|
|
556
|
+
// 试听:服务端念它定的那句(有缓存);同时只放一个,再点一下停
|
|
557
|
+
const lookPlay = async (kind, voice) => {
|
|
558
|
+
const key = kind + ' ' + voice;
|
|
559
|
+
if (LOOK.playing === key) { lookStop(); lookDraw(); return; }
|
|
560
|
+
lookStop(); LOOK.playing = key; lookDraw();
|
|
561
|
+
try {
|
|
562
|
+
const r = await fetch(lookUrl(LOOK.open) + '/preview?kind=' + kind + '&voice=' + encodeURIComponent(voice), { cache: 'no-store' });
|
|
563
|
+
if (!r.ok) { const j = await r.json().catch(() => null); throw new Error((j && j.message) || 'HTTP ' + r.status); }
|
|
564
|
+
const blob = await r.blob();
|
|
565
|
+
if (LOOK.playing !== key) return;
|
|
566
|
+
const a = new Audio(URL.createObjectURL(blob)); LOOK.audio = a;
|
|
567
|
+
a.onended = () => { if (LOOK.playing === key) { lookStop(); lookDraw(); } };
|
|
568
|
+
await a.play();
|
|
569
|
+
} catch (e) { if (LOOK.playing === key) { lookStop(); lookDraw(); } toast('试听不了:' + e.message); }
|
|
570
|
+
};
|
|
571
|
+
const lookFace = (o) => o.img ? h('img', { src: o.img, alt: '' }) : (o.text || '?');
|
|
572
|
+
|
|
573
|
+
// 一个声音长什么样:圆里一个「女 / 男」(底色也分),名字、年纪,标签(语言口音、童声、场景、提醒、现在用的 / 出厂),特点
|
|
574
|
+
const ageOf = (a) => (a <= 12 ? '童声' : a <= 19 ? '少年' : a <= 35 ? '青年' : '成熟');
|
|
575
|
+
const vFace = (v) => h('span', { class: 'vface' + (v.gender === '女' ? ' g-f' : v.gender === '男' ? ' g-m' : '') }, v.gender || '?');
|
|
576
|
+
const vTags = (kind, v, current) => {
|
|
577
|
+
const out = [];
|
|
578
|
+
const plain = kind === 'mouth' ? v.lang === '普通话' : v.lang === '中文';
|
|
579
|
+
out.push(h('span', { class: 'vt ' + (plain ? 't-lang' : kind === 'mouth' ? 't-dia' : 't-en') }, v.lang));
|
|
580
|
+
if (v.age && v.age <= 12) out.push(h('span', { class: 'vt t-kid' }, '童声'));
|
|
581
|
+
if (v.scene) out.push(h('span', { class: 'vt t-line' }, v.scene));
|
|
582
|
+
if (v.warn) out.push(h('span', { class: 'vt t-warn' }, v.warn));
|
|
583
|
+
if (v.voice === current) out.push(h('span', { class: 'vt t-line' }, '现在用的'));
|
|
584
|
+
else if (v.shipped) out.push(h('span', { class: 'vt t-line' }, '出厂'));
|
|
585
|
+
return out;
|
|
586
|
+
};
|
|
587
|
+
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); } } }); };
|
|
588
|
+
const vBody = (kind, v, current) => h('span', { class: 'vtx' },
|
|
589
|
+
h('span', { class: 'vnm' }, h('b', {}, v.name), v.age ? h('small', {}, v.age + ' 岁') : null),
|
|
590
|
+
h('span', { class: 'vtags' }, ...vTags(kind, v, current)),
|
|
591
|
+
v.trait ? h('small', { class: 'vtr' }, v.trait) : null);
|
|
592
|
+
|
|
593
|
+
// ---- 选声音的弹窗:宽屏居中,窄屏从底下升起;搜、筛(每个选项后面是筛出来几个,跟着别的条件变)、试听、选中,按「用」才存 ----
|
|
594
|
+
const VP_STEP = 60;
|
|
595
|
+
const VP = { kind: null, sel: null, q: '', f: {}, n: VP_STEP, more: false };
|
|
596
|
+
const VP_DIMS = {
|
|
597
|
+
mouth: [['g', '性别', (v) => v.gender], ['lang', '口音', (v) => v.lang]],
|
|
598
|
+
card: [['g', '性别', (v) => v.gender], ['lang', '语言', (v) => v.lang], ['age', '年纪', (v) => (v.age ? ageOf(v.age) : '')], ['scene', '场景', (v) => v.scene || '']],
|
|
599
|
+
};
|
|
600
|
+
const VP_ORDER = { g: ['女', '男'], age: ['童声', '少年', '青年', '成熟'] };
|
|
601
|
+
const SCENE_SHOWN = 7;
|
|
602
|
+
const vpPass = (v, skip) => {
|
|
603
|
+
const q = VP.q.trim().toLowerCase();
|
|
604
|
+
if (q && ![v.name, v.trait, v.scene || '', v.lang, v.voice].some((x) => x.toLowerCase().includes(q))) return false;
|
|
605
|
+
return VP_DIMS[VP.kind].every(([k, , of]) => k === skip || !VP.f[k] || of(v) === VP.f[k]);
|
|
606
|
+
};
|
|
607
|
+
const vpOpen = (kind) => {
|
|
608
|
+
if (!LOOK.d) return;
|
|
609
|
+
VP.kind = kind; VP.sel = LOOK.d[kind].current; VP.q = ''; VP.f = {}; VP.n = VP_STEP; VP.more = false;
|
|
610
|
+
const t = D && D.tutors.find((x) => x.name === LOOK.open);
|
|
611
|
+
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(); } } });
|
|
612
|
+
const box = h('div', { class: 'vpbox', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'vp-t', tabindex: '-1' },
|
|
613
|
+
h('div', { class: 'vphd' },
|
|
614
|
+
h('div', { class: 'tx' }, h('b', { id: 'vp-t' }, (t ? t.display : '') + ' · ' + (kind === 'mouth' ? '说话的声音' : '点读的声音')),
|
|
615
|
+
h('small', {}, (kind === 'mouth' ? '孩子听到老师开口就是它' : '点读卡、单词卡、听写念词都用它') + ' · 一共 ' + LOOK.d[kind].options.length + ' 个')),
|
|
616
|
+
h('button', { type: 'button', class: 'vpx', 'aria-label': '关掉', html: LK_X, on: { click: vpClose } })),
|
|
617
|
+
h('div', { class: 'vpctl' }, q, h('div', { id: 'vp-f' })),
|
|
618
|
+
h('div', { class: 'vplist', id: 'vp-list' }),
|
|
619
|
+
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 } })));
|
|
620
|
+
const el = h('div', { id: 'vp', class: 'vp', on: { click: (e) => { if (e.target === el) vpClose(); }, keydown: (e) => { if (e.key === 'Escape') vpClose(); } } }, box);
|
|
621
|
+
document.body.append(el); document.body.style.overflow = 'hidden';
|
|
622
|
+
vpPaint(); box.focus();
|
|
623
|
+
};
|
|
624
|
+
function vpClose() {
|
|
625
|
+
if (!VP.kind) return;
|
|
626
|
+
if (LOOK.playing && LOOK.playing.startsWith(VP.kind + ' ')) lookStop();
|
|
627
|
+
VP.kind = null; const el = $('#vp'); if (el) el.remove(); document.body.style.overflow = '';
|
|
628
|
+
const p = $('#look'); if (p) lookDraw();
|
|
629
|
+
}
|
|
630
|
+
async function vpUse() {
|
|
631
|
+
const kind = VP.kind, voice = VP.sel;
|
|
632
|
+
if (!kind || !voice || voice === LOOK.d[kind].current) return;
|
|
633
|
+
if (await lookSave(kind === 'mouth' ? { mouth: voice } : { voice })) { vpClose(); toast(kind === 'mouth' ? '存好了,下次开口就换' : '存好了,配过音的卡下次点开才换'); }
|
|
634
|
+
}
|
|
635
|
+
function vpPaint() {
|
|
636
|
+
const kind = VP.kind; if (!kind || !LOOK.d) return;
|
|
637
|
+
const block = LOOK.d[kind], all = block.options;
|
|
638
|
+
// 筛选:一组一排,选项后面是「别的条件不变、换成它」筛出来几个
|
|
639
|
+
const groups = VP_DIMS[kind].map(([k, label, of]) => {
|
|
640
|
+
const vals = [];
|
|
641
|
+
all.forEach((v) => { const x = of(v); if (x && !vals.includes(x)) vals.push(x); });
|
|
642
|
+
const n = (x) => all.filter((v) => vpPass(v, k) && (!x || of(v) === x)).length;
|
|
643
|
+
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);
|
|
644
|
+
if (k === 'lang' && kind === 'mouth') list = ['普通话'].concat(list.filter((x) => x !== '普通话'));
|
|
645
|
+
const cut = k === 'scene' && !VP.more && list.length > SCENE_SHOWN + 1;
|
|
646
|
+
const shown = cut ? list.slice(0, SCENE_SHOWN).concat(VP.f.scene && list.indexOf(VP.f.scene) >= SCENE_SHOWN ? [VP.f.scene] : []) : list;
|
|
647
|
+
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)))); });
|
|
648
|
+
if (cut) chips.push(h('button', { type: 'button', class: 'chip more', on: { click: () => { VP.more = true; vpPaint(); } } }, '更多 ' + (list.length - SCENE_SHOWN) + ' 种'));
|
|
649
|
+
return h('div', { class: 'vpg' }, h('span', { class: 'lbl' }, label), h('div', { class: 'chips' }, ...chips));
|
|
650
|
+
});
|
|
651
|
+
$('#vp-f').replaceChildren(...groups);
|
|
652
|
+
const hits = all.filter((v) => vpPass(v, ''));
|
|
653
|
+
const shown = hits.slice(0, VP.n);
|
|
654
|
+
const cards = shown.map((v) => {
|
|
655
|
+
const on = v.voice === VP.sel;
|
|
656
|
+
return h('div', { class: 'vcard' + (on ? ' on' : '') },
|
|
657
|
+
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)),
|
|
658
|
+
vPlayBtn(kind, v));
|
|
659
|
+
});
|
|
660
|
+
const left = hits.length - shown.length;
|
|
661
|
+
const list = $('#vp-list'), top = list.scrollTop;
|
|
662
|
+
list.replaceChildren(
|
|
663
|
+
hits.length ? h('div', { class: 'vpgrid', role: 'radiogroup', 'aria-label': kind === 'mouth' ? '说话的声音' : '点读的声音' }, ...cards)
|
|
664
|
+
: 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(); } } }, '清掉筛选')),
|
|
665
|
+
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);
|
|
666
|
+
list.scrollTop = top;
|
|
667
|
+
const picked = all.find((v) => v.voice === VP.sel), same = VP.sel === block.current;
|
|
668
|
+
$('#vp-st').textContent = '筛出 ' + hits.length + ' 个 · 试听念服务端定的一句';
|
|
669
|
+
const go = $('#vp-use'); go.textContent = same ? '现在就是它' : '用「' + (picked ? picked.name : '') + '」'; go.disabled = same;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function lookPanel() {
|
|
673
|
+
const t = D && D.tutors.find((x) => x.name === LOOK.open), d = LOOK.d;
|
|
674
|
+
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 ? '存好了' : ''));
|
|
675
|
+
if (!t || !d) return h('div', { id: 'look', class: 'lk' }, head, h('div', { class: 'grp hint' }, '在读…'));
|
|
676
|
+
const curA = d.avatar.options.find((o) => o.value === d.avatar.current);
|
|
677
|
+
const big = h('div', { class: 'lkbig' }, curA ? lookFace(curA) : avatar(t));
|
|
678
|
+
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 }); } } },
|
|
679
|
+
h('span', { class: 'lkf' }, lookFace(o)), o.label ? h('small', {}, o.label) : null)));
|
|
680
|
+
const cur = (kind, label) => {
|
|
681
|
+
const block = d[kind], v = block.options.find((o) => o.voice === block.current);
|
|
682
|
+
return h('div', { class: 'vcur', id: 'lk-' + kind },
|
|
683
|
+
h('div', { class: 'lbl' }, label),
|
|
684
|
+
v ? h('div', { class: 'vrow2' }, vFace(v), vBody(kind, v, null), vPlayBtn(kind, v)) : h('div', { class: 'lbl' }, '还没配'),
|
|
685
|
+
h('button', { type: 'button', class: 'vchg', on: { click: () => vpOpen(kind) } }, '换一个 · ' + block.options.length + ' 个可选', h('span', { html: CHEV })));
|
|
686
|
+
};
|
|
687
|
+
return h('div', { id: 'look', class: 'lk' }, head, big,
|
|
688
|
+
h('div', {}, h('div', { class: 'gh' }, h('span', {}, '头像 · 孩子的首页、老师页上都是这张')), grid),
|
|
689
|
+
h('div', {}, h('div', { class: 'gh' }, h('span', {}, '声音')),
|
|
690
|
+
h('div', { class: 'grp' }, cur('mouth', '说话的声音 · 老师开口说话'), cur('card', '点读的声音 · 点读卡、单词卡、听写念词')),
|
|
691
|
+
h('div', { class: 'note' }, '改了说话的声音,下次开口就换;已经配好音的卡,下次点开才换。')));
|
|
692
|
+
}
|
|
385
693
|
const editButton = (t, k) => {
|
|
386
694
|
const f = buttonForm(t, k, () => closeSheet());
|
|
387
695
|
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;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 家长端「设置」(/parent/settings):设一次就不常动的。现在只有一项——孩子的平板上,按住说话那一栏放哪边(cotutor.json 的 kid.hand)。
|
|
3
|
+
* 两张示意图(不是截图,页面现画的 SVG):横着的平板,窄的一栏是老师头像 + 按住说话,宽的一边是写字的白纸;点一张就存。
|
|
4
|
+
* 孩子端首页的接口带着它(/api/kid/home 的 hand),孩子下次进老师页就换过来;画板那一列工具跟着一起挪。
|
|
5
|
+
* 零依赖内联脚本,只走 /api/parent/settings(GET 读,PATCH {kid: {hand}} 改)。
|
|
6
|
+
* 用 String.raw:脚本里的反斜杠原样留着;脚本里不写反引号和 ${。
|
|
7
|
+
*/
|
|
8
|
+
import { faviconLinks } from "../shared/favicon.js";
|
|
9
|
+
import { BRAND_CSS, BRAND_FOOTER, BRAND_JS, titleLogo } from "../shared/brand.js";
|
|
10
|
+
import { pageDomJs } from "../shared/page-dom.js";
|
|
11
|
+
export const PARENT_SETTINGS_PAGE = String.raw `<!doctype html>
|
|
12
|
+
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
14
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
15
|
+
<link rel="manifest" href="/parent/manifest.webmanifest">
|
|
16
|
+
${faviconLinks()}
|
|
17
|
+
<title>家长 · 设置</title>
|
|
18
|
+
<style>
|
|
19
|
+
:root { --paper:#f6f5f1; --ink:#2b2b2b; --dim:#6b675f; --mute:#8a8781; --line:#efece5; --card:#fff; --accent:#b4531f; }
|
|
20
|
+
* { box-sizing:border-box; }
|
|
21
|
+
html,body { margin:0; background:var(--paper); color:var(--ink); font:16px/1.5 -apple-system,"PingFang SC","Hiragino Sans GB",sans-serif; -webkit-text-size-adjust:100%; }
|
|
22
|
+
button { font:inherit; color:inherit; }
|
|
23
|
+
.wrap { max-width:640px; margin:0 auto; padding:calc(env(safe-area-inset-top) + 20px) 16px calc(env(safe-area-inset-bottom) + 40px); display:flex; flex-direction:column; gap:16px; }
|
|
24
|
+
.top { display:flex; flex-wrap:wrap; align-items:center; gap:12px 20px; }
|
|
25
|
+
.top b { font-size:20px; }
|
|
26
|
+
.top .ttl { display:inline-flex; align-items:center; gap:10px; }
|
|
27
|
+
#ct-mark { display:flex; }
|
|
28
|
+
${BRAND_CSS}
|
|
29
|
+
.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; margin-left:auto; }
|
|
30
|
+
.top .seg { order:3; flex:1 0 100%; }
|
|
31
|
+
.seg { display:flex; padding:3px; border-radius:12px; background:#e9e6de; }
|
|
32
|
+
.seg > * { flex:1; height:36px; border-radius:9px; font-size:15px; color:var(--dim); display:flex; align-items:center; justify-content:center; text-decoration:none; }
|
|
33
|
+
.seg > .on { background:#fff; color:var(--ink); font-weight:600; box-shadow:0 1px 2px rgba(0,0,0,.08); }
|
|
34
|
+
.gh { font-size:13px; color:var(--mute); padding:8px 16px 6px; }
|
|
35
|
+
.grp { background:var(--card); border-radius:14px; padding:14px; display:flex; flex-direction:column; gap:12px; }
|
|
36
|
+
.q { font-size:16px; font-weight:500; padding:0 2px; }
|
|
37
|
+
.hands { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
38
|
+
.hand { position:relative; display:flex; flex-direction:column; align-items:center; gap:8px; padding:10px 10px 12px; border:2px solid var(--line); border-radius:14px; background:#fff; cursor:pointer; -webkit-tap-highlight-color:transparent; }
|
|
39
|
+
.hand svg { display:block; width:100%; height:auto; }
|
|
40
|
+
.hand .lb { font-size:15px; color:var(--dim); }
|
|
41
|
+
.hand .lb small { display:block; font-size:12px; color:var(--mute); text-align:center; }
|
|
42
|
+
.hand.on { border-color:var(--accent); }
|
|
43
|
+
.hand.on .lb { color:var(--ink); font-weight:600; }
|
|
44
|
+
.hand .ck { position:absolute; top:-9px; right:-9px; width:22px; height:22px; border-radius:50%; background:var(--accent); color:#fff; display:none; place-items:center; }
|
|
45
|
+
.hand.on .ck { display:grid; }
|
|
46
|
+
.hand:disabled { cursor:default; opacity:.6; }
|
|
47
|
+
.note { font-size:13px; color:var(--mute); line-height:1.55; padding:0 2px; }
|
|
48
|
+
#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; }
|
|
49
|
+
#toast.on { display:block; }
|
|
50
|
+
@media (min-width:1000px) {
|
|
51
|
+
.wrap { padding-top:calc(env(safe-area-inset-top) + 22px); gap:20px; }
|
|
52
|
+
.top .seg { order:0; flex:0 0 260px; }
|
|
53
|
+
.gh { padding:8px 6px 6px; }
|
|
54
|
+
}
|
|
55
|
+
</style></head><body>
|
|
56
|
+
<div class="wrap">
|
|
57
|
+
<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 href="/parent/home">首页</a><a href="/parent">记录</a><a class="on" href="/parent/settings">设置</a></nav><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>
|
|
58
|
+
<div>
|
|
59
|
+
<div class="gh">孩子的平板</div>
|
|
60
|
+
<div class="grp">
|
|
61
|
+
<div class="q" id="hand-q">按住说话那一栏放哪边</div>
|
|
62
|
+
<div class="hands" id="hands" role="radiogroup" aria-labelledby="hand-q"></div>
|
|
63
|
+
<div class="note">平板横着用的时候,老师页上那一栏(老师的头像、按住说话)放在这边,另一边留给写字;画板上那一列工具也跟着放这边。孩子下次进老师页就换过来。</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
${BRAND_FOOTER}
|
|
67
|
+
</div>
|
|
68
|
+
<div id="toast"></div>
|
|
69
|
+
<script>
|
|
70
|
+
(() => {
|
|
71
|
+
${BRAND_JS}
|
|
72
|
+
// $ / h / toast / api(page-dom.ts,几页共用)
|
|
73
|
+
${pageDomJs(3200)}
|
|
74
|
+
const CHECK = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"></path></svg>';
|
|
75
|
+
/** 示意图:横着的平板;col = 窄的那一栏在哪边('left' / 'right')。窄栏里是老师的头像与按住说话的胶囊,宽的一边是白纸上写着几行字 */
|
|
76
|
+
const sketch = (col) => {
|
|
77
|
+
const W = 240, H = 168, sx = 10, sy = 10, sw = 220, sh = 148, cw = 66;
|
|
78
|
+
const cx = col === 'left' ? sx : sx + sw - cw;
|
|
79
|
+
const px = col === 'left' ? sx + cw + 12 : sx + 12;
|
|
80
|
+
const pw = sw - cw - 24;
|
|
81
|
+
const mid = cx + cw / 2;
|
|
82
|
+
const id = 'scr-' + col;
|
|
83
|
+
const lines = [0.78, 0.6, 0.7].map((k, i) => '<path d="M' + (px + 12) + ' ' + (sy + 30 + i * 15) + 'h' + Math.round((pw - 24) * k) + '" stroke="#ddd8cc" stroke-width="4" stroke-linecap="round"></path>').join('');
|
|
84
|
+
const wx = px + 14, wy = sy + 104;
|
|
85
|
+
const ink = '<path d="M' + wx + ' ' + wy + 'c6-14 12-14 14 0s8 14 14 0 8-14 14 0 8 14 14 0" fill="none" stroke="#7a4fc9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path>';
|
|
86
|
+
return '<svg viewBox="0 0 ' + W + ' ' + H + '" aria-hidden="true">'
|
|
87
|
+
+ '<defs><clipPath id="' + id + '"><rect x="' + sx + '" y="' + sy + '" width="' + sw + '" height="' + sh + '" rx="9"></rect></clipPath></defs>'
|
|
88
|
+
+ '<rect x="2" y="2" width="' + (W - 4) + '" height="' + (H - 4) + '" rx="16" fill="#34312c"></rect>'
|
|
89
|
+
+ '<g clip-path="url(#' + id + ')">'
|
|
90
|
+
+ '<rect x="' + sx + '" y="' + sy + '" width="' + sw + '" height="' + sh + '" fill="#f4f1ea"></rect>'
|
|
91
|
+
+ '<rect x="' + cx + '" y="' + sy + '" width="' + cw + '" height="' + sh + '" fill="#fffdf8"></rect>'
|
|
92
|
+
+ '<path d="M' + (col === 'left' ? cx + cw : cx) + ' ' + sy + 'v' + sh + '" stroke="#e6e1d6" stroke-width="1.5"></path>'
|
|
93
|
+
+ '</g>'
|
|
94
|
+
+ '<rect x="' + px + '" y="' + (sy + 12) + '" width="' + pw + '" height="' + (sh - 24) + '" rx="6" fill="#fff" stroke="#e6e1d6"></rect>'
|
|
95
|
+
+ lines + ink
|
|
96
|
+
+ '<circle cx="' + mid + '" cy="' + (sy + 62) + '" r="18" fill="#f6e6da" stroke="#b4531f" stroke-width="3"></circle>'
|
|
97
|
+
+ '<circle cx="' + (mid - 6) + '" cy="' + (sy + 59) + '" r="2.2" fill="#6b4a36"></circle><circle cx="' + (mid + 6) + '" cy="' + (sy + 59) + '" r="2.2" fill="#6b4a36"></circle>'
|
|
98
|
+
+ '<path d="M' + (mid - 6) + ' ' + (sy + 67) + 'q6 5 12 0" fill="none" stroke="#6b4a36" stroke-width="2" stroke-linecap="round"></path>'
|
|
99
|
+
+ '<rect x="' + (mid - 24) + '" y="' + (sy + 96) + '" width="48" height="20" rx="10" fill="#b4531f"></rect>'
|
|
100
|
+
+ '<rect x="' + (mid - 3) + '" y="' + (sy + 100) + '" width="6" height="9" rx="3" fill="#fff"></rect>'
|
|
101
|
+
+ '<path d="M' + (mid - 6) + ' ' + (sy + 106) + 'a6 6 0 0 0 12 0M' + mid + ' ' + (sy + 112) + 'v2" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round"></path>'
|
|
102
|
+
+ '</svg>';
|
|
103
|
+
};
|
|
104
|
+
// hand(cotutor.json 的键)→ 那一栏在哪边:right(缺省)= 栏在左边;left = 栏在右边
|
|
105
|
+
const CHOICES = [{ hand: 'right', col: 'left', label: '在左边', sub: '缺省' }, { hand: 'left', col: 'right', label: '在右边', sub: '' }];
|
|
106
|
+
let S = null; let busy = false;
|
|
107
|
+
const draw = () => {
|
|
108
|
+
const box = $('#hands');
|
|
109
|
+
box.replaceChildren(...CHOICES.map((c) => {
|
|
110
|
+
const on = S && S.hand === c.hand;
|
|
111
|
+
return h('button', { type: 'button', class: 'hand' + (on ? ' on' : ''), role: 'radio', 'aria-checked': on ? 'true' : 'false', 'aria-label': '按住说话' + c.label, disabled: busy || !S, on: { click: () => pick(c) } },
|
|
112
|
+
h('span', { class: 'ck', html: CHECK }), h('span', { style: 'display:block;width:100%', html: sketch(c.col) }),
|
|
113
|
+
h('span', { class: 'lb' }, '按住说话' + c.label, c.sub ? h('small', {}, c.sub) : null));
|
|
114
|
+
}));
|
|
115
|
+
};
|
|
116
|
+
const pick = async (c) => {
|
|
117
|
+
if (!S || busy || S.hand === c.hand) return;
|
|
118
|
+
busy = true; draw();
|
|
119
|
+
try { await api('PATCH', '/api/parent/settings', { kid: { hand: c.hand } }); S.hand = c.hand; toast('存好了:按住说话' + c.label + '。孩子下次进老师页就换过来'); }
|
|
120
|
+
catch (e) { toast('没改成:' + e.message); }
|
|
121
|
+
busy = false; draw();
|
|
122
|
+
};
|
|
123
|
+
const load = async () => {
|
|
124
|
+
try { S = await api('GET', '/api/parent/settings'); } catch (e) { toast('读不到:' + e.message); }
|
|
125
|
+
if (S) { document.title = S.title + ' · 家长 · 设置'; $('#title').textContent = S.title; }
|
|
126
|
+
draw();
|
|
127
|
+
};
|
|
128
|
+
draw();
|
|
129
|
+
load();
|
|
130
|
+
})();
|
|
131
|
+
</script>
|
|
132
|
+
</body></html>
|
|
133
|
+
`;
|