@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,302 @@
|
|
|
1
|
+
// 家具图鉴页(E9b):经真实 loadFurnitureCatalog 读取随包共用家具库,展示真实缩略图、
|
|
2
|
+
// 名称、尺寸、风格、放置方式与互动;页内勾选生成「名称+assetId」清单,供用户复制回
|
|
3
|
+
// 聊天交 AI 布置。纯看图挑选页:不加载模型(loader 只用于确认引用)、不按业务过滤、
|
|
4
|
+
// 不请求 inventory / /api/room、不写业务数据;选择只存本页内存,刷新即清空。
|
|
5
|
+
// 所有外部名称、描述、错误一律 textContent / DOM 安全赋值,不拼 HTML。
|
|
6
|
+
import { loadFurnitureCatalog } from './furniture/catalog.js'
|
|
7
|
+
import { $, fetchJson, renderEmptyWorkspaceParam, setStatus, workspaceParamEmpty } from './common.js'
|
|
8
|
+
|
|
9
|
+
const INDEX_URL = '/specdev-workbench/assets/furniture/index.json'
|
|
10
|
+
const ASSET_ROOT = '/specdev-workbench/assets/furniture/'
|
|
11
|
+
|
|
12
|
+
// 风格标记固定映射;未知值按原文字展示,不伪装成已知风格。
|
|
13
|
+
const STYLE_LABELS = { fairy: '童话', ceramic: '陶瓷', realistic: '写实' }
|
|
14
|
+
// 放置方式转人话;详细限制以元数据描述为准,这里不复制 placement 算法。
|
|
15
|
+
const KIND_LABELS = { floor: '落地摆放', surface: '放桌面/承载面' }
|
|
16
|
+
// 互动能力固定映射;没有的能力不伪造入口。
|
|
17
|
+
const CAPABILITY_LABELS = { 'open-document-collection': '可查看文档与流程图' }
|
|
18
|
+
|
|
19
|
+
const sizeText = (asset) => {
|
|
20
|
+
const b = asset.bounds
|
|
21
|
+
if (!b || typeof b !== 'object') return '尺寸信息不完整'
|
|
22
|
+
// 先检查原始轴值,避免减法把 null、布尔值或数字字符串悄悄转换成有效尺寸。
|
|
23
|
+
if (![b.minX, b.maxX, b.minZ, b.maxZ, b.minY, b.maxY].every(Number.isFinite)) return '尺寸信息不完整'
|
|
24
|
+
const width = b.maxX - b.minX
|
|
25
|
+
const depth = b.maxZ - b.minZ
|
|
26
|
+
const height = b.maxY - b.minY // Y 是高度轴;深度取 Z,不把 Y 当深度
|
|
27
|
+
if (![width, depth, height].every(Number.isFinite)) return '尺寸信息不完整'
|
|
28
|
+
return `宽×深×高:${width.toFixed(2)} × ${depth.toFixed(2)} × ${height.toFixed(2)} 米`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const kindText = (asset) => {
|
|
32
|
+
const kinds = Array.isArray(asset.placement?.allowedKinds) ? asset.placement.allowedKinds : []
|
|
33
|
+
const labels = kinds.map((kind) => KIND_LABELS[kind] ?? String(kind))
|
|
34
|
+
return labels.length > 0 ? labels.join('或') : '放置方式见详情'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const capabilityText = (asset) => {
|
|
38
|
+
const caps = Array.isArray(asset.capabilities) ? asset.capabilities : []
|
|
39
|
+
const labels = caps.map((cap) => CAPABILITY_LABELS[cap] ?? String(cap))
|
|
40
|
+
return labels.length > 0 ? labels.join(';') : '无页内动作'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const el = (tag, className) => {
|
|
44
|
+
const node = document.createElement(tag)
|
|
45
|
+
if (className) node.className = className
|
|
46
|
+
return node
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 选择集:assetId → 名称,清单输出按目录顺序稳定排列;不落任何存储。 ──────────
|
|
50
|
+
const selection = new Map()
|
|
51
|
+
let catalogOrder = [] // 目录里的 assetId 顺序(加载成功后填充),清单按它排序
|
|
52
|
+
let selectionVersion = 0 // 每次变化自增:晚到的复制结果不能误称当前清单已复制
|
|
53
|
+
let copyNoteTimer = 0
|
|
54
|
+
|
|
55
|
+
const listText = () => {
|
|
56
|
+
const lines = []
|
|
57
|
+
for (const assetId of catalogOrder) {
|
|
58
|
+
if (!selection.has(assetId)) continue
|
|
59
|
+
lines.push(`- ${selection.get(assetId)}(assetId:${assetId})`)
|
|
60
|
+
}
|
|
61
|
+
return [
|
|
62
|
+
'【家具图鉴挑选清单】',
|
|
63
|
+
...lines,
|
|
64
|
+
'默认每款一件。请结合已说明的风格和用途布置;数量或其他要求可以补充;放不下或承载冲突时先说明。',
|
|
65
|
+
].join('\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const refreshPicker = () => {
|
|
69
|
+
$('pickCount').textContent = `已选 ${selection.size} 款`
|
|
70
|
+
const empty = selection.size === 0
|
|
71
|
+
$('copyButton').disabled = empty
|
|
72
|
+
$('clearButton').disabled = empty
|
|
73
|
+
$('pickList').value = empty ? '' : listText()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const showCopyNote = (text, kind) => {
|
|
77
|
+
const note = $('copyNote')
|
|
78
|
+
note.textContent = text
|
|
79
|
+
note.dataset.kind = kind
|
|
80
|
+
note.hidden = false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const onPickChange = () => {
|
|
84
|
+
selectionVersion += 1
|
|
85
|
+
$('copyNote').hidden = true // 选择变化后旧的「已复制」提示失效
|
|
86
|
+
refreshPicker()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── 卡片 ──────────────────────────────────────────────────────────────────────
|
|
90
|
+
/** 缩略图:previewRef 为 null 直接显示暂无图片;加载失败一次性降级,不反复请求。 */
|
|
91
|
+
const mountThumb = (box, asset) => {
|
|
92
|
+
if (typeof asset.previewRef !== 'string' || asset.previewRef === '') {
|
|
93
|
+
mountNoPreview(box, asset)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
const img = el('img')
|
|
97
|
+
img.alt = `${asset.name} 缩略图`
|
|
98
|
+
img.loading = 'lazy'
|
|
99
|
+
img.src = ASSET_ROOT + asset.previewRef // 固定包前缀+目录校验过的 previewRef,不接外链
|
|
100
|
+
img.addEventListener('error', () => mountNoPreview(box, asset), { once: true })
|
|
101
|
+
box.appendChild(img)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const mountNoPreview = (box, asset) => {
|
|
105
|
+
box.textContent = ''
|
|
106
|
+
const p = el('p', 'no-preview')
|
|
107
|
+
p.textContent = `暂无图片(${asset.name} 的名称与选择功能不受影响)`
|
|
108
|
+
box.appendChild(p)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const mountCard = (asset) => {
|
|
112
|
+
const card = el('article', 'furniture-card')
|
|
113
|
+
const pick = el('label', 'furniture-pick')
|
|
114
|
+
const box = document.createElement('input')
|
|
115
|
+
box.type = 'checkbox'
|
|
116
|
+
box.checked = selection.has(asset.assetId)
|
|
117
|
+
box.addEventListener('change', () => {
|
|
118
|
+
if (box.checked) selection.set(asset.assetId, asset.name)
|
|
119
|
+
else selection.delete(asset.assetId)
|
|
120
|
+
onPickChange()
|
|
121
|
+
})
|
|
122
|
+
const name = el('span', 'furniture-name')
|
|
123
|
+
name.textContent = asset.name
|
|
124
|
+
pick.append(box, name)
|
|
125
|
+
|
|
126
|
+
const thumb = el('div', 'furniture-thumb')
|
|
127
|
+
mountThumb(thumb, asset)
|
|
128
|
+
|
|
129
|
+
const size = el('p', 'furniture-size')
|
|
130
|
+
size.textContent = sizeText(asset)
|
|
131
|
+
const styleLine = el('p', 'furniture-meta')
|
|
132
|
+
const styleNames = (Array.isArray(asset.styleIds) ? asset.styleIds : [])
|
|
133
|
+
.map((id) => STYLE_LABELS[id] ?? String(id)).join('、')
|
|
134
|
+
styleLine.textContent = `风格:${styleNames || '未标注'}(风格可混搭)`
|
|
135
|
+
const kindLine = el('p', 'furniture-meta')
|
|
136
|
+
kindLine.textContent = `放置:${kindText(asset)}`
|
|
137
|
+
const capLine = el('p', 'furniture-meta')
|
|
138
|
+
capLine.textContent = `互动:${capabilityText(asset)}`
|
|
139
|
+
const idLine = el('p', 'asset-id')
|
|
140
|
+
idLine.textContent = `assetId:${asset.assetId}`
|
|
141
|
+
|
|
142
|
+
const details = el('details')
|
|
143
|
+
const summary = el('summary')
|
|
144
|
+
summary.textContent = '用途与详情'
|
|
145
|
+
const description = el('p')
|
|
146
|
+
description.textContent = asset.description
|
|
147
|
+
const recommend = el('p')
|
|
148
|
+
recommend.textContent = asset.recommendations?.note ?? ''
|
|
149
|
+
details.append(summary, description)
|
|
150
|
+
if (recommend.textContent !== '') details.append(recommend)
|
|
151
|
+
|
|
152
|
+
card.append(pick, thumb, size, styleLine, kindLine, capLine, idLine, details)
|
|
153
|
+
return card
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── 目录加载与渲染 ────────────────────────────────────────────────────────────
|
|
157
|
+
// run 序号:离页(含往返缓存离开)后晚到的目录结果不再改 DOM;bfcache 返回若还停在
|
|
158
|
+
// 加载态则用新序号重读一次,不永久卡在「正在加载」。
|
|
159
|
+
let runSerial = 0
|
|
160
|
+
let finished = false
|
|
161
|
+
let pageGone = false // 离页后晚到的复制回调也不再改 DOM(返回时复位)
|
|
162
|
+
|
|
163
|
+
const renderCatalog = (catalog, categories) => {
|
|
164
|
+
const main = $('catalog')
|
|
165
|
+
main.textContent = ''
|
|
166
|
+
if (catalog.assets.length === 0) {
|
|
167
|
+
const empty = el('p', 'furniture-empty')
|
|
168
|
+
empty.textContent = '目录里还没有登记家具。'
|
|
169
|
+
main.appendChild(empty)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
// 分类映射来自同一次 loader 验证成功的索引:叶类型 → 分类名;映射不全时整页退回单一列表。
|
|
173
|
+
const leafToName = new Map()
|
|
174
|
+
for (const category of categories ?? []) {
|
|
175
|
+
for (const type of Array.isArray(category.types) ? category.types : []) leafToName.set(type, category.name)
|
|
176
|
+
}
|
|
177
|
+
const groups = new Map() // 分类名 → 资产数组;组顺序按索引 categories 声明顺序
|
|
178
|
+
const categoryOrder = []
|
|
179
|
+
for (const category of categories ?? []) {
|
|
180
|
+
if (isNonEmpty(category?.name)) categoryOrder.push(category.name)
|
|
181
|
+
}
|
|
182
|
+
const groupOrder = []
|
|
183
|
+
const groupOf = (name) => {
|
|
184
|
+
if (!groups.has(name)) { groups.set(name, []); groupOrder.push(name) }
|
|
185
|
+
return groups.get(name)
|
|
186
|
+
}
|
|
187
|
+
for (const name of categoryOrder) groupOf(name) // 先按声明顺序立组(空组不渲染)
|
|
188
|
+
let fallbackUsed = false
|
|
189
|
+
for (const asset of catalog.assets) {
|
|
190
|
+
const groupName = leafToName.get(asset.category)
|
|
191
|
+
if (groupName === undefined) { groupOf('全部家具'); fallbackUsed = true }
|
|
192
|
+
else groups.get(groupName).push(asset)
|
|
193
|
+
}
|
|
194
|
+
const toRender = groupOrder.filter((name) => (groups.get(name) ?? []).length > 0)
|
|
195
|
+
if (toRender.length === 1 && toRender[0] === '全部家具') {
|
|
196
|
+
const grid = el('div', 'furniture-cards')
|
|
197
|
+
for (const asset of catalog.assets) grid.appendChild(mountCard(asset))
|
|
198
|
+
main.appendChild(grid)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
for (const name of toRender) {
|
|
202
|
+
const group = el('section', 'furniture-group')
|
|
203
|
+
const h = el('h2')
|
|
204
|
+
h.textContent = name
|
|
205
|
+
const note = el('p', 'group-note')
|
|
206
|
+
note.textContent = fallbackUsed && name === '全部家具'
|
|
207
|
+
? '这些款式没有归入分类。勾选想要的款式,稍后在页面顶部复制清单。'
|
|
208
|
+
: '勾选想要的款式,稍后在页面顶部复制清单。'
|
|
209
|
+
const grid = el('div', 'furniture-cards')
|
|
210
|
+
for (const asset of groups.get(name)) grid.appendChild(mountCard(asset))
|
|
211
|
+
group.append(h, note, grid)
|
|
212
|
+
main.appendChild(group)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const isNonEmpty = (value) => typeof value === 'string' && value !== ''
|
|
217
|
+
|
|
218
|
+
const main = async () => {
|
|
219
|
+
const run = ++runSerial
|
|
220
|
+
finished = false
|
|
221
|
+
let loadedIndex = null
|
|
222
|
+
// 轻量包装:目录读取仍走真实 fetchJson 与 loader 校验,这里只额外记下读到的索引,
|
|
223
|
+
// 供分类映射使用;不改 catalog 的返回契约,也不另建分类目录。
|
|
224
|
+
const tracingFetch = async (url) => {
|
|
225
|
+
const body = await fetchJson(url)
|
|
226
|
+
if (url === INDEX_URL && body && typeof body === 'object' && !Array.isArray(body)) loadedIndex = body
|
|
227
|
+
return body
|
|
228
|
+
}
|
|
229
|
+
let result
|
|
230
|
+
try {
|
|
231
|
+
result = await loadFurnitureCatalog({ fetchJson: tracingFetch })
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (run !== runSerial) return // 已离页或已被新一轮取代,晚到结果不碰 DOM
|
|
234
|
+
setStatus('读取失败', 'bad')
|
|
235
|
+
$('errorBox').hidden = false
|
|
236
|
+
$('errorBox').textContent = `读取家具目录时出现未预料的错误:${error instanceof Error ? error.message : String(error)}`
|
|
237
|
+
finished = true
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (run !== runSerial) return
|
|
241
|
+
finished = true
|
|
242
|
+
if (!result.ok) {
|
|
243
|
+
setStatus('目录读取失败', 'bad')
|
|
244
|
+
$('errorBox').hidden = false
|
|
245
|
+
$('errorBox').textContent = result.diagnostics.map((d) => d.message).join('\n')
|
|
246
|
+
$('catalog').textContent = '' // 坏目录不残留半份列表
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
setStatus(`已加载 ${result.catalog.assets.length} 款家具`, 'ok')
|
|
250
|
+
catalogOrder = result.catalog.assets.map((asset) => asset.assetId)
|
|
251
|
+
renderCatalog(result.catalog, Array.isArray(loadedIndex?.categories) ? loadedIndex.categories : null)
|
|
252
|
+
refreshPicker()
|
|
253
|
+
$('picker').hidden = false
|
|
254
|
+
$('pickListBox').hidden = false
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── 复制与清空 ────────────────────────────────────────────────────────────────
|
|
258
|
+
$('clearButton').addEventListener('click', () => {
|
|
259
|
+
selection.clear()
|
|
260
|
+
for (const input of document.querySelectorAll('.furniture-pick input')) input.checked = false
|
|
261
|
+
onPickChange()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
$('copyButton').addEventListener('click', () => {
|
|
265
|
+
const text = listText()
|
|
266
|
+
const version = selectionVersion
|
|
267
|
+
const note = $('copyNote')
|
|
268
|
+
note.hidden = true
|
|
269
|
+
const myTimer = ++copyNoteTimer
|
|
270
|
+
const late = () => myTimer !== copyNoteTimer // 期间又点了复制:旧结果不再覆盖新结果
|
|
271
|
+
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
|
272
|
+
showCopyNote('当前环境没有可用的复制接口,请在下方文本框全选后手动复制。', 'bad')
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
navigator.clipboard.writeText(text).then(
|
|
276
|
+
() => {
|
|
277
|
+
if (pageGone || late()) return // 离页后的晚到提示不再写入 DOM
|
|
278
|
+
if (version !== selectionVersion) {
|
|
279
|
+
showCopyNote('清单在复制后发生了变化,刚复制的内容不是当前清单,请重新复制。', 'bad')
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
showCopyNote('已复制。把清单粘贴到聊天发给 AI,即可按选择布置房间。', 'ok')
|
|
283
|
+
},
|
|
284
|
+
() => {
|
|
285
|
+
if (pageGone || late()) return
|
|
286
|
+
showCopyNote('复制被浏览器拒绝,请在下方文本框全选后手动复制。', 'bad')
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
window.addEventListener('pagehide', () => { runSerial += 1; pageGone = true }) // 离页后在途结果(含复制回调)全部失效
|
|
292
|
+
window.addEventListener('pageshow', (event) => {
|
|
293
|
+
pageGone = false
|
|
294
|
+
// 往返缓存返回:若此前停在加载态(未完成),重新读取;已完成则原样保留。
|
|
295
|
+
if (event.persisted && !finished) void main()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
if (workspaceParamEmpty) {
|
|
299
|
+
renderEmptyWorkspaceParam()
|
|
300
|
+
} else {
|
|
301
|
+
void main()
|
|
302
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// 首页只从现有清单汇总项目与业务,不推测代码活动或图与代码的同步情况。
|
|
2
|
+
import { $, businessCard, el, fetchJson, pageTitle, renderEmptyWorkspaceParam, renderGuide, renderRepoLine, renderRepoState, setStatus, showError, workspaceParamEmpty, wsUrl } from './common.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 首页最醒目的那一下:进建筑。只有这个项目**真有建筑**才出现——探测一次 /api/building:
|
|
6
|
+
* 读到了(模型或诊断列表都算)就亮出来;「还没有建筑蓝图」或这一下没读到,入口就不出现,
|
|
7
|
+
* 不给人一个点进去只看得到空态的按钮。只读、不改任何东西;探测失败不影响首页其余部分。
|
|
8
|
+
*/
|
|
9
|
+
async function revealBuildingLink() {
|
|
10
|
+
const link = $('buildingLink')
|
|
11
|
+
try {
|
|
12
|
+
await fetchJson(wsUrl('/specdev-workbench/api/building'))
|
|
13
|
+
link.hidden = false
|
|
14
|
+
} catch {
|
|
15
|
+
link.hidden = true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const root = $('root')
|
|
21
|
+
try {
|
|
22
|
+
if (workspaceParamEmpty) return renderEmptyWorkspaceParam()
|
|
23
|
+
const inventory = await fetchJson(wsUrl('/specdev-workbench/api/inventory'))
|
|
24
|
+
if (inventory.code === 'repo-not-configured') return renderGuide(inventory)
|
|
25
|
+
|
|
26
|
+
document.title = pageTitle(inventory.project.name, inventory.repo)
|
|
27
|
+
$('projectName').textContent = inventory.project.name
|
|
28
|
+
$('projectDesc').textContent = inventory.project.description || '从业务出发,阅读流程、理解实现,回看每一个版本。'
|
|
29
|
+
renderRepoLine(inventory.repo)
|
|
30
|
+
|
|
31
|
+
const businesses = inventory.businesses
|
|
32
|
+
const chartCount = businesses.reduce((count, business) => count + business.charts.length, 0)
|
|
33
|
+
$('businessCount').textContent = businesses.length
|
|
34
|
+
$('chartCount').textContent = chartCount
|
|
35
|
+
$('chartCountNote').textContent = businesses.some((business) => business.descriptorError) ? '张图(不含读取异常的业务)' : '张登记的图'
|
|
36
|
+
$('projectMetrics').hidden = false
|
|
37
|
+
$('heroActions').hidden = businesses.length === 0
|
|
38
|
+
$('showcaseLink').hidden = businesses.length === 0
|
|
39
|
+
// 没有业务就没有建筑(蓝图必须逐间绑业务),这种项目连探测都不发。
|
|
40
|
+
if (businesses.length > 0) void revealBuildingLink()
|
|
41
|
+
root.replaceChildren()
|
|
42
|
+
|
|
43
|
+
for (const [index, business] of businesses.entries()) {
|
|
44
|
+
root.append(businessCard(business, index))
|
|
45
|
+
}
|
|
46
|
+
if (businesses.length === 0) {
|
|
47
|
+
const empty = el('div', 'empty-state')
|
|
48
|
+
const h = el('h3')
|
|
49
|
+
h.textContent = '项目已就绪,等待第一项业务'
|
|
50
|
+
const p = el('p')
|
|
51
|
+
p.textContent = '当前项目清单中还没有业务。添加业务资料后,刷新即可在这里查看。'
|
|
52
|
+
empty.append(h, p)
|
|
53
|
+
root.append(empty)
|
|
54
|
+
}
|
|
55
|
+
setStatus(`共 ${businesses.length} 个业务`, 'ok')
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.repo) renderRepoLine(error.repo)
|
|
58
|
+
if (!renderRepoState(error)) showError(error.message)
|
|
59
|
+
} finally {
|
|
60
|
+
root.querySelector('.loading-copy')?.remove()
|
|
61
|
+
root.setAttribute('aria-busy', 'false')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
main()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// 节点源码对照:只认文案中显式的“(证据 编号)”,不按相似名称推断关联。
|
|
2
|
+
import { findSection, splitInline } from './details.js'
|
|
3
|
+
import { describeSource, displayError, formatCodeBlock } from './evidence.js'
|
|
4
|
+
|
|
5
|
+
const make = (tag, cls, text) => {
|
|
6
|
+
const node = document.createElement(tag)
|
|
7
|
+
if (cls) node.className = cls
|
|
8
|
+
if (text !== undefined) node.textContent = text
|
|
9
|
+
return node
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 围栏内的示例不作为证据声明,也不把示例中的标题拆成业务步骤。
|
|
13
|
+
export function detailGroups(body) {
|
|
14
|
+
const groups = []
|
|
15
|
+
let lines = [], title = '', fence = null
|
|
16
|
+
const flush = () => {
|
|
17
|
+
if (!title && !lines.join('').trim()) return
|
|
18
|
+
const text = lines.join('\n').trim()
|
|
19
|
+
let inFence = null
|
|
20
|
+
const declarations = text.split('\n').filter(line => {
|
|
21
|
+
const mark = /^\s*(`{3,}|~{3,})/.exec(line)?.[1]
|
|
22
|
+
if (mark) {
|
|
23
|
+
if (!inFence) inFence = mark
|
|
24
|
+
else if (mark[0] === inFence[0] && mark.length >= inFence.length) inFence = null
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return !inFence
|
|
28
|
+
}).join('\n')
|
|
29
|
+
const ids = [...declarations.matchAll(/[((]证据\s+([^))\n]+)[))]/g)]
|
|
30
|
+
.flatMap(match => match[1].split(/[、,,;;\s]+/)).filter(Boolean)
|
|
31
|
+
groups.push({ title, body: text, ids: [...new Set(ids)] })
|
|
32
|
+
lines = []
|
|
33
|
+
}
|
|
34
|
+
for (const line of String(body || '').split(/\r?\n/)) {
|
|
35
|
+
const mark = /^\s*(`{3,}|~{3,})/.exec(line)?.[1]
|
|
36
|
+
if (fence) {
|
|
37
|
+
lines.push(line)
|
|
38
|
+
if (mark && mark[0] === fence[0] && mark.length >= fence.length && !line.trim().slice(mark.length).trim()) fence = null
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (mark) { fence = mark; lines.push(line); continue }
|
|
42
|
+
const heading = /^###\s+(.+?)\s*#*\s*$/.exec(line)
|
|
43
|
+
if (heading) { flush(); title = heading[1]; continue }
|
|
44
|
+
// 没有子标题的旧详情按段落对照;有标题则保留整个步骤为一组。
|
|
45
|
+
if (!title && !line.trim()) { flush(); continue }
|
|
46
|
+
lines.push(line)
|
|
47
|
+
}
|
|
48
|
+
flush()
|
|
49
|
+
return groups
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inline(host, text) {
|
|
53
|
+
for (const part of splitInline(text)) {
|
|
54
|
+
host.append(part.code || part.strong ? make(part.code ? 'code' : 'strong', '', part.text) : document.createTextNode(part.text))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function prose(host, text) {
|
|
59
|
+
let fence = null, code = [], list = null
|
|
60
|
+
const flushCode = () => { host.append(make('pre', 'detail-example', code.join('\n'))); code = [] }
|
|
61
|
+
for (const line of text.split('\n')) {
|
|
62
|
+
const mark = /^\s*(`{3,}|~{3,})/.exec(line)?.[1]
|
|
63
|
+
if (fence) {
|
|
64
|
+
if (mark && mark[0] === fence[0] && mark.length >= fence.length && !line.trim().slice(mark.length).trim()) { fence = null; flushCode() }
|
|
65
|
+
else code.push(line)
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (mark) { fence = mark; list = null; continue }
|
|
69
|
+
if (!line.trim()) { list = null; continue }
|
|
70
|
+
const bullet = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/.exec(line)
|
|
71
|
+
const heading = /^#{3,6}\s+(.*)$/.exec(line)
|
|
72
|
+
const node = make(bullet ? 'li' : heading ? 'h4' : 'p')
|
|
73
|
+
inline(node, bullet?.[1] || heading?.[1] || line)
|
|
74
|
+
if (bullet) {
|
|
75
|
+
if (!list) { list = make('ul'); host.append(list) }
|
|
76
|
+
list.append(node)
|
|
77
|
+
} else { list = null; host.append(node) }
|
|
78
|
+
}
|
|
79
|
+
if (fence) flushCode()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function loadDetailEvidence(version, request) {
|
|
83
|
+
if (version.evidenceError) return { refs: [], message: `证据文件读不开:${version.evidenceError}` }
|
|
84
|
+
if (version.files.evidence === null) return { refs: [], message: '这个版本没有证据文件。' }
|
|
85
|
+
try {
|
|
86
|
+
const data = await request(version.files.evidence)
|
|
87
|
+
return { refs: data.refs || [], message: data.parseError ? `证据文件有问题:${data.parseError}` : data.missing ? '这个版本没有证据文件。' : '' }
|
|
88
|
+
} catch (error) { return { refs: [], message: `证据读取失败:${error.message}` } }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderNodeDetail(host, version, details, selection, evidence) {
|
|
92
|
+
host.replaceChildren()
|
|
93
|
+
const found = findSection(details, selection.id, { unreadable: version.detailsError })
|
|
94
|
+
host.append(make('h2', 'reader-title', selection.label), make('p', 'detail-source', `${selection.id} · ${version.kind === 'snapshot' ? version.label || version.tag : '当前工作区'}`))
|
|
95
|
+
if (found.kind !== 'section') {
|
|
96
|
+
const reason = found.kind === 'unreadable' ? `说明文档读不开:${found.reason}`
|
|
97
|
+
: found.kind === 'malformed' ? '说明文档没有按“## 节点编号”分节,暂时无法定位此节点。'
|
|
98
|
+
: found.kind === 'empty' ? '这个版本没有说明文档。' : '这个节点还没有说明。'
|
|
99
|
+
host.append(make('p', 'detail-miss', reason))
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (found.repeats > 1) host.append(make('p', 'details-warn', `此编号有 ${found.repeats} 节说明,当前显示第一份。`))
|
|
103
|
+
if (found.block.note) host.append(make('p', 'muted', found.block.note))
|
|
104
|
+
const labels = make('div', 'reader-columns')
|
|
105
|
+
labels.append(make('span', '', '源码依据'), make('span', '', '业务说明'))
|
|
106
|
+
host.append(labels)
|
|
107
|
+
const groups = detailGroups(found.block.body)
|
|
108
|
+
if (!groups.length) host.append(make('p', 'detail-miss', '这一节还没有正文。'))
|
|
109
|
+
for (const group of groups) {
|
|
110
|
+
const row = make('section', 'reader-pair')
|
|
111
|
+
const left = make('div', 'reader-code')
|
|
112
|
+
const link = make('span', 'reader-link')
|
|
113
|
+
link.setAttribute('aria-hidden', 'true')
|
|
114
|
+
const right = make('div', 'reader-prose')
|
|
115
|
+
if (group.title) right.append(make('h3', '', group.title))
|
|
116
|
+
prose(right, group.body)
|
|
117
|
+
if (!group.ids.length) {
|
|
118
|
+
left.append(make('p', 'muted', '此段未关联源码证据'))
|
|
119
|
+
row.classList.add('reader-unpaired')
|
|
120
|
+
} else if (evidence.loading || evidence.message) {
|
|
121
|
+
left.append(make('p', 'ev-warn', evidence.loading ? '正在读取源码证据…' : evidence.message))
|
|
122
|
+
} else {
|
|
123
|
+
for (const id of group.ids) {
|
|
124
|
+
const refs = evidence.refs.filter(ref => ref.id === id)
|
|
125
|
+
if (refs.length !== 1) {
|
|
126
|
+
left.append(make('p', 'ev-bad', refs.length ? `证据编号重复:${id},无法确定对应源码。` : `找不到引用:${id}`))
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
const ref = refs[0]
|
|
130
|
+
const box = make('figure', 'reader-snippet')
|
|
131
|
+
box.append(make('figcaption', '', ref.label || id))
|
|
132
|
+
if (!ref.ok) box.append(make('p', 'ev-bad', displayError(ref.error)))
|
|
133
|
+
else {
|
|
134
|
+
const source = make('p', 'ev-src', describeSource(ref))
|
|
135
|
+
source.title = `完整提交:${ref.commit}`
|
|
136
|
+
const code = make('pre', 'ev-code', formatCodeBlock(ref.text, ref.fromLine))
|
|
137
|
+
code.tabIndex = 0
|
|
138
|
+
code.setAttribute('aria-label', `源码 ${ref.path}`)
|
|
139
|
+
box.append(source, code)
|
|
140
|
+
}
|
|
141
|
+
left.append(box)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
row.append(left, link, right)
|
|
145
|
+
host.append(row)
|
|
146
|
+
}
|
|
147
|
+
host.append(make('p', 'detail-source', '仅按文案中明确注明的证据编号配对;未关联资料可从“全部阅读资料”查看。源码取自引用写定的提交。'))
|
|
148
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// 资料面板与它里面那份阅读页之间的两句话(同源 postMessage)。为什么需要它:
|
|
2
|
+
// 流程图装在 iframe 里,用户点进阅读页后焦点进了子文档,父文档收不到键盘事件——
|
|
3
|
+
// 面板上的 ESC 就"失灵"了。所以由阅读页在自己那侧接住 ESC,把"关掉资料面板"
|
|
4
|
+
// 转给宿主页;顶层标签页里打开阅读页时什么都不做(没有面板可关)。
|
|
5
|
+
// 另外:iframe 导航失败不会触发 error 事件,别指望用它报加载失败(见 MDN iframe 说明);
|
|
6
|
+
// 宿主页在设置 src 之前先自己取一次入口,那里才是可靠的门。
|
|
7
|
+
|
|
8
|
+
/** 面板与阅读页约定的那一句话(只此一条,不带任何数据)。 */
|
|
9
|
+
export const MATERIAL_CLOSE_REQUEST = 'archify-material-close'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 阅读页侧:嵌在资料面板里时,把"用户想关掉面板"的 ESC 转给宿主页。
|
|
13
|
+
* 阅读页自己的弹层(节点详情/全部资料/保存版本)开着时不动手——那时 ESC 是关弹层的,
|
|
14
|
+
* 交给它们自己的原生 dialog 行为;只有没有弹层时才算"想退出阅读"。
|
|
15
|
+
* 用捕获阶段接:转发只是捎个话,不 preventDefault,但别被别的 document 级监听挡在前面。
|
|
16
|
+
* 同一个窗口可以接多份文档(阅读页自己的、以及它里面那张图的那份)。
|
|
17
|
+
* 返回解绑函数;顶层窗口(parent === window)直接返回空函数。
|
|
18
|
+
*/
|
|
19
|
+
export function forwardEscapeToHost(win, doc, { type = MATERIAL_CLOSE_REQUEST } = {}) {
|
|
20
|
+
const host = win?.parent
|
|
21
|
+
if (!host || host === win || !doc) return () => {}
|
|
22
|
+
const onKeyDown = (event) => {
|
|
23
|
+
if (event.key !== 'Escape') return
|
|
24
|
+
if (doc.querySelector('dialog[open]')) return
|
|
25
|
+
host.postMessage({ type }, win.location.origin)
|
|
26
|
+
}
|
|
27
|
+
doc.addEventListener('keydown', onKeyDown, true)
|
|
28
|
+
return () => doc.removeEventListener('keydown', onKeyDown, true)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 宿主页侧:收到面板里那份阅读页的关闭请求就关面板。
|
|
33
|
+
* 只认同一来源(同源)与约定的那一句话:这条消息只会关掉一个只读面板,
|
|
34
|
+
* 不带数据进来,也不触发任何读写。frame 在手上时再核一次事件来源,
|
|
35
|
+
* 免得别处的同源窗口冒名(拿不到 contentWindow 时不因此拒收——宁可关得上)。
|
|
36
|
+
*/
|
|
37
|
+
export function listenMaterialClose(win, frame, onClose, { type = MATERIAL_CLOSE_REQUEST } = {}) {
|
|
38
|
+
const onMessage = (event) => {
|
|
39
|
+
if (!event?.data || event.data.type !== type) return
|
|
40
|
+
if (event.origin !== win.location.origin) return
|
|
41
|
+
const source = frame?.contentWindow
|
|
42
|
+
if (event.source && source && event.source !== source) return
|
|
43
|
+
onClose()
|
|
44
|
+
}
|
|
45
|
+
win.addEventListener('message', onMessage)
|
|
46
|
+
return () => win.removeEventListener('message', onMessage)
|
|
47
|
+
}
|