@pioneer_zmc/dsh-workbench 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/README.zh.md +100 -0
- package/client.js +122 -0
- package/cordis.patch.yml +8 -0
- package/dist/check-building.js +1669 -0
- package/dist/check-room.js +980 -0
- package/dist/index.js +2873 -0
- package/package.json +60 -0
- package/skills/specdev-building/SKILL.md +85 -0
- package/skills/specdev-building/assets/layouts/court.json +53 -0
- package/skills/specdev-building/assets/layouts/row.json +48 -0
- package/skills/specdev-building/assets/layouts/single.json +27 -0
- package/skills/specdev-building/assets/layouts/tower-9.json +71 -0
- package/skills/specdev-building/assets/layouts/tower.json +59 -0
- package/skills/specdev-building/references/blueprint-contract.md +126 -0
- package/skills/specdev-building/references/diagnostics.md +101 -0
- package/skills/specdev-building/references/layouts.md +112 -0
- package/skills/specdev-business/SKILL.md +91 -0
- package/skills/specdev-business/assets/plan-template.md +66 -0
- package/skills/specdev-business/assets/spec-template.md +111 -0
- package/skills/specdev-business/assets/tasks-template.md +36 -0
- package/skills/specdev-business/references/change.md +117 -0
- package/skills/specdev-business/references/evidence-review.md +125 -0
- package/skills/specdev-business/references/implementation.md +67 -0
- package/skills/specdev-business/references/project-contract.md +211 -0
- package/skills/specdev-business/references/source-map.md +110 -0
- package/skills/specdev-business/references/specification.md +93 -0
- package/skills/specdev-business/references/workflow.md +126 -0
- package/skills/specdev-room/SKILL.md +77 -0
- package/skills/specdev-room/assets/intro-room.json +14 -0
- package/skills/specdev-room/references/diagnostics.md +21 -0
- package/skills/specdev-room/references/room-contract.md +59 -0
- package/vendor/archify-renderer/VERSION.md +52 -0
- package/vendor/archify-renderer/archify/LICENSE +22 -0
- package/vendor/archify-renderer/archify/THIRD_PARTY_NOTICES.md +56 -0
- package/vendor/archify-renderer/archify/assets/template.html +14787 -0
- package/vendor/archify-renderer/archify/bin/archify.mjs +1990 -0
- package/vendor/archify-renderer/archify/renderers/shared/brand-marks.mjs +563 -0
- package/vendor/archify-renderer/archify/renderers/shared/cli.mjs +218 -0
- package/vendor/archify-renderer/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/vendor/archify-renderer/archify/renderers/shared/diagnostics.mjs +127 -0
- package/vendor/archify-renderer/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/vendor/archify-renderer/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/vendor/archify-renderer/archify/renderers/shared/generated-validators.mjs +13 -0
- package/vendor/archify-renderer/archify/renderers/shared/geometry.mjs +1423 -0
- package/vendor/archify-renderer/archify/renderers/shared/i18n.mjs +594 -0
- package/vendor/archify-renderer/archify/renderers/shared/legend.mjs +217 -0
- package/vendor/archify-renderer/archify/renderers/shared/output-path.mjs +321 -0
- package/vendor/archify-renderer/archify/renderers/shared/repository-evidence.mjs +235 -0
- package/vendor/archify-renderer/archify/renderers/shared/text-fit.mjs +49 -0
- package/vendor/archify-renderer/archify/renderers/shared/utils.mjs +232 -0
- package/vendor/archify-renderer/archify/renderers/shared/validator.mjs +86 -0
- package/vendor/archify-renderer/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/vendor/archify-renderer/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/vendor/archify-renderer/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/vendor/archify-renderer/archify/schemas/common.schema.json +115 -0
- package/vendor/archify-renderer/archify/schemas/workflow.schema.json +428 -0
- package/vendor/archify-renderer/archify/scripts/check-render-output.mjs +835 -0
- package/vendor/archify-renderer/shims/process.mjs +18 -0
- package/vendor/archify-renderer/stubs/child_process.mjs +2 -0
- package/vendor/archify-renderer/stubs/crypto.mjs +3 -0
- package/vendor/archify-renderer/stubs/dns-promises.mjs +2 -0
- package/vendor/archify-renderer/stubs/fs.mjs +3 -0
- package/vendor/archify-renderer/stubs/http.mjs +3 -0
- package/vendor/archify-renderer/stubs/https.mjs +3 -0
- package/vendor/archify-renderer/stubs/net.mjs +3 -0
- package/vendor/archify-renderer/stubs/path.mjs +3 -0
- package/vendor/archify-renderer/stubs/url.mjs +3 -0
- package/web/assets/building-kit.js +1157 -0
- package/web/assets/building-look.js +185 -0
- package/web/assets/building-plan.js +495 -0
- package/web/assets/building-scene.js +529 -0
- package/web/assets/building-walk.js +248 -0
- package/web/assets/building.js +859 -0
- package/web/assets/business.js +124 -0
- package/web/assets/chart-link.js +165 -0
- package/web/assets/common.js +302 -0
- package/web/assets/details.js +197 -0
- package/web/assets/evidence.js +30 -0
- package/web/assets/furniture/SOURCES.md +24 -0
- package/web/assets/furniture/assets/ceramic-display-stand-01/asset.json +25 -0
- package/web/assets/furniture/assets/ceramic-display-stand-01/model.js +116 -0
- package/web/assets/furniture/assets/ceramic-display-stand-01/preview.webp +0 -0
- package/web/assets/furniture/assets/ceramic-floor-lamp-01/asset.json +61 -0
- package/web/assets/furniture/assets/ceramic-floor-lamp-01/model.js +56 -0
- package/web/assets/furniture/assets/ceramic-floor-lamp-01/preview.webp +0 -0
- package/web/assets/furniture/assets/ceramic-low-cabinet-01/asset.json +81 -0
- package/web/assets/furniture/assets/ceramic-low-cabinet-01/model.js +67 -0
- package/web/assets/furniture/assets/ceramic-low-cabinet-01/preview.webp +0 -0
- package/web/assets/furniture/assets/ceramic-vase-01/asset.json +21 -0
- package/web/assets/furniture/assets/ceramic-vase-01/model.js +119 -0
- package/web/assets/furniture/assets/ceramic-vase-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-bed-01/asset.json +61 -0
- package/web/assets/furniture/assets/fairy-bed-01/model.js +63 -0
- package/web/assets/furniture/assets/fairy-bed-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-book-stack-01/asset.json +21 -0
- package/web/assets/furniture/assets/fairy-book-stack-01/model.js +71 -0
- package/web/assets/furniture/assets/fairy-book-stack-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-bookshelf-01/asset.json +32 -0
- package/web/assets/furniture/assets/fairy-bookshelf-01/model.js +168 -0
- package/web/assets/furniture/assets/fairy-bookshelf-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-desk-01/asset.json +25 -0
- package/web/assets/furniture/assets/fairy-desk-01/model.js +132 -0
- package/web/assets/furniture/assets/fairy-desk-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-notice-board-01/asset.json +23 -0
- package/web/assets/furniture/assets/fairy-notice-board-01/model.js +125 -0
- package/web/assets/furniture/assets/fairy-notice-board-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-potted-plant-01/asset.json +21 -0
- package/web/assets/furniture/assets/fairy-potted-plant-01/model.js +128 -0
- package/web/assets/furniture/assets/fairy-potted-plant-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-reading-chair-01/asset.json +61 -0
- package/web/assets/furniture/assets/fairy-reading-chair-01/model.js +66 -0
- package/web/assets/furniture/assets/fairy-reading-chair-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-table-lamp-01/asset.json +21 -0
- package/web/assets/furniture/assets/fairy-table-lamp-01/model.js +182 -0
- package/web/assets/furniture/assets/fairy-table-lamp-01/preview.webp +0 -0
- package/web/assets/furniture/catalog.js +137 -0
- package/web/assets/furniture/index.json +110 -0
- package/web/assets/furniture/registry.js +72 -0
- package/web/assets/furniture-page.css +68 -0
- package/web/assets/furniture-page.js +302 -0
- package/web/assets/home.js +65 -0
- package/web/assets/node-detail.js +148 -0
- package/web/assets/panel-link.js +47 -0
- package/web/assets/read.js +746 -0
- package/web/assets/room-scene.js +386 -0
- package/web/assets/room.css +287 -0
- package/web/assets/room.js +437 -0
- package/web/assets/rooms/bindings.js +449 -0
- package/web/assets/rooms/ceramic-reflection.js +193 -0
- package/web/assets/rooms/ceramic-room.js +302 -0
- package/web/assets/rooms/door.js +35 -0
- package/web/assets/rooms/fairy-room.js +492 -0
- package/web/assets/rooms/furniture-assembly.js +113 -0
- package/web/assets/rooms/navigation.js +262 -0
- package/web/assets/rooms/placeholder-catalog.json +71 -0
- package/web/assets/rooms/placeholder-furniture.js +76 -0
- package/web/assets/rooms/placement.js +588 -0
- package/web/assets/rooms/reader.js +441 -0
- package/web/assets/rooms/reading-gate.js +157 -0
- package/web/assets/rooms/room-lighting.js +50 -0
- package/web/assets/rooms/templates.json +50 -0
- package/web/assets/rooms/walk-frame.js +64 -0
- package/web/assets/rooms/walk-session.js +138 -0
- package/web/assets/save-result.js +89 -0
- package/web/assets/screenshots/business.png +0 -0
- package/web/assets/screenshots/node-detail.png +0 -0
- package/web/assets/screenshots/project-home.png +0 -0
- package/web/assets/screenshots/workflow.png +0 -0
- package/web/assets/showcase.js +232 -0
- package/web/assets/style.css +836 -0
- package/web/assets/template.js +7 -0
- package/web/assets/theme.js +29 -0
- package/web/assets/three/OrbitControls.js +1972 -0
- package/web/assets/three/VERSION.md +12 -0
- package/web/assets/three/three.core.js +60586 -0
- package/web/assets/three/three.module.js +19719 -0
- package/web/assets/workspaces.css +21 -0
- package/web/assets/workspaces.js +81 -0
- package/web/building.html +131 -0
- package/web/business.html +43 -0
- package/web/furniture.html +47 -0
- package/web/index.html +57 -0
- package/web/read.html +107 -0
- package/web/room.html +146 -0
- package/web/showcase.html +50 -0
- package/web/workspaces.html +32 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// 业务页:业务介绍、文档入口、图列表(每图带快照数与当前状态)。
|
|
2
|
+
import { $, chartStatusKind, countChartStatuses, el, fetchJson, pageTitle, renderEmptyWorkspaceParam, renderGuide, renderRepoLine, renderRepoState, setStatus, showError, STATUS_SUMMARY_ORDER, statusBadge, workspaceParamEmpty, wsUrl } from './common.js'
|
|
3
|
+
|
|
4
|
+
/** 图列表上方的状态汇总行:点某个状态跳到第一张该状态的图卡并闪一下框,
|
|
5
|
+
* 图多时不用逐张扫徽标找"要处理的"。 */
|
|
6
|
+
function renderChartSummary(charts, root) {
|
|
7
|
+
const box = $('chartSummary')
|
|
8
|
+
if (!box) return
|
|
9
|
+
const counts = countChartStatuses(charts)
|
|
10
|
+
const present = STATUS_SUMMARY_ORDER.filter(([kind]) => counts[kind])
|
|
11
|
+
if (present.length === 0) return
|
|
12
|
+
for (const [kind, label] of present) {
|
|
13
|
+
const chip = el('button', 'badge')
|
|
14
|
+
chip.type = 'button'
|
|
15
|
+
chip.dataset.kind = kind
|
|
16
|
+
chip.textContent = `${label} ${counts[kind]}`
|
|
17
|
+
chip.addEventListener('click', () => {
|
|
18
|
+
const target = root.querySelector(`.card[data-status="${kind}"]`)
|
|
19
|
+
if (!target) return
|
|
20
|
+
target.scrollIntoView({ block: 'center' })
|
|
21
|
+
target.classList.add('flash')
|
|
22
|
+
setTimeout(() => target.classList.remove('flash'), 1200)
|
|
23
|
+
})
|
|
24
|
+
box.append(chip)
|
|
25
|
+
}
|
|
26
|
+
box.hidden = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function businessIdFromLocation() {
|
|
30
|
+
const parts = location.pathname.split('/').filter(Boolean) // ['specdev-workbench','business','<id>']
|
|
31
|
+
return parts[2] || ''
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
if (workspaceParamEmpty) return renderEmptyWorkspaceParam()
|
|
36
|
+
const id = businessIdFromLocation()
|
|
37
|
+
if (!id) return showError('缺少业务 id(路径应为 /specdev-workbench/business/<业务id>)')
|
|
38
|
+
let inventory
|
|
39
|
+
try {
|
|
40
|
+
inventory = await fetchJson(wsUrl('/specdev-workbench/api/inventory'))
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (renderRepoState(error)) return
|
|
43
|
+
return showError(error.message)
|
|
44
|
+
}
|
|
45
|
+
if (inventory.code === 'repo-not-configured') return renderGuide(inventory)
|
|
46
|
+
renderRepoLine(inventory.repo)
|
|
47
|
+
|
|
48
|
+
const business = inventory.businesses.find((b) => b.id === id)
|
|
49
|
+
if (!business) return showError(`业务不存在:${id}`)
|
|
50
|
+
document.title = pageTitle(business.name, inventory.repo)
|
|
51
|
+
$('bizName').textContent = business.name
|
|
52
|
+
$('title').textContent = business.name
|
|
53
|
+
if (business.descriptorError) {
|
|
54
|
+
$('intro').textContent = business.descriptorError
|
|
55
|
+
setStatus('该业务的说明文件有问题,已被跳过展开', 'warn')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (business.intro) $('intro').textContent = business.intro
|
|
59
|
+
|
|
60
|
+
// P1b-2 独立业务房间入口:业务有效(说明文件检查通过)就显示,链接带工作区标识;
|
|
61
|
+
// 不为它预读房间接口——没有 room.json 时由房间页给出明确空态。
|
|
62
|
+
const roomEntry = $('roomEntry')
|
|
63
|
+
roomEntry.href = wsUrl(`/specdev-workbench/room/${encodeURIComponent(id)}`)
|
|
64
|
+
$('roomEntryLine').hidden = false
|
|
65
|
+
|
|
66
|
+
if (business.docs.length > 0) {
|
|
67
|
+
const list = $('docList')
|
|
68
|
+
for (const doc of business.docs) {
|
|
69
|
+
const li = el('li')
|
|
70
|
+
const a = el('a')
|
|
71
|
+
a.href = wsUrl(`/specdev-workbench/api/doc?business=${encodeURIComponent(id)}&path=${encodeURIComponent(doc)}`)
|
|
72
|
+
a.target = '_blank'
|
|
73
|
+
a.rel = 'noopener'
|
|
74
|
+
a.textContent = doc
|
|
75
|
+
li.appendChild(a)
|
|
76
|
+
list.appendChild(li)
|
|
77
|
+
}
|
|
78
|
+
$('docs').hidden = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const root = $('root')
|
|
82
|
+
root.textContent = ''
|
|
83
|
+
for (const chart of business.charts) {
|
|
84
|
+
const card = el('a', 'card')
|
|
85
|
+
card.href = wsUrl(`/specdev-workbench/read/${id}/${chart.id}`)
|
|
86
|
+
card.dataset.status = chartStatusKind(chart)
|
|
87
|
+
const h = el('h2')
|
|
88
|
+
h.textContent = chart.name
|
|
89
|
+
h.insertAdjacentHTML('beforeend', statusBadge(chart.currentStatus, chart.compareError))
|
|
90
|
+
card.appendChild(h)
|
|
91
|
+
if (chart.idConflict) {
|
|
92
|
+
const badge = el('span', 'badge')
|
|
93
|
+
badge.dataset.kind = 'invalid'
|
|
94
|
+
badge.textContent = '编号冲突'
|
|
95
|
+
card.appendChild(badge)
|
|
96
|
+
const p = el('p')
|
|
97
|
+
p.textContent = chart.idConflict
|
|
98
|
+
card.appendChild(p)
|
|
99
|
+
} else if (chart.descriptorError) {
|
|
100
|
+
const badge = el('span', 'badge')
|
|
101
|
+
badge.dataset.kind = 'invalid'
|
|
102
|
+
badge.textContent = '说明文件问题'
|
|
103
|
+
card.appendChild(badge)
|
|
104
|
+
const p = el('p')
|
|
105
|
+
p.textContent = chart.descriptorError
|
|
106
|
+
card.appendChild(p)
|
|
107
|
+
} else if (chart.summary) {
|
|
108
|
+
const p = el('p')
|
|
109
|
+
p.textContent = chart.summary
|
|
110
|
+
card.appendChild(p)
|
|
111
|
+
}
|
|
112
|
+
const meta = el('p', 'meta')
|
|
113
|
+
const invalidNote = chart.invalidTagCount > 0 ? `,另有 ${chart.invalidTagCount} 个不合约定的标签被忽略` : ''
|
|
114
|
+
meta.textContent = chart.snapshotCount > 0
|
|
115
|
+
? `快照 ${chart.snapshotCount} 版${invalidNote}`
|
|
116
|
+
: `还没有保存过快照${invalidNote}`
|
|
117
|
+
card.appendChild(meta)
|
|
118
|
+
root.appendChild(card)
|
|
119
|
+
}
|
|
120
|
+
renderChartSummary(business.charts, root)
|
|
121
|
+
setStatus(`${business.name}:${business.charts.length} 张图`, 'ok')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// 图与外层的连接:观察原生选中标记,并集中适配节点双击详情。
|
|
2
|
+
// 不修改 vendor,不拦截单击、键盘与拖动。上游升级时复验此适配层。
|
|
3
|
+
//
|
|
4
|
+
// 页面切版本是整页跳转,图与观察随页面一起没了,所以阅读页不需要在页内清理;
|
|
5
|
+
// dispose() 留给"同一页里想换一张图"的将来用法。
|
|
6
|
+
//
|
|
7
|
+
// 选中含义沿用旧增强阅读器的规则:**恰好一个**节点带标记才算选中,多个按未选中处理
|
|
8
|
+
// (那种情形是"多点聚焦",不是"选中了谁")。
|
|
9
|
+
|
|
10
|
+
const SVG_SELECTOR = 'svg[role="img"]'
|
|
11
|
+
const MARKER = 'data-focus-selected'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 从"带选中标记的节点"清单得出结论:恰好一个才算选中,多个按未选中处理。
|
|
15
|
+
* 纯函数(只认 { id }),Node 里可直接测。
|
|
16
|
+
*/
|
|
17
|
+
export function selectedId(selected) {
|
|
18
|
+
const ids = (selected || [])
|
|
19
|
+
.map((node) => (node && typeof node.id === 'string' ? node.id : ''))
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
return ids.length === 1 ? ids[0] : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从主图 SVG 里读一次当前选中:返回 { id, label } 或 null。
|
|
26
|
+
* label 取图上那个节点自己的名字(data-node-label);缺了就退回编号,不编造。
|
|
27
|
+
* 只用到 querySelectorAll / getAttribute,Node 里给个同形替身即可测。
|
|
28
|
+
*/
|
|
29
|
+
export function readSelection(svg) {
|
|
30
|
+
const selected = [...svg.querySelectorAll(`[${MARKER}]`)].map((node) => ({
|
|
31
|
+
id: node.getAttribute('data-node-id'),
|
|
32
|
+
label: node.getAttribute('data-node-label') || node.getAttribute('data-node-id'),
|
|
33
|
+
}))
|
|
34
|
+
const id = selectedId(selected)
|
|
35
|
+
if (!id) return null
|
|
36
|
+
const hit = selected.find((node) => node.id === id)
|
|
37
|
+
return { id, label: hit.label }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 进小房间取文档:装配前的空文档不算,等 load 落地后再交出来;被拦下走 onBlocked。
|
|
42
|
+
* 失败有尽头,不留"永远等着"的口子(复审修复):load 都到过了仍拿不到文档——
|
|
43
|
+
* 不会再有下一个信号,到此为止明确报;一直等不到 load,超时也报。
|
|
44
|
+
*/
|
|
45
|
+
function whenLoaded(frame, onDoc, onBlocked, timeoutMs = 10000) {
|
|
46
|
+
let settled = false
|
|
47
|
+
let timer = null
|
|
48
|
+
|
|
49
|
+
function finish(run) {
|
|
50
|
+
if (settled) return
|
|
51
|
+
settled = true
|
|
52
|
+
if (timer) clearTimeout(timer)
|
|
53
|
+
frame.removeEventListener('load', onLoad)
|
|
54
|
+
run()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function docOf() {
|
|
58
|
+
try {
|
|
59
|
+
return { doc: frame.contentDocument }
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return { blocked: `进不去图所在的小房间(${error.name || '跨源'})` }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onLoad() {
|
|
66
|
+
const got = docOf()
|
|
67
|
+
if (got.blocked) return finish(() => onBlocked(got.blocked))
|
|
68
|
+
const doc = got.doc
|
|
69
|
+
if (!doc) return finish(() => onBlocked('图加载完了,页面却进不去它的内部文档'))
|
|
70
|
+
if (doc.location && doc.location.href !== 'about:blank') return finish(() => onDoc(doc))
|
|
71
|
+
// 这次 load 的是装配前的空文档:不算数,接着等真正那次
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const initial = docOf()
|
|
75
|
+
if (initial.blocked) {
|
|
76
|
+
finish(() => onBlocked(initial.blocked))
|
|
77
|
+
return () => {}
|
|
78
|
+
}
|
|
79
|
+
if (initial.doc && initial.doc.location && initial.doc.location.href !== 'about:blank') {
|
|
80
|
+
finish(() => onDoc(initial.doc))
|
|
81
|
+
return () => {}
|
|
82
|
+
}
|
|
83
|
+
// 还没加载出来或还在空文档:等真正那次 load;真等不来,到点按超时收尾
|
|
84
|
+
frame.addEventListener('load', onLoad)
|
|
85
|
+
timer = setTimeout(
|
|
86
|
+
() => finish(() => onBlocked(`等图加载等得太久(超过 ${Math.max(1, Math.round(timeoutMs / 1000))} 秒)`)),
|
|
87
|
+
timeoutMs,
|
|
88
|
+
)
|
|
89
|
+
return () => finish(() => {})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 接上画布里的图:onSelection({ id, label } | null, ...) 每次结论变化报一次(含初读);
|
|
94
|
+
* 接不上(进不去小房间 / 图里找不到主图 / 等图加载等到超时)走 onUnavailable(原因),只报一次。
|
|
95
|
+
* timeoutMs 是等图加载的最长时间(毫秒,默认 10 秒),到点仍没等到就按接不上报。
|
|
96
|
+
* 返回 { dispose() },调用方不需要时可以不接(切版本整页重载)。
|
|
97
|
+
*/
|
|
98
|
+
export function connectChartSelection(frame, { onSelection, onUnavailable, onOpenDetail, timeoutMs } = {}) {
|
|
99
|
+
let observer = null
|
|
100
|
+
let removeDoubleClick = () => {}
|
|
101
|
+
let stopWaiting = () => {}
|
|
102
|
+
let cancelled = false
|
|
103
|
+
let reported = false
|
|
104
|
+
let seen = false
|
|
105
|
+
let lastId = null
|
|
106
|
+
|
|
107
|
+
const fail = (reason) => {
|
|
108
|
+
if (cancelled || reported) return
|
|
109
|
+
reported = true
|
|
110
|
+
onUnavailable?.(reason)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const publish = (selection) => {
|
|
114
|
+
if (cancelled) return
|
|
115
|
+
const id = selection ? selection.id : null
|
|
116
|
+
if (seen && id === lastId) return // 结论没变不重复打扰
|
|
117
|
+
seen = true
|
|
118
|
+
lastId = id
|
|
119
|
+
onSelection?.(selection)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const start = (doc) => {
|
|
123
|
+
let svg = null
|
|
124
|
+
try {
|
|
125
|
+
svg = doc.querySelector(SVG_SELECTOR)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
fail(`图里的内容读不到(${error.name || '取不到' })`)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (!svg) {
|
|
131
|
+
fail('图里找不到主图')
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const sync = () => publish(readSelection(svg))
|
|
135
|
+
// 双击适配集中于此;不修改上游,不拦截单击、拖动或空白区事件。
|
|
136
|
+
const open = (event) => {
|
|
137
|
+
if (cancelled || event.button !== 0) return
|
|
138
|
+
const node = event.target.closest?.('[data-node-id]')
|
|
139
|
+
if (!node || !svg.contains(node)) return
|
|
140
|
+
const id = node.getAttribute('data-node-id')
|
|
141
|
+
if (!id) return
|
|
142
|
+
onOpenDetail?.({ id, label: node.getAttribute('data-node-label') || id }, node)
|
|
143
|
+
}
|
|
144
|
+
if (onOpenDetail) {
|
|
145
|
+
svg.addEventListener('dblclick', open)
|
|
146
|
+
removeDoubleClick = () => svg.removeEventListener('dblclick', open)
|
|
147
|
+
}
|
|
148
|
+
observer = new MutationObserver(sync)
|
|
149
|
+
// 只看这一个标记:图内其它变化(缩放、悬停、播放)与选中无关,不惊动外层
|
|
150
|
+
observer.observe(svg, { subtree: true, attributes: true, attributeFilter: [MARKER] })
|
|
151
|
+
sync() // 初读一次:连接时图里可能已经有选中
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
stopWaiting = whenLoaded(frame, start, fail, timeoutMs)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
dispose() {
|
|
158
|
+
cancelled = true
|
|
159
|
+
observer?.disconnect()
|
|
160
|
+
observer = null
|
|
161
|
+
removeDoubleClick()
|
|
162
|
+
stopWaiting()
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// 三个页面共用的小工具:取数、错误与"repoRoot 未配置"指引渲染。
|
|
2
|
+
// 第二期第 2 步:链接带 ?workspace=<工作区标识> 时整条链路按该工作区读写——
|
|
3
|
+
// 本页标识在这里取一次,站内所有跳转与取数都经 wsUrl() 补上它(丢了就会串回手动模式的项目)。
|
|
4
|
+
export const $ = (id) => document.getElementById(id)
|
|
5
|
+
|
|
6
|
+
// null=链接里没有 workspace 参数(允许手动模式);''=有参数但为空(必须报错:
|
|
7
|
+
// 空参数若当"没有参数"处理,配置过 repoRoot 的页面会悄悄打开手动配置的项目)。
|
|
8
|
+
const workspaceParam = new URLSearchParams(location.search).get('workspace')
|
|
9
|
+
export const workspaceId = workspaceParam || ''
|
|
10
|
+
export const workspaceParamEmpty = workspaceParam === ''
|
|
11
|
+
|
|
12
|
+
/** 给站内 URL 补上本页的工作区标识;手动模式(没有标识)原样返回。 */
|
|
13
|
+
export function wsUrl(url) {
|
|
14
|
+
if (!workspaceId) return url
|
|
15
|
+
return `${url}${url.includes('?') ? '&' : '?'}workspace=${encodeURIComponent(workspaceId)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 面包屑里的"项目首页"是 HTML 里写死的裸地址,这里统一补上标识(首页自己没有面包屑,查不到就不动)。
|
|
19
|
+
// 阅读页的"业务"链接有业务段、且必须在取数前绑定(失败页也要带标识),由 read.js 自己设置。
|
|
20
|
+
document.querySelector('.crumbs a[href="/specdev-workbench/"]')?.setAttribute('href', wsUrl('/specdev-workbench/'))
|
|
21
|
+
document.querySelectorAll('[data-home-link]').forEach((link) => link.setAttribute('href', wsUrl('/specdev-workbench/')))
|
|
22
|
+
// 站内其它裸地址入口(如首页进展示页的按钮):写死 href 作无标识兜底,这里按本页标识补全。
|
|
23
|
+
document.querySelectorAll('[data-ws-link]').forEach((link) => link.setAttribute('href', wsUrl(link.getAttribute('data-ws-link'))))
|
|
24
|
+
|
|
25
|
+
export function setStatus(text, kind = 'info') {
|
|
26
|
+
const n = $('status')
|
|
27
|
+
if (!n) return
|
|
28
|
+
n.textContent = text
|
|
29
|
+
n.dataset.kind = kind
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function showError(message) {
|
|
33
|
+
setStatus('读取失败', 'bad')
|
|
34
|
+
const box = $('errorBox')
|
|
35
|
+
box.hidden = false
|
|
36
|
+
box.textContent = message
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 链接里的工作区标识是空的:如实报错并停住,不发任何请求(不会悄悄落回手动模式)。 */
|
|
40
|
+
export function renderEmptyWorkspaceParam() {
|
|
41
|
+
setStatus('链接不完整', 'warn')
|
|
42
|
+
const box = $('errorBox')
|
|
43
|
+
box.hidden = false
|
|
44
|
+
box.textContent = '链接里的工作区标识是空的(workspace= 后面没有编号)。请从 DSH 的工作区重新进入;手动复制链接时请复制完整编号。'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 把后端错误体上的 code 与 repo(工作区信息)挂到抛出的错误上,供状态页分支与显示。 */
|
|
48
|
+
function withBody(error, body) {
|
|
49
|
+
if (body && body.code && !error.code) error.code = body.code
|
|
50
|
+
if (body && body.repo && !error.repo) error.repo = body.repo
|
|
51
|
+
return error
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 统一取数:非 2xx 时抛出后端给出的中文错误(带 code 与 repo,供状态页分支与显示);
|
|
55
|
+
* repo 未配置交给调用方渲染指引。 */
|
|
56
|
+
export async function fetchJson(url) {
|
|
57
|
+
let body
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(url)
|
|
60
|
+
body = await response.json()
|
|
61
|
+
if (response.status === 400 && body.code === 'repo-not-configured') return body
|
|
62
|
+
if (!response.ok) throw withBody(new Error(body.error || `HTTP ${response.status}`), body)
|
|
63
|
+
return body
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (body && body.code === 'repo-not-configured') return body
|
|
66
|
+
throw withBody(error instanceof Error ? error : new Error(String(error)), body)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 带正文发 POST 取 JSON(证据清单走这里):把第一次响应里的清单原文原样交给服务端解析。 */
|
|
71
|
+
export async function postJson(url, text) {
|
|
72
|
+
let body
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
77
|
+
body: text,
|
|
78
|
+
})
|
|
79
|
+
body = await response.json()
|
|
80
|
+
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`)
|
|
81
|
+
return body
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(error.message || String(error))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 渲染"repoRoot 未配置"指引:插件正常加载、页面给可复制的配置示例。 */
|
|
88
|
+
export function renderGuide(payload) {
|
|
89
|
+
setStatus('尚未配置业务项目目录', 'warn')
|
|
90
|
+
const box = $('guideBox')
|
|
91
|
+
box.hidden = false
|
|
92
|
+
box.textContent = ''
|
|
93
|
+
const h = document.createElement('h2')
|
|
94
|
+
h.textContent = '还没有指定业务项目目录(repoRoot)'
|
|
95
|
+
box.appendChild(h)
|
|
96
|
+
const p = document.createElement('p')
|
|
97
|
+
p.textContent = payload.error
|
|
98
|
+
box.appendChild(p)
|
|
99
|
+
const where = document.createElement('p')
|
|
100
|
+
where.innerHTML = `<b>配置文件位置:</b>`
|
|
101
|
+
where.appendChild(document.createTextNode(payload.guide.where))
|
|
102
|
+
box.appendChild(where)
|
|
103
|
+
const pre = document.createElement('pre')
|
|
104
|
+
pre.textContent = payload.guide.example
|
|
105
|
+
box.appendChild(pre)
|
|
106
|
+
const effect = document.createElement('p')
|
|
107
|
+
effect.textContent = payload.guide.takesEffect
|
|
108
|
+
box.appendChild(effect)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function el(tag, className) {
|
|
112
|
+
const n = document.createElement(tag)
|
|
113
|
+
if (className) n.className = className
|
|
114
|
+
return n
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 状态汇总(首页业务卡小行与业务页汇总行共用)─────────────────────
|
|
118
|
+
|
|
119
|
+
/** 汇总顺序与文案:要处理的靠前;"异常"涵盖编号冲突与说明文件问题。 */
|
|
120
|
+
export const STATUS_SUMMARY_ORDER = [
|
|
121
|
+
['changed', '已改动'],
|
|
122
|
+
['invalid', '异常'],
|
|
123
|
+
['compare-failed', '无法比较'],
|
|
124
|
+
['no-snapshot', '无快照'],
|
|
125
|
+
['identical', '一致'],
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
/** 图的状态归类:编号冲突 / 说明文件问题算"异常",其余用服务端算好的四状态。 */
|
|
129
|
+
export function chartStatusKind(chart) {
|
|
130
|
+
if (chart.idConflict || chart.descriptorError) return 'invalid'
|
|
131
|
+
return chart.currentStatus
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 按状态清点图列表;只清点已知状态,未知值不伪造计数。 */
|
|
135
|
+
export function countChartStatuses(charts) {
|
|
136
|
+
const counts = {}
|
|
137
|
+
for (const chart of charts) {
|
|
138
|
+
const kind = chartStatusKind(chart)
|
|
139
|
+
if (STATUS_SUMMARY_ORDER.some(([value]) => value === kind)) counts[kind] = (counts[kind] || 0) + 1
|
|
140
|
+
}
|
|
141
|
+
return counts
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 只读状态小行:只列非零状态;没有可展示的状态返回 null(不放空行占位)。 */
|
|
145
|
+
export function statusChips(charts) {
|
|
146
|
+
const counts = countChartStatuses(charts)
|
|
147
|
+
const present = STATUS_SUMMARY_ORDER.filter(([kind]) => counts[kind])
|
|
148
|
+
if (present.length === 0) return null
|
|
149
|
+
const row = el('div', 'chip-row')
|
|
150
|
+
for (const [kind, label] of present) {
|
|
151
|
+
const chip = el('span', 'badge')
|
|
152
|
+
chip.dataset.kind = kind
|
|
153
|
+
chip.textContent = `${label} ${counts[kind]}`
|
|
154
|
+
row.append(chip)
|
|
155
|
+
}
|
|
156
|
+
return row
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 业务卡(首页与展示页列表视图共用):图标、序号、名称、介绍、状态小行、
|
|
160
|
+
* 底部图数与进入动作。说明文件读不开的业务不清点,异常说明如实显示。 */
|
|
161
|
+
export function businessCard(business, index) {
|
|
162
|
+
const card = el('a', 'card business-card')
|
|
163
|
+
card.href = wsUrl(`/specdev-workbench/business/${encodeURIComponent(business.id)}`)
|
|
164
|
+
const top = el('div', 'business-card-top')
|
|
165
|
+
const icon = el('span', 'business-icon')
|
|
166
|
+
icon.setAttribute('aria-hidden', 'true')
|
|
167
|
+
icon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="14" width="7" height="7" rx="2"/><path d="M10 6.5h5.5a2 2 0 0 1 2 2V14M6.5 10v5.5a2 2 0 0 0 2 2H14"/></svg>'
|
|
168
|
+
const number = el('span', 'business-number')
|
|
169
|
+
number.textContent = String(index + 1).padStart(2, '0')
|
|
170
|
+
top.append(icon, number)
|
|
171
|
+
const h = el('h3')
|
|
172
|
+
h.textContent = business.name
|
|
173
|
+
const p = el('p', 'business-intro')
|
|
174
|
+
p.textContent = business.intro || '进入业务,查看流程图与相关说明。'
|
|
175
|
+
card.append(top, h, p)
|
|
176
|
+
if (!business.descriptorError) {
|
|
177
|
+
const chips = statusChips(business.charts)
|
|
178
|
+
if (chips) card.append(chips)
|
|
179
|
+
}
|
|
180
|
+
const footer = el('div', 'business-card-footer')
|
|
181
|
+
const meta = el('span')
|
|
182
|
+
if (business.descriptorError) {
|
|
183
|
+
const warning = el('p', 'business-warning')
|
|
184
|
+
warning.textContent = `说明文件问题:${business.descriptorError}`
|
|
185
|
+
card.append(warning)
|
|
186
|
+
meta.textContent = '查看异常说明'
|
|
187
|
+
} else {
|
|
188
|
+
meta.textContent = `${business.charts.length} 张流程图`
|
|
189
|
+
}
|
|
190
|
+
const action = el('span', 'business-enter')
|
|
191
|
+
action.textContent = '进入业务 ↗'
|
|
192
|
+
footer.append(meta, action)
|
|
193
|
+
card.append(footer)
|
|
194
|
+
return card
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 当前状态徽标文案(§3.2:一致 / 已改动 / 无快照 / 无法比较,服务端算好传下来)。 */
|
|
198
|
+
export function statusBadge(status, title) {
|
|
199
|
+
const text = status === 'identical' ? '一致' : status === 'changed' ? '已改动'
|
|
200
|
+
: status === 'compare-failed' ? '无法比较' : '无快照'
|
|
201
|
+
const titleAttr = title ? ` title="${String(title).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')}"` : ''
|
|
202
|
+
return `<span class="badge" data-kind="${status}"${titleAttr}>${text}</span>`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── 第二期第 2 步:目录状态页与工作区行 ──────────────────────────────
|
|
206
|
+
|
|
207
|
+
/** 各状态的标题与补充说明:四状态分别说清,不能都显示成"暂无流程图"。 */
|
|
208
|
+
const REPO_STATE_COPY = {
|
|
209
|
+
'no-convention-root': {
|
|
210
|
+
title: '这里还没有流程图资料',
|
|
211
|
+
status: '还没有流程图资料',
|
|
212
|
+
note: '管理页只读不改:不会自动创建或修改这个目录里的任何文件。',
|
|
213
|
+
},
|
|
214
|
+
'bad-inventory': {
|
|
215
|
+
title: '流程图资料格式有问题',
|
|
216
|
+
status: '资料格式有问题',
|
|
217
|
+
note: '修好对应的说明文件后,刷新本页即可读取。',
|
|
218
|
+
},
|
|
219
|
+
'repo-unavailable': {
|
|
220
|
+
title: '目录读取不了',
|
|
221
|
+
status: '目录读取不了',
|
|
222
|
+
note: '工作区的目录可能被移动、删除或没有读取权限;从 DSH 的工作区重新进入试试。',
|
|
223
|
+
},
|
|
224
|
+
'not-a-git-repo': {
|
|
225
|
+
title: '这个目录不是 Git 仓库',
|
|
226
|
+
status: '不是 Git 仓库',
|
|
227
|
+
note: '历史版本与快照信息存放在 Git 里,目录需要是可用的 Git 仓库才能管理。',
|
|
228
|
+
},
|
|
229
|
+
'repo-not-top-level': {
|
|
230
|
+
title: '这个工作区不是 Git 仓库顶层',
|
|
231
|
+
status: '不是仓库顶层',
|
|
232
|
+
note: '目录在某个外层 Git 仓库里但不是仓库顶层,历史版本会与外层仓库混在一起;管理页只在仓库顶层工作,不会自动改绑。请把仓库顶层登记为工作区。',
|
|
233
|
+
},
|
|
234
|
+
'workspace-not-found': {
|
|
235
|
+
title: '工作区不存在或已失效',
|
|
236
|
+
status: '工作区已失效',
|
|
237
|
+
note: '可能该工作区已被删除,或这个链接来自另一个 DSH 实例;请从 DSH 的工作区重新进入。',
|
|
238
|
+
},
|
|
239
|
+
'workspace-service-unavailable': {
|
|
240
|
+
title: '当前 DSH 没有提供工作区服务',
|
|
241
|
+
status: '工作区服务不可用',
|
|
242
|
+
note: '可能是 DSH 版本变化。请从 DSH 的工作区重新进入;或改用手动配置模式(填 repoRoot,去掉链接里的 workspace 参数)。',
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 工作区信息一行字:工作区模式"工作区:名称(路径)",手动模式"手动配置模式:路径"。 */
|
|
247
|
+
export function repoLineText(repo) {
|
|
248
|
+
if (!repo) return ''
|
|
249
|
+
return repo.mode === 'workspace'
|
|
250
|
+
? `工作区:${repo.title}(${repo.path})`
|
|
251
|
+
: `手动配置模式:${repo.path}`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 按 error.code 渲染目录状态页(复用指引框的样式);不属于已知状态的错误返回 false,
|
|
256
|
+
* 调用方继续走普通报错。三个页面共用,保证同一状态在各页说法一致。
|
|
257
|
+
* 错误响应带 repo 块:空状态/错误页也显示工作区名称、
|
|
258
|
+
* 路径,并把标签标题带上工作区名——打开两个空项目时也能区分标签页。
|
|
259
|
+
*/
|
|
260
|
+
export function renderRepoState(error) {
|
|
261
|
+
const copy = REPO_STATE_COPY[error && error.code]
|
|
262
|
+
if (!copy) return false
|
|
263
|
+
setStatus(copy.status, 'warn')
|
|
264
|
+
const box = $('guideBox')
|
|
265
|
+
box.hidden = false
|
|
266
|
+
box.textContent = ''
|
|
267
|
+
const h = document.createElement('h2')
|
|
268
|
+
h.textContent = copy.title
|
|
269
|
+
box.appendChild(h)
|
|
270
|
+
if (error.repo) {
|
|
271
|
+
document.title = pageTitle(copy.title, error.repo)
|
|
272
|
+
const w = document.createElement('p')
|
|
273
|
+
w.textContent = repoLineText(error.repo)
|
|
274
|
+
box.appendChild(w)
|
|
275
|
+
}
|
|
276
|
+
if (error.message) {
|
|
277
|
+
const p = document.createElement('p')
|
|
278
|
+
p.textContent = error.message
|
|
279
|
+
box.appendChild(p)
|
|
280
|
+
}
|
|
281
|
+
const note = document.createElement('p')
|
|
282
|
+
note.textContent = copy.note
|
|
283
|
+
box.appendChild(note)
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** 标签页标题:工作区模式带工作区名(两个标签好区分),手动模式保持原有后缀。 */
|
|
288
|
+
export function pageTitle(main, repo) {
|
|
289
|
+
return repo && repo.mode === 'workspace' ? `${main} · ${repo.title}` : `${main} · SpecDev 工作台`
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 头部工作区行:工作区模式显示"工作区:名称(路径)",手动模式标注"手动配置模式:路径"。 */
|
|
293
|
+
export function renderRepoLine(repo) {
|
|
294
|
+
const line = $('repoLine')
|
|
295
|
+
if (!line || !repo) return
|
|
296
|
+
line.textContent = repoLineText(repo)
|
|
297
|
+
const details = $('workspaceDetails')
|
|
298
|
+
if (details) {
|
|
299
|
+
details.hidden = false
|
|
300
|
+
$('workspaceLabel').textContent = repo.mode === 'workspace' ? `工作区 · ${repo.title}` : '手动配置的项目目录'
|
|
301
|
+
}
|
|
302
|
+
}
|