paseo-req-kit 1.12.1 → 1.13.0-beta.2
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/CHANGELOG.md +33 -0
- package/README.md +8 -3
- package/bin/bootstrap.d.mts +13 -0
- package/bin/bootstrap.mjs +444 -0
- package/bin/req-docs.d.mts +5 -0
- package/bin/req-docs.mjs +169 -0
- package/bin/req.mjs +21 -1
- package/contracts/cli-json.md +2 -0
- package/docs/adr/0032-npm-only-install.md +27 -0
- package/docs/adr/0033-prototype-review-and-ui-check.md +28 -0
- package/docs/adr/README.md +2 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +8 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +22 -2
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +1 -1
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +7 -1
- package/engine/prompts/03-design.md +5 -1
- package/engine/prompts/04b-design-revise.md +1 -1
- package/engine/prompts/browser-verify.md +2 -2
- package/engine/prompts/ui-check.md +15 -0
- package/engine/src/api.ts +17 -2
- package/engine/src/config-view.ts +1 -1
- package/engine/src/pipeline.ts +15 -7
- package/engine/src/review-assets/app.css +420 -0
- package/engine/src/review-assets/app.html +95 -0
- package/engine/src/review-assets/app.js +954 -0
- package/engine/src/review.ts +570 -0
- package/engine/src/ui-check-page.js +208 -0
- package/engine/src/ui-check.ts +366 -0
- package/package.json +3 -1
- package/paseo-plugin/client/data.ts +12 -1
- package/paseo-plugin/client/detail.tsx +3 -0
- package/paseo-plugin/client/inbox.tsx +2 -0
- package/paseo-plugin/client/review-link.tsx +29 -0
- package/paseo-plugin/index.server.ts +2 -1
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +7 -1
- package/paseo-plugin/shared/contracts.ts +9 -0
- package/site-dist/404.html +27 -0
- package/site-dist/assets/app.DWPPPhFm.js +1 -0
- package/site-dist/assets/changelog.md.CoelFFCL.js +1 -0
- package/site-dist/assets/changelog.md.CoelFFCL.lean.js +1 -0
- package/site-dist/assets/chunks/@localSearchIndexroot.Hdq0-4L3.js +1 -0
- package/site-dist/assets/chunks/VPLocalSearchBox.D3_9dRGY.js +9 -0
- package/site-dist/assets/chunks/framework.B9qR_F1m.js +20 -0
- package/site-dist/assets/chunks/theme.Bm8aLiLN.js +2 -0
- package/site-dist/assets/guide_getting-started.md.B4GJeoPz.js +6 -0
- package/site-dist/assets/guide_getting-started.md.B4GJeoPz.lean.js +1 -0
- package/site-dist/assets/guide_how-it-works.md.D-kQsF1t.js +25 -0
- package/site-dist/assets/guide_how-it-works.md.D-kQsF1t.lean.js +1 -0
- package/site-dist/assets/guide_index.md.CLnGCmGW.js +1 -0
- package/site-dist/assets/guide_index.md.CLnGCmGW.lean.js +1 -0
- package/site-dist/assets/guide_install.md.DkCyZsOU.js +42 -0
- package/site-dist/assets/guide_install.md.DkCyZsOU.lean.js +42 -0
- package/site-dist/assets/guide_maintainer.md.RZIzyCTV.js +1 -0
- package/site-dist/assets/guide_maintainer.md.RZIzyCTV.lean.js +1 -0
- package/site-dist/assets/guide_phases.md.BpGAb1re.js +1 -0
- package/site-dist/assets/guide_phases.md.BpGAb1re.lean.js +1 -0
- package/site-dist/assets/guide_project.md.nGlTCo4D.js +77 -0
- package/site-dist/assets/guide_project.md.nGlTCo4D.lean.js +77 -0
- package/site-dist/assets/guide_user.md.tiHFxIsQ.js +1 -0
- package/site-dist/assets/guide_user.md.tiHFxIsQ.lean.js +1 -0
- package/site-dist/assets/index.md.Cf_sSf_f.js +1 -0
- package/site-dist/assets/index.md.Cf_sSf_f.lean.js +1 -0
- package/site-dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/site-dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/site-dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/site-dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/site-dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/site-dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/site-dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/site-dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/site-dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/site-dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/site-dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/site-dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/site-dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/site-dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/site-dist/assets/reference_adr_0001-step-agents-under-origin-chat.md.RtRTq7oi.js +1 -0
- package/site-dist/assets/reference_adr_0001-step-agents-under-origin-chat.md.RtRTq7oi.lean.js +1 -0
- package/site-dist/assets/reference_adr_0002-no-directory-switch-in-chat.md.BxtN-Aa2.js +1 -0
- package/site-dist/assets/reference_adr_0002-no-directory-switch-in-chat.md.BxtN-Aa2.lean.js +1 -0
- package/site-dist/assets/reference_adr_0003-script-engine-not-hub.md.66EgZtqx.js +1 -0
- package/site-dist/assets/reference_adr_0003-script-engine-not-hub.md.66EgZtqx.lean.js +1 -0
- package/site-dist/assets/reference_adr_0004-result-files.md.DbqcCozI.js +1 -0
- package/site-dist/assets/reference_adr_0004-result-files.md.DbqcCozI.lean.js +1 -0
- package/site-dist/assets/reference_adr_0005-no-default-branch-local-commits-only.md.CkImA4s6.js +1 -0
- package/site-dist/assets/reference_adr_0005-no-default-branch-local-commits-only.md.CkImA4s6.lean.js +1 -0
- package/site-dist/assets/reference_adr_0006-skill-install-targets.md.CwY8YepV.js +1 -0
- package/site-dist/assets/reference_adr_0006-skill-install-targets.md.CwY8YepV.lean.js +1 -0
- package/site-dist/assets/reference_adr_0007-own-interview-skill.md.CryhxRyG.js +1 -0
- package/site-dist/assets/reference_adr_0007-own-interview-skill.md.CryhxRyG.lean.js +1 -0
- package/site-dist/assets/reference_adr_0008-entries-do-not-call-entries.md.DsRWAgeA.js +1 -0
- package/site-dist/assets/reference_adr_0008-entries-do-not-call-entries.md.DsRWAgeA.lean.js +1 -0
- package/site-dist/assets/reference_adr_0009-versioned-install-layout.md.CSg1VqlW.js +1 -0
- package/site-dist/assets/reference_adr_0009-versioned-install-layout.md.CSg1VqlW.lean.js +1 -0
- package/site-dist/assets/reference_adr_0010-generated-feature-overview.md.D8F3MK-h.js +1 -0
- package/site-dist/assets/reference_adr_0010-generated-feature-overview.md.D8F3MK-h.lean.js +1 -0
- package/site-dist/assets/reference_adr_0011-resume-after-completion.md.BCrZrKjf.js +1 -0
- package/site-dist/assets/reference_adr_0011-resume-after-completion.md.BCrZrKjf.lean.js +1 -0
- package/site-dist/assets/reference_adr_0012-review-panel.md.k9rrHFFc.js +1 -0
- package/site-dist/assets/reference_adr_0012-review-panel.md.k9rrHFFc.lean.js +1 -0
- package/site-dist/assets/reference_adr_0013-no-release-tooling.md.BQeJ8dEI.js +1 -0
- package/site-dist/assets/reference_adr_0013-no-release-tooling.md.BQeJ8dEI.lean.js +1 -0
- package/site-dist/assets/reference_adr_0014-notifier-slot.md.C23QR1xO.js +1 -0
- package/site-dist/assets/reference_adr_0014-notifier-slot.md.C23QR1xO.lean.js +1 -0
- package/site-dist/assets/reference_adr_0015-incremental-continue.md.Buy7wwO8.js +1 -0
- package/site-dist/assets/reference_adr_0015-incremental-continue.md.Buy7wwO8.lean.js +1 -0
- package/site-dist/assets/reference_adr_0016-upgrade-from-tags.md.qfIW-tqJ.js +1 -0
- package/site-dist/assets/reference_adr_0016-upgrade-from-tags.md.qfIW-tqJ.lean.js +1 -0
- package/site-dist/assets/reference_adr_0017-use-paseo-capabilities.md.CjcGli87.js +1 -0
- package/site-dist/assets/reference_adr_0017-use-paseo-capabilities.md.CjcGli87.lean.js +1 -0
- package/site-dist/assets/reference_adr_0018-own-worktree-option.md.fVDpgGSE.js +1 -0
- package/site-dist/assets/reference_adr_0018-own-worktree-option.md.fVDpgGSE.lean.js +1 -0
- package/site-dist/assets/reference_adr_0019-parallel-orchestration.md.DrfrV0To.js +1 -0
- package/site-dist/assets/reference_adr_0019-parallel-orchestration.md.DrfrV0To.lean.js +1 -0
- package/site-dist/assets/reference_adr_0020-scheduling-through-the-chat.md.BqSemGFG.js +1 -0
- package/site-dist/assets/reference_adr_0020-scheduling-through-the-chat.md.BqSemGFG.lean.js +1 -0
- package/site-dist/assets/reference_adr_0021-npm-version-manager.md.CU3PG6y5.js +1 -0
- package/site-dist/assets/reference_adr_0021-npm-version-manager.md.CU3PG6y5.lean.js +1 -0
- package/site-dist/assets/reference_adr_0022-develop-and-verify-phases.md.C6fQDLq4.js +1 -0
- package/site-dist/assets/reference_adr_0022-develop-and-verify-phases.md.C6fQDLq4.lean.js +1 -0
- package/site-dist/assets/reference_adr_0023-types-gate-slices.md.D2E6yyYQ.js +1 -0
- package/site-dist/assets/reference_adr_0023-types-gate-slices.md.D2E6yyYQ.lean.js +1 -0
- package/site-dist/assets/reference_adr_0024-confirm-early-review-axes.md.FmK_ke0x.js +1 -0
- package/site-dist/assets/reference_adr_0024-confirm-early-review-axes.md.FmK_ke0x.lean.js +1 -0
- package/site-dist/assets/reference_adr_0025-joint-requirements.md.ChoKvMUx.js +1 -0
- package/site-dist/assets/reference_adr_0025-joint-requirements.md.ChoKvMUx.lean.js +1 -0
- package/site-dist/assets/reference_adr_0026-machine-records-and-api.md.veybjBPq.js +1 -0
- package/site-dist/assets/reference_adr_0026-machine-records-and-api.md.veybjBPq.lean.js +1 -0
- package/site-dist/assets/reference_adr_0027-paseo-plugin.md.BMX33ZNU.js +1 -0
- package/site-dist/assets/reference_adr_0027-paseo-plugin.md.BMX33ZNU.lean.js +1 -0
- package/site-dist/assets/reference_adr_0028-external-integrations.md.DtdMe8RK.js +1 -0
- package/site-dist/assets/reference_adr_0028-external-integrations.md.DtdMe8RK.lean.js +1 -0
- package/site-dist/assets/reference_adr_0029-requirements-from-cards.md.BHd1SQPe.js +1 -0
- package/site-dist/assets/reference_adr_0029-requirements-from-cards.md.BHd1SQPe.lean.js +1 -0
- package/site-dist/assets/reference_adr_0030-card-intake-process.md.kLc-DG9_.js +1 -0
- package/site-dist/assets/reference_adr_0030-card-intake-process.md.kLc-DG9_.lean.js +1 -0
- package/site-dist/assets/reference_adr_0031-fewer-interventions.md.BDnQcz8g.js +1 -0
- package/site-dist/assets/reference_adr_0031-fewer-interventions.md.BDnQcz8g.lean.js +1 -0
- package/site-dist/assets/reference_adr_0032-npm-only-install.md.C-3L1FkX.js +1 -0
- package/site-dist/assets/reference_adr_0032-npm-only-install.md.C-3L1FkX.lean.js +1 -0
- package/site-dist/assets/reference_adr_0033-prototype-review-and-ui-check.md.hFcQzmaa.js +1 -0
- package/site-dist/assets/reference_adr_0033-prototype-review-and-ui-check.md.hFcQzmaa.lean.js +1 -0
- package/site-dist/assets/reference_adr_index.md.BYF-Ofor.js +1 -0
- package/site-dist/assets/reference_adr_index.md.BYF-Ofor.lean.js +1 -0
- package/site-dist/assets/reference_commands.md.ClKbS-t8.js +1 -0
- package/site-dist/assets/reference_commands.md.ClKbS-t8.lean.js +1 -0
- package/site-dist/assets/reference_config.md.CRe27LaA.js +6 -0
- package/site-dist/assets/reference_config.md.CRe27LaA.lean.js +1 -0
- package/site-dist/assets/reference_contracts_cli-json.md.N-v2HNUd.js +1 -0
- package/site-dist/assets/reference_contracts_cli-json.md.N-v2HNUd.lean.js +1 -0
- package/site-dist/assets/reference_contracts_feature-files.md.DSiA8OEH.js +26 -0
- package/site-dist/assets/reference_contracts_feature-files.md.DSiA8OEH.lean.js +26 -0
- package/site-dist/assets/reference_contracts_index.md.Bkznz4px.js +1 -0
- package/site-dist/assets/reference_contracts_index.md.Bkznz4px.lean.js +1 -0
- package/site-dist/assets/reference_contracts_notifier-event.md.CCfrkm_A.js +24 -0
- package/site-dist/assets/reference_contracts_notifier-event.md.CCfrkm_A.lean.js +24 -0
- package/site-dist/assets/reference_contracts_result-file.md.DmR4fjB-.js +1 -0
- package/site-dist/assets/reference_contracts_result-file.md.DmR4fjB-.lean.js +1 -0
- package/site-dist/assets/reference_contracts_spec.md.BwwS0L_j.js +8 -0
- package/site-dist/assets/reference_contracts_spec.md.BwwS0L_j.lean.js +8 -0
- package/site-dist/assets/reference_contracts_state.md.5bNTC3ow.js +1 -0
- package/site-dist/assets/reference_contracts_state.md.5bNTC3ow.lean.js +1 -0
- package/site-dist/assets/reference_contracts_usage-record.md._C0OXWeq.js +10 -0
- package/site-dist/assets/reference_contracts_usage-record.md._C0OXWeq.lean.js +10 -0
- package/site-dist/assets/reference_contracts_verify.md.CbA88u6S.js +29 -0
- package/site-dist/assets/reference_contracts_verify.md.CbA88u6S.lean.js +29 -0
- package/site-dist/assets/reference_scope.md.Kzqw8U_f.js +1 -0
- package/site-dist/assets/reference_scope.md.Kzqw8U_f.lean.js +1 -0
- package/site-dist/assets/style.Dwsa4SMe.css +1 -0
- package/site-dist/changelog.html +30 -0
- package/site-dist/guide/getting-started.html +35 -0
- package/site-dist/guide/how-it-works.html +54 -0
- package/site-dist/guide/index.html +30 -0
- package/site-dist/guide/install.html +71 -0
- package/site-dist/guide/maintainer.html +30 -0
- package/site-dist/guide/phases.html +30 -0
- package/site-dist/guide/project.html +106 -0
- package/site-dist/guide/user.html +30 -0
- package/site-dist/hashmap.json +1 -0
- package/site-dist/icons/agents.svg +1 -0
- package/site-dist/icons/chat.svg +1 -0
- package/site-dist/icons/paseo.svg +1 -0
- package/site-dist/icons/phases.svg +1 -0
- package/site-dist/icons/review.svg +1 -0
- package/site-dist/icons/safe.svg +1 -0
- package/site-dist/index.html +30 -0
- package/site-dist/install.ps1 +118 -0
- package/site-dist/install.sh +198 -0
- package/site-dist/logo.svg +11 -0
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +30 -0
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +30 -0
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +30 -0
- package/site-dist/reference/adr/0004-result-files.html +30 -0
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +30 -0
- package/site-dist/reference/adr/0006-skill-install-targets.html +30 -0
- package/site-dist/reference/adr/0007-own-interview-skill.html +30 -0
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +30 -0
- package/site-dist/reference/adr/0009-versioned-install-layout.html +30 -0
- package/site-dist/reference/adr/0010-generated-feature-overview.html +30 -0
- package/site-dist/reference/adr/0011-resume-after-completion.html +30 -0
- package/site-dist/reference/adr/0012-review-panel.html +30 -0
- package/site-dist/reference/adr/0013-no-release-tooling.html +30 -0
- package/site-dist/reference/adr/0014-notifier-slot.html +30 -0
- package/site-dist/reference/adr/0015-incremental-continue.html +30 -0
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +30 -0
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +30 -0
- package/site-dist/reference/adr/0018-own-worktree-option.html +30 -0
- package/site-dist/reference/adr/0019-parallel-orchestration.html +30 -0
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +30 -0
- package/site-dist/reference/adr/0021-npm-version-manager.html +30 -0
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +30 -0
- package/site-dist/reference/adr/0023-types-gate-slices.html +30 -0
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +30 -0
- package/site-dist/reference/adr/0025-joint-requirements.html +30 -0
- package/site-dist/reference/adr/0026-machine-records-and-api.html +30 -0
- package/site-dist/reference/adr/0027-paseo-plugin.html +30 -0
- package/site-dist/reference/adr/0028-external-integrations.html +30 -0
- package/site-dist/reference/adr/0029-requirements-from-cards.html +30 -0
- package/site-dist/reference/adr/0030-card-intake-process.html +30 -0
- package/site-dist/reference/adr/0031-fewer-interventions.html +30 -0
- package/site-dist/reference/adr/0032-npm-only-install.html +30 -0
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +30 -0
- package/site-dist/reference/adr/index.html +30 -0
- package/site-dist/reference/commands.html +30 -0
- package/site-dist/reference/config.html +35 -0
- package/site-dist/reference/contracts/cli-json.html +30 -0
- package/site-dist/reference/contracts/feature-files.html +55 -0
- package/site-dist/reference/contracts/index.html +30 -0
- package/site-dist/reference/contracts/notifier-event.html +53 -0
- package/site-dist/reference/contracts/result-file.html +30 -0
- package/site-dist/reference/contracts/spec.html +37 -0
- package/site-dist/reference/contracts/state.html +30 -0
- package/site-dist/reference/contracts/usage-record.html +39 -0
- package/site-dist/reference/contracts/verify.html +58 -0
- package/site-dist/reference/scope.html +30 -0
- package/site-dist/vp-icons.css +1 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
// The prototype review page (req-kit 1.13). Plain JavaScript, no build step. The prototype is
|
|
2
|
+
// shown in a same-origin iframe in which nothing of its own runs (the server sends it with
|
|
3
|
+
// script-src 'none'); this page reads and marks it from outside.
|
|
4
|
+
(() => {
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const WIDTHS = [
|
|
8
|
+
{ px: 1280, label: "电脑" },
|
|
9
|
+
{ px: 768, label: "平板" },
|
|
10
|
+
{ px: 390, label: "手机" },
|
|
11
|
+
];
|
|
12
|
+
const KINDS = [
|
|
13
|
+
{ id: "text", label: "文字改成…" },
|
|
14
|
+
{ id: "bigger", label: "大一点" },
|
|
15
|
+
{ id: "smaller", label: "小一点" },
|
|
16
|
+
{ id: "color", label: "换颜色" },
|
|
17
|
+
{ id: "move", label: "换位置" },
|
|
18
|
+
{ id: "remove", label: "删掉" },
|
|
19
|
+
{ id: "add", label: "这里加东西" },
|
|
20
|
+
{ id: "other", label: "其他" },
|
|
21
|
+
];
|
|
22
|
+
const KIND_LABEL = Object.fromEntries(KINDS.map((k) => [k.id, k.label.replace("…", "")]));
|
|
23
|
+
const NOTE_HINT = {
|
|
24
|
+
text: "为什么这样改(可以不写)",
|
|
25
|
+
bigger: "大多少、为什么(可以不写)",
|
|
26
|
+
smaller: "小多少、为什么(可以不写)",
|
|
27
|
+
color: "想要什么颜色,例如:和旁边的“取消”按钮一样的灰色",
|
|
28
|
+
move: "挪到哪里,例如:放到表格右上角",
|
|
29
|
+
remove: "为什么不要(可以不写)",
|
|
30
|
+
add: "要加什么,例如:在这里加一个“导出”按钮",
|
|
31
|
+
other: "想怎么改",
|
|
32
|
+
};
|
|
33
|
+
const STATUS = {
|
|
34
|
+
waiting_for_approval: ["等你确认方案", true],
|
|
35
|
+
waiting_for_business: ["等你回答问题", true],
|
|
36
|
+
running: ["开发中", false],
|
|
37
|
+
completed: ["已完成", false],
|
|
38
|
+
stopped: ["已停止", false],
|
|
39
|
+
failed: ["没有完成", false],
|
|
40
|
+
queued: ["排队中", false],
|
|
41
|
+
};
|
|
42
|
+
const CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳";
|
|
43
|
+
const circled = (n) => (n >= 1 && n <= CIRCLED.length ? CIRCLED[n - 1] : `(${n})`);
|
|
44
|
+
|
|
45
|
+
const $ = (id) => document.getElementById(id);
|
|
46
|
+
const frame = $("frame");
|
|
47
|
+
const view = {
|
|
48
|
+
scale: 1,
|
|
49
|
+
info: null,
|
|
50
|
+
file: null,
|
|
51
|
+
state: null,
|
|
52
|
+
width: 1280,
|
|
53
|
+
mode: "comment",
|
|
54
|
+
comments: [],
|
|
55
|
+
general: "",
|
|
56
|
+
regions: [],
|
|
57
|
+
pending: null,
|
|
58
|
+
clickTimer: null,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Talking to the server
|
|
63
|
+
|
|
64
|
+
async function api(path, options) {
|
|
65
|
+
const r = await fetch(path, options);
|
|
66
|
+
const body = await r.json().catch(() => ({ ok: false, message: `服务返回 ${r.status}` }));
|
|
67
|
+
if (!r.ok && body.ok !== true) throw Object.assign(new Error(body.message || `服务返回 ${r.status}`), { body });
|
|
68
|
+
return body;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let saveTimer = null;
|
|
72
|
+
function saveDraft() {
|
|
73
|
+
if (view.info?.readOnly) return;
|
|
74
|
+
clearTimeout(saveTimer);
|
|
75
|
+
$("saved").textContent = "正在保存…";
|
|
76
|
+
saveTimer = setTimeout(async () => {
|
|
77
|
+
try {
|
|
78
|
+
await api("api/draft", {
|
|
79
|
+
method: "PUT",
|
|
80
|
+
headers: { "content-type": "application/json" },
|
|
81
|
+
body: JSON.stringify({ comments: view.comments, general: view.general }),
|
|
82
|
+
});
|
|
83
|
+
$("saved").textContent = "已自动保存,关掉页面也不会丢。";
|
|
84
|
+
} catch (e) {
|
|
85
|
+
$("saved").textContent = `没能保存:${e.message}`;
|
|
86
|
+
}
|
|
87
|
+
}, 400);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Header controls
|
|
92
|
+
|
|
93
|
+
function seg(container, items, current, onPick) {
|
|
94
|
+
container.replaceChildren(
|
|
95
|
+
...items.map((it) => {
|
|
96
|
+
const b = document.createElement("button");
|
|
97
|
+
b.type = "button";
|
|
98
|
+
b.textContent = it.label;
|
|
99
|
+
b.setAttribute("aria-pressed", String(it.value === current));
|
|
100
|
+
b.addEventListener("click", () => onPick(it.value));
|
|
101
|
+
return b;
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderControls() {
|
|
107
|
+
const files = view.info.files;
|
|
108
|
+
$("filesGroup").hidden = files.length < 2;
|
|
109
|
+
seg($("files"), files.map((f) => ({ label: f.title, value: f.path })), view.file, (v) => {
|
|
110
|
+
view.file = v;
|
|
111
|
+
view.state = null;
|
|
112
|
+
loadFrame();
|
|
113
|
+
});
|
|
114
|
+
seg($("widths"), WIDTHS.map((w) => ({ label: `${w.label} ${w.px}`, value: w.px })), view.width, (v) => {
|
|
115
|
+
view.width = v;
|
|
116
|
+
renderControls();
|
|
117
|
+
sizeFrame();
|
|
118
|
+
});
|
|
119
|
+
seg(
|
|
120
|
+
$("modes"),
|
|
121
|
+
[
|
|
122
|
+
{ label: "点选提意见", value: "comment" },
|
|
123
|
+
{ label: "试用", value: "try" },
|
|
124
|
+
],
|
|
125
|
+
view.mode,
|
|
126
|
+
(v) => {
|
|
127
|
+
view.mode = v;
|
|
128
|
+
closePop();
|
|
129
|
+
renderControls();
|
|
130
|
+
hint();
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderStates(doc) {
|
|
136
|
+
const states = [...new Set([...doc.querySelectorAll("[data-state]")].map((e) => e.getAttribute("data-state")).filter(Boolean))];
|
|
137
|
+
$("statesGroup").hidden = states.length === 0;
|
|
138
|
+
if (states.length > 0 && !states.includes(view.state)) view.state = states[0];
|
|
139
|
+
if (states.length === 0) view.state = null;
|
|
140
|
+
seg($("states"), states.map((s) => ({ label: s, value: s })), view.state, (v) => {
|
|
141
|
+
view.state = v;
|
|
142
|
+
applyState();
|
|
143
|
+
afterLayout();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function hint() {
|
|
148
|
+
const scaled = view.scale < 1 ? `(窗口不够宽,按 ${Math.round(view.scale * 100)}% 显示)` : "";
|
|
149
|
+
$("hint").textContent =
|
|
150
|
+
(view.mode === "comment"
|
|
151
|
+
? "点选模式:点任何地方提意见,双击文字直接改字。要像平时一样点按钮、切换页面时,换到“试用”。"
|
|
152
|
+
: "试用模式:可以像平时一样点。要提意见时换回“点选提意见”。") + scaled;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// The prototype in the frame
|
|
157
|
+
|
|
158
|
+
const doc = () => frame.contentDocument;
|
|
159
|
+
|
|
160
|
+
function loadFrame() {
|
|
161
|
+
closePop();
|
|
162
|
+
renderControls();
|
|
163
|
+
frame.src = `proto/cur/${view.file.split("/").map(encodeURIComponent).join("/")}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
frame.addEventListener("load", () => {
|
|
167
|
+
const d = doc();
|
|
168
|
+
if (!d || !d.body) return;
|
|
169
|
+
const style = d.createElement("style");
|
|
170
|
+
style.id = "rk-style";
|
|
171
|
+
style.textContent = `
|
|
172
|
+
.rk-hover { outline: 2px solid #2457d6 !important; outline-offset: 2px !important; cursor: pointer !important; }
|
|
173
|
+
.rk-region { outline: 2px dashed #d97706 !important; outline-offset: 4px !important; }
|
|
174
|
+
.rk-changed { outline: 2px solid #10b981 !important; outline-offset: 2px !important; box-shadow: inset 0 0 0 9999px rgba(16,185,129,.10) !important; }
|
|
175
|
+
.rk-edited { box-shadow: inset 0 0 0 9999px rgba(250,204,21,.35) !important; }
|
|
176
|
+
.rk-flash { outline: 3px solid #e11d48 !important; outline-offset: 3px !important; }
|
|
177
|
+
#rk-layer { position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 2147483646; }
|
|
178
|
+
#rk-layer > * { position: absolute; font: 700 12px/22px system-ui, sans-serif; border-radius: 999px; text-align: center; box-shadow: 0 1px 4px rgba(0,0,0,.35); }
|
|
179
|
+
.rk-badge { background: #d97706; color: #111; padding: 0 7px; pointer-events: none; white-space: nowrap; }
|
|
180
|
+
.rk-pin { background: #e11d48; color: #fff; min-width: 22px; height: 22px; padding: 0 5px; cursor: pointer; }
|
|
181
|
+
[contenteditable="true"] { outline: 2px solid #f59e0b !important; background: #fffbe6 !important; color: #111 !important; }
|
|
182
|
+
`;
|
|
183
|
+
d.head.appendChild(style);
|
|
184
|
+
const layer = d.createElement("div");
|
|
185
|
+
layer.id = "rk-layer";
|
|
186
|
+
d.body.appendChild(layer);
|
|
187
|
+
d.addEventListener("mouseover", onHover, true);
|
|
188
|
+
d.addEventListener("mouseout", onHoverOut, true);
|
|
189
|
+
d.addEventListener("click", onClick, true);
|
|
190
|
+
d.addEventListener("dblclick", onDblClick, true);
|
|
191
|
+
d.addEventListener("submit", (e) => e.preventDefault(), true);
|
|
192
|
+
d.addEventListener("keydown", (e) => {
|
|
193
|
+
if (e.key === "Escape") closePop();
|
|
194
|
+
});
|
|
195
|
+
renderStates(d);
|
|
196
|
+
applyState();
|
|
197
|
+
afterLayout();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
function applyState() {
|
|
201
|
+
const d = doc();
|
|
202
|
+
if (!d) return;
|
|
203
|
+
let s = d.getElementById("rk-state");
|
|
204
|
+
if (!s) {
|
|
205
|
+
s = d.createElement("style");
|
|
206
|
+
s.id = "rk-state";
|
|
207
|
+
d.head.appendChild(s);
|
|
208
|
+
}
|
|
209
|
+
s.textContent = view.state ? `[data-state]:not([data-state="${CSS.escape(view.state)}"]) { display: none !important; }` : "";
|
|
210
|
+
renderStates(d);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The prototype at the chosen width, shrunk to fit when the window is narrower. */
|
|
214
|
+
function sizeFrame() {
|
|
215
|
+
frame.style.width = `${view.width}px`;
|
|
216
|
+
const room = Math.max(200, $("frameWrap").clientWidth - 26);
|
|
217
|
+
view.scale = Math.min(1, room / view.width);
|
|
218
|
+
frame.style.transform = view.scale < 1 ? `scale(${view.scale})` : "";
|
|
219
|
+
const d = doc();
|
|
220
|
+
let h = 480;
|
|
221
|
+
if (d?.documentElement) {
|
|
222
|
+
frame.style.height = "480px";
|
|
223
|
+
h = Math.max(480, d.documentElement.scrollHeight, d.body?.scrollHeight ?? 0) + 4;
|
|
224
|
+
}
|
|
225
|
+
frame.style.height = `${h}px`;
|
|
226
|
+
const sizer = $("sizer");
|
|
227
|
+
sizer.style.width = `${Math.floor(view.width * view.scale)}px`;
|
|
228
|
+
sizer.style.height = `${Math.ceil(h * view.scale)}px`;
|
|
229
|
+
hint();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function afterLayout() {
|
|
233
|
+
sizeFrame();
|
|
234
|
+
// Sizes settle after the frame grows; mark once it has.
|
|
235
|
+
requestAnimationFrame(() => {
|
|
236
|
+
markRegions();
|
|
237
|
+
drawPins();
|
|
238
|
+
if ($("showDiff").checked) markDiff();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function visible(el) {
|
|
243
|
+
const r = el.getBoundingClientRect();
|
|
244
|
+
return r.width > 0 && r.height > 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function pageRect(el) {
|
|
248
|
+
const r = el.getBoundingClientRect();
|
|
249
|
+
const w = el.ownerDocument.defaultView;
|
|
250
|
+
return { left: r.left + w.scrollX, top: r.top + w.scrollY, width: r.width, height: r.height };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function layer() {
|
|
254
|
+
return doc()?.getElementById("rk-layer");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function clearLayer(cls) {
|
|
258
|
+
layer()
|
|
259
|
+
?.querySelectorAll(`.${cls}`)
|
|
260
|
+
.forEach((n) => n.remove());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// What to look at: the regions the design marked with data-ac
|
|
265
|
+
|
|
266
|
+
function markRegions() {
|
|
267
|
+
const d = doc();
|
|
268
|
+
if (!d) return;
|
|
269
|
+
d.querySelectorAll(".rk-region").forEach((e) => e.classList.remove("rk-region"));
|
|
270
|
+
clearLayer("rk-badge");
|
|
271
|
+
view.regions = [...d.querySelectorAll("[data-ac]")].filter(visible);
|
|
272
|
+
const show = $("showRegions").checked;
|
|
273
|
+
view.regions.forEach((el, i) => {
|
|
274
|
+
if (!show) return;
|
|
275
|
+
el.classList.add("rk-region");
|
|
276
|
+
const r = pageRect(el);
|
|
277
|
+
const b = d.createElement("div");
|
|
278
|
+
b.className = "rk-badge";
|
|
279
|
+
b.textContent = `${circled(i + 1)} ${el.getAttribute("data-ac")}`;
|
|
280
|
+
b.style.left = `${Math.max(0, r.left - 6)}px`;
|
|
281
|
+
b.style.top = `${Math.max(0, r.top - 14)}px`;
|
|
282
|
+
layer().appendChild(b);
|
|
283
|
+
});
|
|
284
|
+
renderRegions();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function criterionText(ac) {
|
|
288
|
+
const ids = String(ac || "")
|
|
289
|
+
.split(/[\s,,、]+/)
|
|
290
|
+
.filter(Boolean);
|
|
291
|
+
return ids
|
|
292
|
+
.map((id) => view.info.criteria.find((c) => c.id === id))
|
|
293
|
+
.filter(Boolean)
|
|
294
|
+
.map((c) => `${c.id}:${c.text}`)
|
|
295
|
+
.join(";");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderRegions() {
|
|
299
|
+
const ul = $("regions");
|
|
300
|
+
if (view.regions.length === 0) {
|
|
301
|
+
const crit = view.info.criteria;
|
|
302
|
+
ul.replaceChildren(
|
|
303
|
+
li(
|
|
304
|
+
`<p class="muted small">这个原型没有标出改动区域。可以对照下面的验收标准看${view.state ? "(也看看上面切换出来的其他状态)" : ""}。</p>` +
|
|
305
|
+
(crit.length ? `<ul class="diff-list">${crit.map((c) => `<li>${esc(c.id)}:${esc(c.text)}</li>`).join("")}</ul>` : ""),
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
ul.replaceChildren(
|
|
311
|
+
...view.regions.map((el, i) => {
|
|
312
|
+
const ac = el.getAttribute("data-ac");
|
|
313
|
+
const label = el.getAttribute("data-label") || "";
|
|
314
|
+
const item = li(
|
|
315
|
+
`<div class="row"><span class="badge region">${circled(i + 1)}</span><div class="grow"><div class="strong">${esc(label || ac)}</div><div class="muted small">${esc(criterionText(ac) || ac)}</div></div><button type="button" class="linkish">看一下</button></div>`,
|
|
316
|
+
);
|
|
317
|
+
item.querySelector("button").addEventListener("click", () => flash(el));
|
|
318
|
+
return item;
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function li(html) {
|
|
324
|
+
const n = document.createElement("li");
|
|
325
|
+
n.innerHTML = html;
|
|
326
|
+
return n;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function esc(s) {
|
|
330
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function flash(el) {
|
|
334
|
+
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
335
|
+
const r = pageRect(el);
|
|
336
|
+
const wrap = $("frameWrap");
|
|
337
|
+
wrap.scrollTo({ top: Math.max(0, r.top * view.scale - wrap.clientHeight / 3), behavior: "smooth" });
|
|
338
|
+
el.classList.add("rk-flash");
|
|
339
|
+
setTimeout(() => el.classList.remove("rk-flash"), 1600);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Describing what was clicked, in words a person and an agent can both use
|
|
344
|
+
|
|
345
|
+
const TAG_DESC = {
|
|
346
|
+
BUTTON: "按钮",
|
|
347
|
+
A: "链接",
|
|
348
|
+
INPUT: "输入框",
|
|
349
|
+
SELECT: "下拉框",
|
|
350
|
+
TEXTAREA: "多行输入框",
|
|
351
|
+
IMG: "图片",
|
|
352
|
+
H1: "标题",
|
|
353
|
+
H2: "标题",
|
|
354
|
+
H3: "标题",
|
|
355
|
+
H4: "小标题",
|
|
356
|
+
H5: "小标题",
|
|
357
|
+
H6: "小标题",
|
|
358
|
+
TABLE: "表格",
|
|
359
|
+
TH: "表头",
|
|
360
|
+
TD: "表格单元格",
|
|
361
|
+
TR: "表格行",
|
|
362
|
+
LABEL: "标签文字",
|
|
363
|
+
LI: "列表项",
|
|
364
|
+
P: "文字",
|
|
365
|
+
SPAN: "文字",
|
|
366
|
+
STRONG: "文字",
|
|
367
|
+
EM: "文字",
|
|
368
|
+
SMALL: "小字",
|
|
369
|
+
NAV: "导航",
|
|
370
|
+
HEADER: "页头",
|
|
371
|
+
FOOTER: "页脚",
|
|
372
|
+
FORM: "表单",
|
|
373
|
+
UL: "列表",
|
|
374
|
+
OL: "列表",
|
|
375
|
+
};
|
|
376
|
+
const ROLE_DESC = { button: "按钮", link: "链接", tab: "标签页", dialog: "弹窗", checkbox: "勾选框", switch: "开关", menuitem: "菜单项", alert: "提示" };
|
|
377
|
+
|
|
378
|
+
function hex(color) {
|
|
379
|
+
const m = /rgba?\(([^)]+)\)/.exec(color || "");
|
|
380
|
+
if (!m) return color || "";
|
|
381
|
+
const [r, g, b, a = "1"] = m[1].split(/[\s,/]+/).filter(Boolean);
|
|
382
|
+
if (Number(a) === 0) return "";
|
|
383
|
+
return `#${[r, g, b].map((v) => Number(v).toString(16).padStart(2, "0")).join("")}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function backgroundOf(el) {
|
|
387
|
+
for (let n = el; n && n.nodeType === 1; n = n.parentElement) {
|
|
388
|
+
const c = hex(n.ownerDocument.defaultView.getComputedStyle(n).backgroundColor);
|
|
389
|
+
if (c) return c;
|
|
390
|
+
}
|
|
391
|
+
return "#ffffff";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function textOf(el) {
|
|
395
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") return el.value || el.getAttribute("placeholder") || "";
|
|
396
|
+
if (el.tagName === "IMG") return el.getAttribute("alt") || "";
|
|
397
|
+
return (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function cssPath(el) {
|
|
401
|
+
const parts = [];
|
|
402
|
+
for (let n = el; n && n.nodeType === 1 && n.tagName !== "HTML"; n = n.parentElement) {
|
|
403
|
+
if (n.id && !n.id.startsWith("rk-")) {
|
|
404
|
+
parts.unshift(`#${CSS.escape(n.id)}`);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
let part = n.tagName.toLowerCase();
|
|
408
|
+
const parent = n.parentElement;
|
|
409
|
+
if (parent) {
|
|
410
|
+
const same = [...parent.children].filter((c) => c.tagName === n.tagName);
|
|
411
|
+
if (same.length > 1) part += `:nth-of-type(${same.indexOf(n) + 1})`;
|
|
412
|
+
}
|
|
413
|
+
parts.unshift(part);
|
|
414
|
+
if (n.tagName === "BODY") break;
|
|
415
|
+
}
|
|
416
|
+
return parts.join(" > ");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function describe(el) {
|
|
420
|
+
const w = el.ownerDocument.defaultView;
|
|
421
|
+
const cs = w.getComputedStyle(el);
|
|
422
|
+
const r = el.getBoundingClientRect();
|
|
423
|
+
const role = el.getAttribute("role");
|
|
424
|
+
let desc = (role && ROLE_DESC[role]) || TAG_DESC[el.tagName] || "区域";
|
|
425
|
+
if (el.tagName === "INPUT" && /checkbox|radio/.test(el.type)) desc = el.type === "checkbox" ? "勾选框" : "单选项";
|
|
426
|
+
if (el.tagName === "INPUT" && /submit|button/.test(el.type)) desc = "按钮";
|
|
427
|
+
const regionEl = el.closest("[data-ac]");
|
|
428
|
+
const index = regionEl ? view.regions.indexOf(regionEl) + 1 : 0;
|
|
429
|
+
return {
|
|
430
|
+
desc,
|
|
431
|
+
text: textOf(el).slice(0, 120),
|
|
432
|
+
selector: cssPath(el),
|
|
433
|
+
region: regionEl ? { ac: regionEl.getAttribute("data-ac"), label: regionEl.getAttribute("data-label") || "", index: index > 0 ? circled(index) : null } : null,
|
|
434
|
+
styles: {
|
|
435
|
+
color: hex(cs.color),
|
|
436
|
+
background: backgroundOf(el),
|
|
437
|
+
fontSize: cs.fontSize,
|
|
438
|
+
size: `${Math.round(r.width)}×${Math.round(r.height)}`,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** A click on a big container or the page itself: say where it is by what is around it. */
|
|
444
|
+
function targetOf(el) {
|
|
445
|
+
if (el.id === "rk-layer" || el.closest?.("#rk-layer")) return null;
|
|
446
|
+
if (el.tagName === "HTML" || el.tagName === "BODY") return { el, empty: true };
|
|
447
|
+
return { el, empty: false };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Clicking in the prototype
|
|
452
|
+
|
|
453
|
+
let hovered = null;
|
|
454
|
+
function onHover(e) {
|
|
455
|
+
if (view.mode !== "comment") return;
|
|
456
|
+
const t = e.target;
|
|
457
|
+
if (!t || t.nodeType !== 1 || t.closest("#rk-layer") || t.tagName === "BODY" || t.tagName === "HTML") return;
|
|
458
|
+
hovered?.classList.remove("rk-hover");
|
|
459
|
+
hovered = t;
|
|
460
|
+
t.classList.add("rk-hover");
|
|
461
|
+
}
|
|
462
|
+
function onHoverOut(e) {
|
|
463
|
+
if (e.target === hovered) {
|
|
464
|
+
hovered.classList.remove("rk-hover");
|
|
465
|
+
hovered = null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function onClick(e) {
|
|
470
|
+
const pin = e.target.closest?.(".rk-pin");
|
|
471
|
+
if (pin) {
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
e.stopPropagation();
|
|
474
|
+
focusComment(pin.dataset.id);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (view.mode !== "comment") return;
|
|
478
|
+
if (e.target.isContentEditable) return;
|
|
479
|
+
e.preventDefault();
|
|
480
|
+
e.stopPropagation();
|
|
481
|
+
if (view.info.readOnly) return;
|
|
482
|
+
const t = targetOf(e.target);
|
|
483
|
+
if (!t) return;
|
|
484
|
+
// A double click edits the text instead: wait a moment before opening the box.
|
|
485
|
+
clearTimeout(view.clickTimer);
|
|
486
|
+
const { clientX, clientY } = e;
|
|
487
|
+
view.clickTimer = setTimeout(() => openPop(t, clientX, clientY), 260);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function onDblClick(e) {
|
|
491
|
+
if (view.mode !== "comment" || view.info.readOnly) return;
|
|
492
|
+
clearTimeout(view.clickTimer);
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
e.stopPropagation();
|
|
495
|
+
const el = e.target;
|
|
496
|
+
if (!el || el.nodeType !== 1 || el.closest("#rk-layer")) return;
|
|
497
|
+
const hasText = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim());
|
|
498
|
+
if (!hasText || el.tagName === "BODY") {
|
|
499
|
+
openPop({ el, empty: false }, e.clientX, e.clientY, "text");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
closePop();
|
|
503
|
+
const before = describe(el);
|
|
504
|
+
const original = el.dataset.rkOriginal ?? textOf(el);
|
|
505
|
+
el.dataset.rkOriginal = original;
|
|
506
|
+
el.contentEditable = "true";
|
|
507
|
+
el.focus();
|
|
508
|
+
const range = el.ownerDocument.createRange();
|
|
509
|
+
range.selectNodeContents(el);
|
|
510
|
+
const sel = el.ownerDocument.getSelection();
|
|
511
|
+
sel.removeAllRanges();
|
|
512
|
+
sel.addRange(range);
|
|
513
|
+
const finish = () => {
|
|
514
|
+
el.contentEditable = "false";
|
|
515
|
+
el.removeEventListener("blur", finish);
|
|
516
|
+
el.removeEventListener("keydown", onKey);
|
|
517
|
+
const now = textOf(el);
|
|
518
|
+
if (now && now !== original) {
|
|
519
|
+
el.classList.add("rk-edited");
|
|
520
|
+
const existing = view.comments.find((c) => c.kind === "text" && c.file === view.file && c.target?.selector === before.selector);
|
|
521
|
+
if (existing) existing.newText = now;
|
|
522
|
+
else addComment({ kind: "text", newText: now, note: "", target: { ...before, text: original } });
|
|
523
|
+
renderComments();
|
|
524
|
+
drawPins();
|
|
525
|
+
saveDraft();
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const onKey = (k) => {
|
|
529
|
+
if (k.key === "Enter" && !k.shiftKey) {
|
|
530
|
+
k.preventDefault();
|
|
531
|
+
el.blur();
|
|
532
|
+
}
|
|
533
|
+
if (k.key === "Escape") {
|
|
534
|
+
el.textContent = original;
|
|
535
|
+
el.blur();
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
el.addEventListener("blur", finish);
|
|
539
|
+
el.addEventListener("keydown", onKey);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// The "how to change it" box
|
|
544
|
+
|
|
545
|
+
function openPop(t, x, y, kind) {
|
|
546
|
+
const el = t.el;
|
|
547
|
+
const info = t.empty ? { desc: "空白处", text: "", selector: "body", region: null, styles: null } : describe(el);
|
|
548
|
+
view.pending = { el, info, kind: kind || (t.empty ? "add" : null), edit: null };
|
|
549
|
+
$("popTitle").textContent = t.empty ? "在页面空白处" : `${info.desc}${info.text ? `“${info.text.slice(0, 30)}${info.text.length > 30 ? "…" : ""}”` : ""}`;
|
|
550
|
+
$("popSub").textContent = info.region
|
|
551
|
+
? `在区域 ${info.region.index ?? ""}${info.region.label ? `「${info.region.label}」` : ""}${info.region.ac ? `(${info.region.ac})` : ""}`
|
|
552
|
+
: "选一种改法,再补一句";
|
|
553
|
+
renderKinds();
|
|
554
|
+
$("note").value = "";
|
|
555
|
+
$("newText").value = info.text || "";
|
|
556
|
+
syncPopFields();
|
|
557
|
+
const pop = $("pop");
|
|
558
|
+
pop.hidden = false;
|
|
559
|
+
const fr = frame.getBoundingClientRect();
|
|
560
|
+
const px = Math.min(window.innerWidth - pop.offsetWidth - 12, Math.max(12, fr.left + x * view.scale + 12));
|
|
561
|
+
const py = Math.min(window.innerHeight - pop.offsetHeight - 12, Math.max(12, fr.top + y * view.scale + 12));
|
|
562
|
+
pop.style.left = `${px}px`;
|
|
563
|
+
pop.style.top = `${py}px`;
|
|
564
|
+
(view.pending.kind ? $("note") : $("kinds").querySelector("button"))?.focus();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function editComment(c) {
|
|
568
|
+
const el = findTarget(c);
|
|
569
|
+
view.pending = { el, info: c.target, kind: c.kind, edit: c.id };
|
|
570
|
+
$("popTitle").textContent = `修改第 ${view.comments.indexOf(c) + 1} 条意见`;
|
|
571
|
+
$("popSub").textContent = c.target?.desc ? `${c.target.desc}${c.target.text ? `“${c.target.text.slice(0, 30)}”` : ""}` : "";
|
|
572
|
+
renderKinds();
|
|
573
|
+
$("note").value = c.note || "";
|
|
574
|
+
$("newText").value = c.newText || "";
|
|
575
|
+
syncPopFields();
|
|
576
|
+
const pop = $("pop");
|
|
577
|
+
pop.hidden = false;
|
|
578
|
+
pop.style.left = `${Math.max(12, window.innerWidth - pop.offsetWidth - 400)}px`;
|
|
579
|
+
pop.style.top = "120px";
|
|
580
|
+
$("note").focus();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function renderKinds() {
|
|
584
|
+
$("kinds").replaceChildren(
|
|
585
|
+
...KINDS.map((k) => {
|
|
586
|
+
const b = document.createElement("button");
|
|
587
|
+
b.type = "button";
|
|
588
|
+
b.textContent = k.label;
|
|
589
|
+
b.setAttribute("role", "radio");
|
|
590
|
+
b.setAttribute("aria-checked", String(view.pending.kind === k.id));
|
|
591
|
+
b.addEventListener("click", () => {
|
|
592
|
+
view.pending.kind = k.id;
|
|
593
|
+
renderKinds();
|
|
594
|
+
syncPopFields();
|
|
595
|
+
(k.id === "text" ? $("newText") : $("note")).focus();
|
|
596
|
+
});
|
|
597
|
+
return b;
|
|
598
|
+
}),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function syncPopFields() {
|
|
603
|
+
const kind = view.pending?.kind;
|
|
604
|
+
$("newTextField").hidden = kind !== "text";
|
|
605
|
+
$("newTextLabel").textContent = "改成这样的文字";
|
|
606
|
+
$("noteLabel").textContent = NOTE_HINT[kind] || "用你自己的话说说(可以不写)";
|
|
607
|
+
$("popOk").disabled = !kind;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function closePop() {
|
|
611
|
+
$("pop").hidden = true;
|
|
612
|
+
view.pending = null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
$("popCancel").addEventListener("click", closePop);
|
|
616
|
+
$("pop").addEventListener("keydown", (e) => {
|
|
617
|
+
if (e.key === "Escape") closePop();
|
|
618
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) $("popOk").click();
|
|
619
|
+
});
|
|
620
|
+
$("popOk").addEventListener("click", () => {
|
|
621
|
+
const p = view.pending;
|
|
622
|
+
if (!p?.kind) return;
|
|
623
|
+
const note = $("note").value.trim();
|
|
624
|
+
const newText = p.kind === "text" ? $("newText").value.trim() : "";
|
|
625
|
+
if (p.kind === "text" && !newText) {
|
|
626
|
+
$("newText").focus();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (p.edit) {
|
|
630
|
+
const c = view.comments.find((x) => x.id === p.edit);
|
|
631
|
+
if (c) Object.assign(c, { kind: p.kind, note, newText: newText || undefined });
|
|
632
|
+
} else {
|
|
633
|
+
addComment({ kind: p.kind, note, newText: newText || undefined, target: p.info });
|
|
634
|
+
}
|
|
635
|
+
closePop();
|
|
636
|
+
renderComments();
|
|
637
|
+
drawPins();
|
|
638
|
+
saveDraft();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
function addComment(c) {
|
|
642
|
+
view.comments.push({ id: `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, file: view.file, state: view.state, width: view.width, ...c });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
// The list of comments and their pins
|
|
647
|
+
|
|
648
|
+
function findTarget(c) {
|
|
649
|
+
if (c.file !== view.file || !c.target?.selector) return null;
|
|
650
|
+
try {
|
|
651
|
+
return doc()?.querySelector(c.target.selector) ?? null;
|
|
652
|
+
} catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function drawPins() {
|
|
658
|
+
clearLayer("rk-pin");
|
|
659
|
+
const d = doc();
|
|
660
|
+
if (!d || !layer()) return;
|
|
661
|
+
view.comments.forEach((c, i) => {
|
|
662
|
+
if (c.file !== view.file || (c.state && view.state && c.state !== view.state)) return;
|
|
663
|
+
const el = findTarget(c);
|
|
664
|
+
if (!el || !visible(el)) return;
|
|
665
|
+
const r = pageRect(el);
|
|
666
|
+
const pin = d.createElement("div");
|
|
667
|
+
pin.className = "rk-pin";
|
|
668
|
+
pin.dataset.id = c.id;
|
|
669
|
+
pin.textContent = String(i + 1);
|
|
670
|
+
pin.title = `第 ${i + 1} 条:${KIND_LABEL[c.kind]}`;
|
|
671
|
+
pin.style.left = `${Math.max(0, r.left + r.width - 8)}px`;
|
|
672
|
+
pin.style.top = `${Math.max(0, r.top - 15)}px`;
|
|
673
|
+
layer().appendChild(pin);
|
|
674
|
+
if (c.kind === "text" && c.newText && el.dataset.rkOriginal === undefined && !el.isContentEditable) {
|
|
675
|
+
// Show the requested text in place, as the person typed it (plain text elements only:
|
|
676
|
+
// replacing the text of one with icons inside would drop the icons).
|
|
677
|
+
el.classList.add("rk-edited");
|
|
678
|
+
if (el.childElementCount === 0) {
|
|
679
|
+
el.dataset.rkOriginal = textOf(el);
|
|
680
|
+
el.textContent = c.newText;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function summary(c) {
|
|
687
|
+
if (c.kind === "text") return `文字改成“${c.newText ?? ""}”`;
|
|
688
|
+
return KIND_LABEL[c.kind] || "其他";
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function renderComments() {
|
|
692
|
+
$("count").textContent = view.comments.length ? `(${view.comments.length})` : "";
|
|
693
|
+
const ul = $("comments");
|
|
694
|
+
if (view.comments.length === 0) {
|
|
695
|
+
ul.replaceChildren(li('<p class="muted small">还没有意见。点原型上想改的地方开始。</p>'));
|
|
696
|
+
} else {
|
|
697
|
+
ul.replaceChildren(
|
|
698
|
+
...view.comments.map((c, i) => {
|
|
699
|
+
const where = [
|
|
700
|
+
view.info.files.length > 1 ? (view.info.files.find((f) => f.path === c.file)?.title ?? c.file) : "",
|
|
701
|
+
c.state ? `状态“${c.state}”` : "",
|
|
702
|
+
c.target?.region?.index ? `区域 ${c.target.region.index}` : "",
|
|
703
|
+
]
|
|
704
|
+
.filter(Boolean)
|
|
705
|
+
.join(" · ");
|
|
706
|
+
const item = li(
|
|
707
|
+
`<div class="row"><span class="badge pin">${i + 1}</span><div class="grow"><div class="strong">${esc(summary(c))}</div>` +
|
|
708
|
+
`<div class="muted small">${esc(c.target?.desc ?? "")}${c.target?.text ? `“${esc(c.target.text.slice(0, 40))}”` : ""}${where ? ` · ${esc(where)}` : ""}</div>` +
|
|
709
|
+
(c.note ? `<div class="small">${esc(c.note)}</div>` : "") +
|
|
710
|
+
`<div><button type="button" class="linkish" data-act="show">在原型上看</button><button type="button" class="linkish" data-act="edit">修改</button><button type="button" class="linkish danger" data-act="del">删除</button></div></div></div>`,
|
|
711
|
+
);
|
|
712
|
+
item.dataset.id = c.id;
|
|
713
|
+
item.querySelector('[data-act="show"]').addEventListener("click", () => showComment(c));
|
|
714
|
+
item.querySelector('[data-act="edit"]').addEventListener("click", () => editComment(c));
|
|
715
|
+
item.querySelector('[data-act="del"]').addEventListener("click", () => {
|
|
716
|
+
view.comments = view.comments.filter((x) => x.id !== c.id);
|
|
717
|
+
const el = findTarget(c);
|
|
718
|
+
if (el && el.dataset.rkOriginal !== undefined && c.kind === "text") {
|
|
719
|
+
el.textContent = el.dataset.rkOriginal;
|
|
720
|
+
delete el.dataset.rkOriginal;
|
|
721
|
+
el.classList.remove("rk-edited");
|
|
722
|
+
}
|
|
723
|
+
renderComments();
|
|
724
|
+
drawPins();
|
|
725
|
+
saveDraft();
|
|
726
|
+
});
|
|
727
|
+
return item;
|
|
728
|
+
}),
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
renderActions();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function showComment(c) {
|
|
735
|
+
const go = () => {
|
|
736
|
+
const el = findTarget(c);
|
|
737
|
+
if (el) flash(el);
|
|
738
|
+
focusComment(c.id);
|
|
739
|
+
};
|
|
740
|
+
if (c.file !== view.file) {
|
|
741
|
+
view.file = c.file;
|
|
742
|
+
view.state = c.state ?? null;
|
|
743
|
+
frame.addEventListener("load", () => setTimeout(go, 150), { once: true });
|
|
744
|
+
loadFrame();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (c.state && c.state !== view.state) {
|
|
748
|
+
view.state = c.state;
|
|
749
|
+
applyState();
|
|
750
|
+
afterLayout();
|
|
751
|
+
}
|
|
752
|
+
setTimeout(go, 60);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function focusComment(id) {
|
|
756
|
+
const item = [...$("comments").children].find((n) => n.dataset.id === id);
|
|
757
|
+
if (!item) return;
|
|
758
|
+
item.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
759
|
+
item.classList.add("flash");
|
|
760
|
+
setTimeout(() => item.classList.remove("flash"), 1500);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
// What changed since the version before
|
|
765
|
+
|
|
766
|
+
function leafTexts(root) {
|
|
767
|
+
const out = [];
|
|
768
|
+
const walk = (n) => {
|
|
769
|
+
if (n.nodeType !== 1 || /^(SCRIPT|STYLE|TEMPLATE)$/.test(n.tagName) || n.id === "rk-layer") return;
|
|
770
|
+
const own = [...n.childNodes]
|
|
771
|
+
.filter((c) => c.nodeType === 3)
|
|
772
|
+
.map((c) => c.textContent)
|
|
773
|
+
.join("")
|
|
774
|
+
.replace(/\s+/g, " ")
|
|
775
|
+
.trim();
|
|
776
|
+
const attr = n.getAttribute("placeholder") || n.getAttribute("alt") || (n.tagName === "INPUT" ? n.getAttribute("value") : "") || "";
|
|
777
|
+
if (own || attr) out.push({ el: n, text: own || attr });
|
|
778
|
+
[...n.children].forEach(walk);
|
|
779
|
+
};
|
|
780
|
+
walk(root);
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function markDiff() {
|
|
785
|
+
const d = doc();
|
|
786
|
+
if (!d) return;
|
|
787
|
+
d.querySelectorAll(".rk-changed").forEach((e) => e.classList.remove("rk-changed"));
|
|
788
|
+
const prev = view.info.versions.previous;
|
|
789
|
+
const card = $("changesCard");
|
|
790
|
+
if (!$("showDiff").checked || !prev) {
|
|
791
|
+
card.hidden = true;
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
card.hidden = false;
|
|
795
|
+
let html;
|
|
796
|
+
try {
|
|
797
|
+
const r = await fetch(`proto/v/${prev}/${view.file.split("/").map(encodeURIComponent).join("/")}`);
|
|
798
|
+
if (r.status === 404) {
|
|
799
|
+
$("changes").innerHTML = '<p class="muted small">这个页面是这一版新加的。</p>';
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
html = await r.text();
|
|
803
|
+
} catch {
|
|
804
|
+
$("changes").innerHTML = '<p class="muted small">没能读到上一版。</p>';
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const old = new DOMParser().parseFromString(html, "text/html");
|
|
808
|
+
const oldTexts = new Set(leafTexts(old.body).map((x) => x.text));
|
|
809
|
+
const now = leafTexts(d.body);
|
|
810
|
+
const nowTexts = new Set(now.map((x) => x.text));
|
|
811
|
+
let changed = 0;
|
|
812
|
+
for (const x of now) {
|
|
813
|
+
if (!oldTexts.has(x.text) && visible(x.el)) {
|
|
814
|
+
x.el.classList.add("rk-changed");
|
|
815
|
+
changed++;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const removed = [...oldTexts].filter((t) => !nowTexts.has(t)).slice(0, 20);
|
|
819
|
+
$("changes").innerHTML =
|
|
820
|
+
`<p class="small">绿色框是这一版新出现或改过文字的地方,共 ${changed} 处。</p>` +
|
|
821
|
+
(removed.length ? `<p class="small muted">上一版有、这一版没有的文字:</p><ul class="diff-list">${removed.map((t) => `<li>${esc(t.slice(0, 60))}</li>`).join("")}</ul>` : "") +
|
|
822
|
+
(changed === 0 && removed.length === 0 ? '<p class="muted small">文字没有变化,改动可能在样式或布局上。</p>' : "");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ---------------------------------------------------------------------------
|
|
826
|
+
// Sending
|
|
827
|
+
|
|
828
|
+
function renderActions() {
|
|
829
|
+
const info = view.info;
|
|
830
|
+
const note = $("actionNote");
|
|
831
|
+
const changes = $("submitChanges");
|
|
832
|
+
const approve = $("approve");
|
|
833
|
+
if (info.readOnly) {
|
|
834
|
+
$("actions").hidden = true;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
changes.textContent = view.comments.length ? `提交修改意见(${view.comments.length} 条)` : "提交修改意见";
|
|
838
|
+
const hasAny = view.comments.length > 0 || view.general.trim().length > 0;
|
|
839
|
+
if (!info.canReply) {
|
|
840
|
+
note.textContent = "这个需求不是从 Paseo 对话开始的,这里的意见只会保存,请在卡片或终端中处理。";
|
|
841
|
+
changes.disabled = true;
|
|
842
|
+
approve.disabled = !info.waitingForApproval;
|
|
843
|
+
} else if (info.waitingForApproval) {
|
|
844
|
+
note.textContent = hasAny ? "提交后,需求会话会按你的意见改需求说明,再请你确认。" : "看完了,没有要改的就同意;有要改的先点原型提意见。";
|
|
845
|
+
changes.disabled = !hasAny;
|
|
846
|
+
approve.disabled = false;
|
|
847
|
+
} else if (info.waitingForReply) {
|
|
848
|
+
note.textContent = "需求现在在等你回答问题,意见会一起转给需求会话。";
|
|
849
|
+
changes.disabled = !hasAny;
|
|
850
|
+
approve.disabled = true;
|
|
851
|
+
} else {
|
|
852
|
+
note.textContent = `现在不在等你确认方案(${STATUS[info.status]?.[0] ?? info.status ?? "还没开始"})。可以先写意见,会自动保存,等到要确认时再提交。`;
|
|
853
|
+
changes.disabled = true;
|
|
854
|
+
approve.disabled = true;
|
|
855
|
+
}
|
|
856
|
+
approve.hidden = !info.waitingForApproval;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function submit(action) {
|
|
860
|
+
const result = $("result");
|
|
861
|
+
if (action === "approve" && view.comments.length > 0 && !confirm(`还有 ${view.comments.length} 条意见没有提交。确定不改,直接同意开始写代码吗?`)) return;
|
|
862
|
+
$("submitChanges").disabled = true;
|
|
863
|
+
$("approve").disabled = true;
|
|
864
|
+
result.className = "result";
|
|
865
|
+
result.textContent = "正在提交…";
|
|
866
|
+
try {
|
|
867
|
+
const r = await api("api/submit", {
|
|
868
|
+
method: "POST",
|
|
869
|
+
headers: { "content-type": "application/json" },
|
|
870
|
+
body: JSON.stringify({ action, comments: view.comments, general: view.general }),
|
|
871
|
+
});
|
|
872
|
+
result.className = "result ok";
|
|
873
|
+
result.textContent = action === "approve" ? `已确认方案,开始写代码。${r.message ? `(${r.message.split("\n").at(-1)})` : ""}` : "已转给需求会话。它会按你的意见改需求说明,改好后请你确认。";
|
|
874
|
+
view.comments = [];
|
|
875
|
+
view.general = "";
|
|
876
|
+
$("general").value = "";
|
|
877
|
+
await refreshInfo();
|
|
878
|
+
renderComments();
|
|
879
|
+
drawPins();
|
|
880
|
+
} catch (e) {
|
|
881
|
+
result.className = "result err";
|
|
882
|
+
result.textContent = `没有提交成功:${e.message}`;
|
|
883
|
+
renderActions();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
$("submitChanges").addEventListener("click", () => submit("changes"));
|
|
887
|
+
$("approve").addEventListener("click", () => submit("approve"));
|
|
888
|
+
$("general").addEventListener("input", (e) => {
|
|
889
|
+
view.general = e.target.value;
|
|
890
|
+
renderActions();
|
|
891
|
+
saveDraft();
|
|
892
|
+
});
|
|
893
|
+
$("showRegions").addEventListener("change", () => markRegions());
|
|
894
|
+
$("showDiff").addEventListener("change", () => markDiff());
|
|
895
|
+
window.addEventListener("resize", () => afterLayout());
|
|
896
|
+
document.addEventListener("keydown", (e) => {
|
|
897
|
+
if (e.key === "Escape") closePop();
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// ---------------------------------------------------------------------------
|
|
901
|
+
// Start
|
|
902
|
+
|
|
903
|
+
async function refreshInfo() {
|
|
904
|
+
view.info = await api("api/info");
|
|
905
|
+
const [label, waiting] = STATUS[view.info.status] ?? [view.info.status ? view.info.status : "还没开始", false];
|
|
906
|
+
const pill = $("status");
|
|
907
|
+
pill.hidden = false;
|
|
908
|
+
pill.textContent = label;
|
|
909
|
+
pill.className = waiting ? "pill wait" : "pill";
|
|
910
|
+
$("diffWrap").hidden = !view.info.versions.previous;
|
|
911
|
+
renderActions();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function start() {
|
|
915
|
+
hint();
|
|
916
|
+
try {
|
|
917
|
+
await refreshInfo();
|
|
918
|
+
} catch (e) {
|
|
919
|
+
$("title").textContent = "打不开";
|
|
920
|
+
$("empty").hidden = false;
|
|
921
|
+
$("empty").textContent = e.message;
|
|
922
|
+
$("frameWrap").hidden = true;
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const info = view.info;
|
|
926
|
+
$("title").textContent = info.title;
|
|
927
|
+
document.title = `原型审查 · ${info.title}`;
|
|
928
|
+
if (info.readOnly) $("guide").open = false;
|
|
929
|
+
try {
|
|
930
|
+
const draft = await api("api/draft");
|
|
931
|
+
view.comments = Array.isArray(draft.comments) ? draft.comments : [];
|
|
932
|
+
view.general = typeof draft.general === "string" ? draft.general : "";
|
|
933
|
+
$("general").value = view.general;
|
|
934
|
+
if (view.comments.length) $("saved").textContent = `上次写的 ${view.comments.length} 条意见还在。`;
|
|
935
|
+
} catch {
|
|
936
|
+
/* start empty */
|
|
937
|
+
}
|
|
938
|
+
renderComments();
|
|
939
|
+
if (info.files.length === 0) {
|
|
940
|
+
$("frameWrap").hidden = true;
|
|
941
|
+
$("empty").hidden = false;
|
|
942
|
+
$("empty").textContent = "这个需求还没有界面原型。界面改动的需求在方案设计完成后才会有,到时再打开这个页面。";
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
view.file = info.files[0].path;
|
|
946
|
+
// On a phone, start with the phone width.
|
|
947
|
+
if (window.innerWidth < 700) view.width = 390;
|
|
948
|
+
// Auto-suggest the diff when the design was revised since the last look.
|
|
949
|
+
if (info.versions.previous) $("showDiff").checked = true;
|
|
950
|
+
loadFrame();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
start();
|
|
954
|
+
})();
|