gis-view-webgl2 0.1.0 → 0.1.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.
Files changed (87) hide show
  1. package/README.md +40 -8
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1036 -249
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
  7. package/docs/GisView.md +9 -8
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
  9. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +172 -0
  10. package/docs/api/index.html +545 -59
  11. package/docs/demos/BackendSyncDemo.vue +10 -8
  12. package/docs/demos/BasicDemo.vue +101 -101
  13. package/docs/demos/BatchAddDemo.vue +5 -5
  14. package/docs/demos/ChannelDemo.vue +617 -10
  15. package/docs/demos/ConnectDemo.vue +392 -0
  16. package/docs/demos/CopyMergeDemo.vue +980 -929
  17. package/docs/demos/DecorationDraftDemo.vue +1300 -0
  18. package/docs/demos/DemoShell.vue +569 -240
  19. package/docs/demos/DragDropDemo.vue +11 -11
  20. package/docs/demos/EffectsDemo.vue +6 -6
  21. package/docs/demos/EventDemo.vue +327 -327
  22. package/docs/demos/ExtensionDemo.vue +458 -0
  23. package/docs/demos/GisViewDemo.vue +61 -48
  24. package/docs/demos/GraphicDemo.vue +338 -0
  25. package/docs/demos/LabelDemo.vue +553 -497
  26. package/docs/demos/MinimapDemo.vue +358 -0
  27. package/docs/demos/PerfDemo.vue +29 -0
  28. package/docs/demos/SearchFilterDemo.vue +412 -0
  29. package/docs/demos/StyleDemo.vue +121 -121
  30. package/docs/demos/api-index.js +98 -77
  31. package/docs/demos/demos.js +159 -135
  32. package/docs/demos/shared/demoData.js +107 -107
  33. package/docs/demos/shared/demoStyle.css +249 -249
  34. package/docs/demos/shared/fakeBackend.js +93 -6
  35. package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +28 -8
  36. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  37. package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
  38. package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +6 -6
  39. package/docs/guide/sample-document.html +4 -4
  40. package/docs/index.html +180 -9
  41. package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
  42. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
  43. package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
  44. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -0
  45. package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
  46. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
  47. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
  48. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
  49. package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +111 -10
  50. package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +47 -9
  51. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
  52. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
  53. package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +74 -7
  54. package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
  55. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
  56. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
  57. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
  58. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
  59. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
  60. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
  61. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
  62. package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +4 -4
  63. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
  64. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +22 -6
  65. package/docs/playground/index.html +5 -5
  66. package/docs/steps/README.md +14 -10
  67. package/docs/steps/check-doc-truth.mjs +1 -1
  68. package/docs/steps/selftest-actions.html +10 -10
  69. package/docs/steps/selftest-data.html +96 -14
  70. package/docs/steps/selftest-decoration.html +577 -0
  71. package/docs/steps/selftest-demo.html +6 -5
  72. package/docs/steps/selftest-demos.html +2201 -1612
  73. package/docs/steps/selftest-docs.html +89 -16
  74. package/docs/steps/selftest-effects.html +5 -5
  75. package/docs/steps/selftest-features.html +757 -86
  76. package/docs/steps/selftest-full.html +583 -583
  77. package/docs/steps/selftest-labels.html +1034 -652
  78. package/docs/steps/selftest-perf.html +1899 -1844
  79. package/docs/steps/selftest-scale.html +117 -7
  80. package/docs/steps/selftest-select.html +24 -17
  81. package/docs/steps/step-10-full.js +1 -1
  82. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
  83. package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
  84. package/package.json +43 -43
  85. package/docs/demos/HistoryDemo.vue +0 -230
  86. package/docs/demos/InterfaceDemo.vue +0 -309
  87. package/docs/demos/SampleDemo.vue +0 -283
@@ -1,337 +1,666 @@
1
- <script setup>
2
- /**
3
- * 演示外壳:分组页签 + 一句说明 + 当前演示(动态组件)。
4
- *
5
- * 每个演示都是一个独立组件(BasicDemo / LabelDemo / ... / GisViewDemo),
6
- * 切到哪个才 import 哪个 —— 综合示例那份最大,不点进去就不会下载。
7
- * 组件卸载即销毁各自的视图(各演示自己 onBeforeUnmount destroy()),来回切不会漏画布。
8
- *
9
- * 外壳只管「怎么找演示」:页签按 DEMO_GROUPS 分组、每句说明旁边给一条对应文档的直达链接、
10
- * 地址栏可点播(`?demo=labels`)、记不住地址就接着上次看的(localStorage),
11
- * 键盘 `[` / `]` 前后翻页签(焦点在输入框里时不抢键)。
12
- */
13
- import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
14
- import { DEFAULT_DEMO_ID, DEMO_GROUPS, DEMOS } from './demos.js'
15
-
16
- const publicBase = String(import.meta.env?.BASE_URL ?? '/').replace(/\/?$/, '/')
17
- /** 外壳顶部的五个常驻入口:看文档、查 API、跑步进代码、看拆解、跑自测,都不用先离开当前演示 */
18
- const quickLinks = [
19
- { label: '文档中心', href: `${publicBase}docs/index.html`, title: '按章节阅读全部文档' },
20
- { label: 'API 速查', href: `${publicBase}docs/api/index.html`, title: '搜索实例方法、属性与 options' },
21
- { label: '调试页', href: `${publicBase}docs/playground/index.html`, title: '在可运行实例上改代码' },
22
- { label: '分步演示', href: `${publicBase}docs/steps/index.html`, title: '从空画布到完整组件的十三步' },
23
- { label: '自测', href: `${publicBase}docs/steps/selftest-demos.html`, title: '打开演示页的 PASS / FAIL 清单' }
24
- ]
25
-
26
- /** 地址栏可以点播:`?demo=labels` 直接进标签演示(文档里给链接、自测页切页签都靠它) */
27
- const requestedId = typeof window === 'undefined' ? '' : new URLSearchParams(window.location.search).get('demo')
28
- /** 上次看过的演示:地址栏没点播时接着它;读不出来(隐私模式 / 禁掉存储)就当没记过 */
29
- const DEMO_STORAGE_KEY = 'gis-view:last-demo'
30
- const storedId = (() => {
31
- try {
32
- return window.localStorage.getItem(DEMO_STORAGE_KEY) ?? ''
33
- }
34
- catch {
35
- return ''
36
- }
37
- })()
38
- const currentId = ref(
39
- DEMOS.some(item => item.id === requestedId) ? requestedId
40
- : DEMOS.some(item => item.id === storedId) ? storedId
41
- : DEFAULT_DEMO_ID
42
- )
43
- const current = computed(() => DEMOS.find(item => item.id === currentId.value) ?? DEMOS[0])
44
- /** 按 DEMO_GROUPS 的次序把演示分组:外壳一个循环画出「组名 + 组里的页签」 */
1
+ <script setup>
2
+ /**
3
+ * 演示外壳:分组页签 + 一句说明 + 当前演示(动态组件)。
4
+ *
5
+ * 每个演示都是一个独立组件(BasicDemo / LabelDemo / ... / GisViewDemo),
6
+ * 切到哪个才 import 哪个 —— 综合示例那份最大,不点进去就不会下载。
7
+ * 组件卸载即销毁各自的视图(各演示自己 onBeforeUnmount destroy()),来回切不会漏画布。
8
+ *
9
+ * 外壳只管「怎么找演示」:页签按 DEMO_GROUPS 分组、每句说明旁边给一条对应文档的直达链接、
10
+ * 地址栏可点播(`?demo=labels`)、记不住地址就接着上次看的(localStorage),
11
+ * 键盘 `[` / `]` 前后翻页签(焦点在输入框里时不抢键)。
12
+ */
13
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
14
+ import { DEFAULT_DEMO_ID, DEMO_GROUPS, DEMOS } from './demos.js'
15
+
16
+ const publicBase = String(import.meta.env?.BASE_URL ?? '/').replace(/\/?$/, '/')
17
+ /** 外壳顶部的五个常驻入口:看文档、查 API、跑步进代码、看拆解、跑自测,都不用先离开当前演示 */
18
+ const quickLinks = [
19
+ { label: '文档中心', href: `${publicBase}docs/index.html`, title: '按章节阅读全部文档' },
20
+ { label: 'API 速查', href: `${publicBase}docs/api/index.html`, title: '搜索实例方法、属性与 options' },
21
+ { label: '调试页', href: `${publicBase}docs/playground/index.html`, title: '在可运行实例上改代码' },
22
+ { label: '分步演示', href: `${publicBase}docs/steps/index.html`, title: '从空画布到完整组件的十三步' },
23
+ { label: '自测', href: `${publicBase}docs/steps/selftest-demos.html`, title: '打开演示页的 PASS / FAIL 清单' }
24
+ ]
25
+
26
+ /** 地址栏可以点播:`?demo=labels` 直接进标签演示(文档里给链接、自测页切页签都靠它) */
27
+ const requestedId = typeof window === 'undefined' ? '' : new URLSearchParams(window.location.search).get('demo')
28
+ /** 上次看过的演示:地址栏没点播时接着它;读不出来(隐私模式 / 禁掉存储)就当没记过 */
29
+ const DEMO_STORAGE_KEY = 'gis-view:last-demo'
30
+ const storedId = (() => {
31
+ try {
32
+ return window.localStorage.getItem(DEMO_STORAGE_KEY) ?? ''
33
+ }
34
+ catch {
35
+ return ''
36
+ }
37
+ })()
38
+ const currentId = ref(
39
+ DEMOS.some(item => item.id === requestedId) ? requestedId
40
+ : DEMOS.some(item => item.id === storedId) ? storedId
41
+ : DEFAULT_DEMO_ID
42
+ )
43
+ const current = computed(() => DEMOS.find(item => item.id === currentId.value) ?? DEMOS[0])
44
+ const currentIndex = computed(() => Math.max(0, DEMOS.findIndex(item => item.id === current.value.id)))
45
+ const currentGroup = computed(() => DEMO_GROUPS.find(group => group.id === current.value.group) ?? DEMO_GROUPS[0])
46
+ /** 上一个 / 下一步:按钮与键盘共用,到了头尾就绕回另一端 */
47
+ const stepDemo = (step) => {
48
+ const next = DEMOS[(currentIndex.value + step + DEMOS.length) % DEMOS.length]
49
+ currentId.value = next.id
50
+ }
51
+ /** 按 DEMO_GROUPS 的次序把演示分组:外壳一个循环画出「组名 + 组里的页签」 */
45
52
  const groupedDemos = computed(() => DEMO_GROUPS
46
53
  .map(group => ({ ...group, items: DEMOS.filter(item => item.group === group.id) }))
47
54
  .filter(group => group.items.length > 0))
55
+ /** 分组按钮:切到该组的第一项;当前项本来就在这组时保持不动 */
56
+ const selectGroup = (group) => {
57
+ if (!group.items.some(item => item.id === currentId.value) && group.items[0]) {
58
+ currentId.value = group.items[0].id
59
+ }
60
+ }
48
61
  const loaded = shallowRef(null)
49
62
  const loading = ref(true)
63
+ const loadError = ref('')
50
64
  let requestSeq = 0
51
-
52
- watch(currentId, async () => {
53
- const seq = (requestSeq += 1)
54
- loading.value = true
55
- loaded.value = null
56
- const module = await current.value.component()
57
- // 加载期间用户又切了页签:这一份迟到的结果直接丢掉,别把当前演示顶掉
58
- if (seq !== requestSeq) {
59
- return
65
+ /** 已经拉过的演示模块:切回来直接用缓存,失败的那一条删掉,重试时重新请求 */
66
+ const moduleCache = new Map()
67
+ const loadDemoModule = (demo) => {
68
+ let task = moduleCache.get(demo.id)
69
+ if (!task) {
70
+ task = Promise.resolve()
71
+ .then(() => demo.component())
72
+ .then(module => module.default ?? module)
73
+ moduleCache.set(demo.id, task)
74
+ task.catch(() => {
75
+ if (moduleCache.get(demo.id) === task) {
76
+ moduleCache.delete(demo.id)
77
+ }
78
+ })
60
79
  }
61
- loaded.value = module.default ?? module
62
- loading.value = false
63
- }, { immediate: true })
64
-
65
- /** 组件自己报一句读数(比如「已加载 N 个节点」):外壳只负责显示,不掺和演示内部 */
66
- const notice = ref('')
67
- const notify = (text) => {
68
- notice.value = text ?? ''
80
+ return task
69
81
  }
70
- // 切演示时把上一份的读数清掉,免得看着像新演示报的
71
- watch(currentId, () => { notice.value = '' })
72
-
73
- // 切页签时把当前演示写进地址栏:刷新 / 分享链接还停在同一个演示上
74
- watch(currentId, (id) => {
75
- if (typeof window === 'undefined') {
82
+ /** 当前演示加载完之后,空闲时把前后两份也拉下来:首次切过去不用再等网络 */
83
+ let idleHandle = 0
84
+ const clearIdleWarmup = () => {
85
+ if (!idleHandle) {
76
86
  return
77
87
  }
78
- const url = new URL(window.location.href)
79
- url.searchParams.set('demo', id)
80
- window.history.replaceState(null, '', url)
81
- try {
82
- window.localStorage.setItem(DEMO_STORAGE_KEY, id)
88
+ if (typeof window.cancelIdleCallback === 'function') {
89
+ window.cancelIdleCallback(idleHandle)
83
90
  }
84
- catch {
85
- // 存不了就算了:只是少一次「接着上次看」的回忆
91
+ else {
92
+ window.clearTimeout(idleHandle)
86
93
  }
87
- }, { immediate: true })
88
- /**
89
- * 键盘翻页签:`[` / `]` 前后一个(到头绕回)。焦点在输入框 / 可编辑元素里时不抢键 ——
90
- * 演示面板里到处是输入框,抢键会让「打一个方括号」变成切演示。
91
- */
92
- const handleDemoKeys = (event) => {
93
- if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
94
+ idleHandle = 0
95
+ }
96
+ const warmAdjacentDemos = () => {
97
+ clearIdleWarmup()
98
+ if (typeof window === 'undefined') {
94
99
  return
95
100
  }
96
- if (event.key !== '[' && event.key !== ']') {
97
- return
101
+ const run = () => {
102
+ idleHandle = 0
103
+ const next = DEMOS[(currentIndex.value + 1) % DEMOS.length]
104
+ const previous = DEMOS[(currentIndex.value - 1 + DEMOS.length) % DEMOS.length]
105
+ for (const demo of [next, previous]) {
106
+ if (demo && demo.id !== current.value.id) {
107
+ loadDemoModule(demo).catch(() => {})
108
+ }
109
+ }
98
110
  }
99
- const target = event.target
100
- if (target instanceof HTMLElement && (target.isContentEditable || target.closest('input, textarea, select'))) {
101
- return
111
+ idleHandle = typeof window.requestIdleCallback === 'function'
112
+ ? window.requestIdleCallback(run, { timeout: 2500 })
113
+ : window.setTimeout(run, 600)
114
+ }
115
+
116
+ const loadCurrent = async () => {
117
+ clearIdleWarmup()
118
+ const seq = (requestSeq += 1)
119
+ loading.value = true
120
+ loadError.value = ''
121
+ loaded.value = null
122
+ try {
123
+ const component = await loadDemoModule(current.value)
124
+ // 加载期间用户又切了页签:这一份迟到的结果直接丢掉,别把当前演示顶掉
125
+ if (seq !== requestSeq) {
126
+ return
127
+ }
128
+ loaded.value = component
129
+ loading.value = false
130
+ warmAdjacentDemos()
102
131
  }
103
- const step = event.key === ']' ? 1 : -1
104
- const at = DEMOS.findIndex(item => item.id === currentId.value)
105
- const next = DEMOS[(at + step + DEMOS.length) % DEMOS.length]
106
- currentId.value = next.id
132
+ catch (error) {
133
+ if (seq !== requestSeq) {
134
+ return
135
+ }
136
+ loadError.value = error instanceof Error ? error.message : String(error)
137
+ loading.value = false
138
+ }
139
+ }
140
+ watch(currentId, loadCurrent, { immediate: true })
141
+ /** 断网 / 部署路径写错这类失败之后不用刷新整页:清掉这一条的缓存再拉一次 */
142
+ const retryCurrent = () => {
143
+ moduleCache.delete(currentId.value)
144
+ loadCurrent()
107
145
  }
146
+
147
+ /** 组件自己报一句读数(比如「已加载 N 个节点」):外壳只负责显示,不掺和演示内部 */
148
+ const notice = ref('')
149
+ const notify = (text) => {
150
+ notice.value = text ?? ''
151
+ }
152
+ // 切演示时把上一份的读数清掉,免得看着像新演示报的
153
+ watch(currentId, () => { notice.value = '' })
154
+
155
+ // 切页签时把当前演示写进地址栏:刷新 / 分享链接还停在同一个演示上
156
+ watch(currentId, (id) => {
157
+ if (typeof window === 'undefined') {
158
+ return
159
+ }
160
+ const url = new URL(window.location.href)
161
+ url.searchParams.set('demo', id)
162
+ window.history.replaceState(null, '', url)
163
+ try {
164
+ window.localStorage.setItem(DEMO_STORAGE_KEY, id)
165
+ }
166
+ catch {
167
+ // 存不了就算了:只是少一次「接着上次看」的回忆
168
+ }
169
+ }, { immediate: true })
170
+ /**
171
+ * 键盘翻页签:`[` / `]` 前后一个(到头绕回)。焦点在输入框 / 可编辑元素里时不抢键 ——
172
+ * 演示面板里到处是输入框,抢键会让「打一个方括号」变成切演示。
173
+ */
174
+ const handleDemoKeys = (event) => {
175
+ if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
176
+ return
177
+ }
178
+ if (event.key !== '[' && event.key !== ']') {
179
+ return
180
+ }
181
+ const target = event.target
182
+ if (target instanceof HTMLElement && (target.isContentEditable || target.closest('input, textarea, select'))) {
183
+ return
184
+ }
185
+ const step = event.key === ']' ? 1 : -1
186
+ stepDemo(step)
187
+ }
108
188
  onMounted(() => window.addEventListener('keydown', handleDemoKeys))
109
189
  onBeforeUnmount(() => {
110
190
  requestSeq += 1
191
+ clearIdleWarmup()
111
192
  window.removeEventListener('keydown', handleDemoKeys)
112
193
  })
113
194
  </script>
114
195
 
115
196
  <template>
116
- <section class="demo-shell">
197
+ <section class="demo-shell" :aria-busy="loading">
117
198
  <div class="demo-shell__sticky">
118
- <nav class="demo-shell__tabs" aria-label="功能演示">
119
- <div v-for="group in groupedDemos" :key="group.id" class="demo-shell__group">
199
+ <nav class="demo-shell__groups" aria-label="演示分组">
200
+ <button
201
+ v-for="group in groupedDemos"
202
+ :key="group.id"
203
+ type="button"
204
+ class="demo-shell__group-tab"
205
+ :class="{ 'demo-shell__group-tab--active': group.id === currentGroup.id }"
206
+ :aria-pressed="group.id === currentGroup.id"
207
+ :data-demo-group="group.id"
208
+ :title="`${group.label}(${group.items.length} 个演示)`"
209
+ @click="selectGroup(group)"
210
+ >
120
211
  <span class="demo-shell__group-label">{{ group.label }}</span>
212
+ <span class="demo-shell__group-count">{{ group.items.length }}</span>
213
+ </button>
214
+ </nav>
215
+ <nav class="demo-shell__tabs" aria-label="当前分组的演示">
216
+ <div
217
+ v-for="group in groupedDemos"
218
+ :key="group.id"
219
+ class="demo-shell__group"
220
+ :class="{ 'demo-shell__group--active': group.id === currentGroup.id }"
221
+ >
121
222
  <button
122
- v-for="item in group.items"
123
- :key="item.id"
124
- type="button"
125
- class="demo-shell__tab"
126
- :class="{ 'demo-shell__tab--active': item.id === currentId }"
127
- :aria-current="item.id === currentId ? 'page' : undefined"
128
- :title="item.blurb"
129
- @click="currentId = item.id"
130
- >
223
+ v-for="item in group.items"
224
+ :key="item.id"
225
+ type="button"
226
+ class="demo-shell__tab"
227
+ :class="{ 'demo-shell__tab--active': item.id === currentId }"
228
+ :aria-current="item.id === currentId ? 'page' : undefined"
229
+ :title="item.blurb"
230
+ @click="currentId = item.id"
231
+ >
131
232
  {{ item.title }}
132
233
  </button>
133
234
  </div>
134
- </nav>
135
- <div class="demo-shell__quick" aria-label="快捷入口">
136
- <span class="demo-shell__quick-label">快速入口</span>
137
- <a
138
- v-for="link in quickLinks"
139
- :key="link.href"
140
- class="demo-shell__quick-link"
141
- :href="link.href"
142
- :title="link.title"
143
- target="_blank"
144
- rel="noreferrer"
145
- >{{ link.label }}</a>
146
- </div>
147
- </div>
148
- <p class="demo-shell__blurb">
149
- <strong>{{ current.title }}</strong>
150
- <span>{{ current.blurb }}</span>
151
- <span v-if="notice" class="demo-shell__notice">{{ notice }}</span>
152
- <a class="demo-shell__doc" :href="current.doc.href" target="_blank" rel="noreferrer">对应文档 · {{ current.doc.label }}</a>
153
- <span class="demo-shell__keys">[ / ] 切换</span>
235
+ </nav>
236
+ <div class="demo-shell__quick" aria-label="快捷入口">
237
+ <span class="demo-shell__quick-label">快速入口</span>
238
+ <a
239
+ v-for="link in quickLinks"
240
+ :key="link.href"
241
+ class="demo-shell__quick-link"
242
+ :href="link.href"
243
+ :title="link.title"
244
+ target="_blank"
245
+ rel="noreferrer"
246
+ >{{ link.label }}</a>
247
+ </div>
248
+ </div>
249
+ <p class="demo-shell__blurb">
250
+ <span class='demo-shell__eyebrow'>{{ currentGroup.label }} · {{ currentIndex + 1 }} / {{ DEMOS.length }}</span>
251
+ <strong>{{ current.title }}</strong>
252
+ <span>{{ current.blurb }}</span>
253
+ <span class='demo-shell__step'>
254
+ <button type='button' class='demo-shell__step-btn' data-demo='demo-prev' title='上一个演示([)' @click='stepDemo(-1)'>← 上一项</button>
255
+ <button type='button' class='demo-shell__step-btn' data-demo='demo-next' title='下一个演示(])' @click='stepDemo(1)'>下一项 →</button>
256
+ </span>
257
+ <span v-if="notice" class="demo-shell__notice">{{ notice }}</span>
258
+ <a class="demo-shell__doc" :href="current.doc.href" target="_blank" rel="noreferrer">对应文档 · {{ current.doc.label }}</a>
259
+ <span class="demo-shell__keys">[ / ] 切换</span>
260
+ </p>
261
+ <component :is="loaded" v-if="loaded" :key="current.id" @notice="notify" />
262
+ <p v-else-if="loading" class="demo-shell__loading" aria-live="polite">
263
+ <span class="demo-shell__spinner" aria-hidden="true"></span>
264
+ 正在加载「{{ current.title }}」…
154
265
  </p>
155
- <component :is="loaded" v-if="loaded" @notice="notify" />
156
- <p v-else-if="loading" class="demo-shell__loading">正在加载「{{ current.title }}」…</p>
266
+ <div v-else-if="loadError" class="demo-shell__error" role="alert">
267
+ <strong>「{{ current.title }}」加载失败</strong>
268
+ <p>{{ loadError }}</p>
269
+ <button type="button" class="demo-shell__retry" @click="retryCurrent">重新加载</button>
270
+ <span class="demo-shell__error-hint">检查网络或部署路径后重试,不会影响其它页签。</span>
271
+ </div>
157
272
  </section>
158
273
  </template>
159
-
160
- <style scoped>
161
- .demo-shell {
162
- min-width: 0;
163
- max-width: 100%;
164
- }
165
-
166
- .demo-shell__sticky {
167
- position: sticky;
168
- top: 0;
169
- z-index: 20;
274
+
275
+ <style scoped>
276
+ .demo-shell {
277
+ min-width: 0;
278
+ max-width: 100%;
279
+ }
280
+
281
+ .demo-shell__sticky {
282
+ position: sticky;
283
+ top: 0;
284
+ z-index: 20;
285
+ display: flex;
286
+ flex-direction: column;
287
+ gap: 6px;
288
+ min-width: 0;
289
+ max-width: 100%;
290
+ margin: -4px -10px 9px;
291
+ padding: 8px 10px 9px;
292
+ border: 1px solid rgba(214, 224, 236, 0.9);
293
+ border-radius: 10px;
294
+ background: rgba(244, 247, 251, 0.94);
295
+ box-shadow: 0 6px 22px rgba(31, 62, 105, 0.1);
296
+ backdrop-filter: blur(10px);
297
+ }
298
+
299
+ .demo-shell__tabs {
170
300
  display: flex;
171
- flex-direction: column;
301
+ align-items: center;
172
302
  gap: 6px;
173
303
  min-width: 0;
174
304
  max-width: 100%;
175
- margin: -4px -10px 9px;
176
- padding: 8px 10px 9px;
177
- border: 1px solid rgba(214, 224, 236, 0.9);
178
- border-radius: 10px;
179
- background: rgba(244, 247, 251, 0.94);
180
- box-shadow: 0 6px 22px rgba(31, 62, 105, 0.1);
181
- backdrop-filter: blur(10px);
305
+ padding-top: 2px;
182
306
  }
183
307
 
184
- .demo-shell__tabs {
308
+ .demo-shell__groups {
185
309
  display: flex;
186
310
  align-items: center;
187
311
  flex-wrap: wrap;
188
- gap: 6px 14px;
312
+ gap: 4px;
189
313
  min-width: 0;
190
- max-width: 100%;
191
314
  }
192
315
 
193
- .demo-shell__group {
194
- display: flex;
316
+ .demo-shell__group-tab {
317
+ display: inline-flex;
195
318
  align-items: center;
196
319
  gap: 6px;
197
- }
198
-
199
- .demo-shell__group-label {
200
- padding: 1px 7px;
201
- border-radius: 4px;
202
- background: #eef2f7;
203
- color: #8a9ab4;
204
- font-size: 11px;
205
- line-height: 18px;
206
- white-space: nowrap;
207
- }
208
-
209
- .demo-shell__tab {
210
- padding: 7px 13px;
320
+ padding: 3px 9px 3px 7px;
211
321
  border: 1px solid #d6e0ec;
212
322
  border-radius: 999px;
213
323
  background: #fff;
214
- color: #2f5e94;
215
- font-size: 13px;
324
+ color: var(--demo-group);
325
+ font: inherit;
326
+ font-size: 12px;
327
+ line-height: 1.6;
216
328
  cursor: pointer;
329
+ transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease;
217
330
  }
218
331
 
219
- .demo-shell__tab:hover {
220
- background: #eef4fc;
332
+ .demo-shell__group-tab:hover {
333
+ border-color: var(--demo-group);
334
+ background: var(--demo-group-soft);
221
335
  }
222
336
 
223
- .demo-shell__tab--active {
224
- border-color: #3678bf;
225
- background: #3678bf;
337
+ .demo-shell__group-tab--active {
338
+ border-color: var(--demo-group);
339
+ background: var(--demo-group);
226
340
  color: #fff;
227
341
  font-weight: 600;
228
342
  }
229
343
 
230
- .demo-shell__quick {
231
- display: flex;
344
+ .demo-shell__group-tab:nth-child(1) {
345
+ --demo-group: #3678bf;
346
+ --demo-group-soft: #eaf2fc;
347
+ }
348
+
349
+ .demo-shell__group-tab:nth-child(2) {
350
+ --demo-group: #2f8668;
351
+ --demo-group-soft: #e7f5ef;
352
+ }
353
+
354
+ .demo-shell__group-tab:nth-child(3) {
355
+ --demo-group: #7959bd;
356
+ --demo-group-soft: #f0ebfb;
357
+ }
358
+
359
+ .demo-shell__group-tab:nth-child(4) {
360
+ --demo-group: #a86418;
361
+ --demo-group-soft: #fbf1e4;
362
+ }
363
+
364
+ .demo-shell__group-tab:nth-child(5) {
365
+ --demo-group: #b24b5d;
366
+ --demo-group-soft: #fbecef;
367
+ }
368
+
369
+ .demo-shell__group-tab:nth-child(6) {
370
+ --demo-group: #4c5f91;
371
+ --demo-group-soft: #ebeff7;
372
+ }
373
+
374
+ .demo-shell__group {
375
+ display: none;
232
376
  align-items: center;
233
377
  flex-wrap: wrap;
234
- gap: 5px;
235
- padding-top: 6px;
236
- border-top: 1px dashed #dce6f1;
378
+ gap: 6px;
379
+ --demo-group: #3678bf;
380
+ --demo-group-soft: #eaf2fc;
237
381
  }
238
382
 
239
- .demo-shell__quick-label {
240
- margin-right: 2px;
241
- color: #93a2b9;
242
- font-size: 11px;
243
- white-space: nowrap;
383
+ .demo-shell__group--active {
384
+ display: flex;
244
385
  }
245
-
246
- .demo-shell__quick-link {
247
- padding: 2px 8px;
386
+
387
+ .demo-shell__group:nth-child(1) {
388
+ --demo-group: #3678bf;
389
+ --demo-group-soft: #eaf2fc;
390
+ }
391
+
392
+ .demo-shell__group:nth-child(2) {
393
+ --demo-group: #2f8668;
394
+ --demo-group-soft: #e7f5ef;
395
+ }
396
+
397
+ .demo-shell__group:nth-child(3) {
398
+ --demo-group: #7959bd;
399
+ --demo-group-soft: #f0ebfb;
400
+ }
401
+
402
+ .demo-shell__group:nth-child(4) {
403
+ --demo-group: #a86418;
404
+ --demo-group-soft: #fbf1e4;
405
+ }
406
+
407
+ .demo-shell__group:nth-child(5) {
408
+ --demo-group: #b24b5d;
409
+ --demo-group-soft: #fbecef;
410
+ }
411
+
412
+ .demo-shell__group:nth-child(6) {
413
+ --demo-group: #4c5f91;
414
+ --demo-group-soft: #ebeff7;
415
+ }
416
+
417
+ .demo-shell__group-count {
418
+ min-width: 16px;
419
+ padding: 0 4px;
248
420
  border-radius: 999px;
249
- background: #eaf2fc;
250
- color: #2f5e94;
251
- font-size: 11px;
252
- text-decoration: none;
253
- transition: background 0.12s ease, color 0.12s ease;
421
+ background: var(--demo-group-soft);
422
+ color: var(--demo-group);
423
+ font-size: 10px;
424
+ line-height: 16px;
425
+ text-align: center;
426
+ font-variant-numeric: tabular-nums;
254
427
  }
255
428
 
256
- .demo-shell__quick-link:hover {
257
- background: #3678bf;
429
+ .demo-shell__group-tab--active .demo-shell__group-count {
430
+ background: rgba(255, 255, 255, 0.2);
258
431
  color: #fff;
259
432
  }
260
-
261
- .demo-shell__blurb {
433
+
434
+ .demo-shell__tab {
435
+ padding: 7px 13px;
436
+ border: 1px solid #d6e0ec;
437
+ border-radius: 999px;
438
+ background: #fff;
439
+ color: var(--demo-group);
440
+ font-size: 13px;
441
+ cursor: pointer;
442
+ }
443
+
444
+ .demo-shell__tab:hover {
445
+ background: var(--demo-group-soft);
446
+ }
447
+
448
+ .demo-shell__tab--active {
449
+ border-color: var(--demo-group);
450
+ background: var(--demo-group);
451
+ color: #fff;
452
+ font-weight: 600;
453
+ }
454
+
455
+ .demo-shell__quick {
456
+ display: flex;
457
+ align-items: center;
458
+ flex-wrap: wrap;
459
+ gap: 5px;
460
+ padding-top: 6px;
461
+ border-top: 1px dashed #dce6f1;
462
+ }
463
+
464
+ .demo-shell__quick-label {
465
+ margin-right: 2px;
466
+ color: #93a2b9;
467
+ font-size: 11px;
468
+ white-space: nowrap;
469
+ }
470
+
471
+ .demo-shell__quick-link {
472
+ padding: 2px 8px;
473
+ border-radius: 999px;
474
+ background: #eaf2fc;
475
+ color: #2f5e94;
476
+ font-size: 11px;
477
+ text-decoration: none;
478
+ transition: background 0.12s ease, color 0.12s ease;
479
+ }
480
+
481
+ .demo-shell__quick-link:hover {
482
+ background: #3678bf;
483
+ color: #fff;
484
+ }
485
+
486
+ .demo-shell__blurb {
487
+ display: flex;
488
+ align-items: baseline;
489
+ flex-wrap: wrap;
490
+ gap: 8px;
491
+ margin: 0 0 12px;
492
+ color: #71809d;
493
+ font-size: 13px;
494
+ line-height: 1.6;
495
+ }
496
+
497
+ .demo-shell__blurb strong {
498
+ color: #17233d;
499
+ font-size: 14px;
500
+ }
501
+
502
+ .demo-shell__eyebrow {
503
+ padding: 1px 8px;
504
+ border-radius: 999px;
505
+ background: #eaf2fc;
506
+ color: #5a7296;
507
+ font-size: 11px;
508
+ line-height: 1.8;
509
+ font-variant-numeric: tabular-nums;
510
+ white-space: nowrap;
511
+ }
512
+
513
+ .demo-shell__step {
514
+ display: inline-flex;
515
+ gap: 4px;
516
+ }
517
+
518
+ .demo-shell__step-btn {
519
+ padding: 2px 8px;
520
+ border: 1px solid #d6e0ec;
521
+ border-radius: 999px;
522
+ background: #fff;
523
+ color: #2f5e94;
524
+ font-size: 11px;
525
+ line-height: 1.7;
526
+ cursor: pointer;
527
+ }
528
+
529
+ .demo-shell__step-btn:hover {
530
+ background: #eef4fc;
531
+ }
532
+
533
+ .demo-shell__notice {
534
+ padding: 2px 8px;
535
+ border-radius: 999px;
536
+ background: #eaf2fc;
537
+ color: #2f5e94;
538
+ font-size: 11px;
539
+ }
540
+
541
+ .demo-shell__doc {
542
+ color: #3678bf;
543
+ font-size: 12px;
544
+ text-decoration: none;
545
+ }
546
+
547
+ .demo-shell__doc:hover {
548
+ text-decoration: underline;
549
+ }
550
+
551
+ .demo-shell__keys {
552
+ margin-left: auto;
553
+ padding: 1px 8px;
554
+ border: 1px solid #e2e8f1;
555
+ border-radius: 999px;
556
+ color: #9aa7bc;
557
+ font-size: 11px;
558
+ white-space: nowrap;
559
+ }
560
+
561
+ .demo-shell__loading {
262
562
  display: flex;
263
- align-items: baseline;
264
- flex-wrap: wrap;
265
- gap: 8px;
266
- margin: 0 0 12px;
267
- color: #71809d;
563
+ align-items: center;
564
+ justify-content: center;
565
+ gap: 9px;
566
+ margin: 0;
567
+ padding: 28px;
568
+ border: 1px dashed #d6e0ec;
569
+ border-radius: 10px;
570
+ background: #fff;
571
+ color: #8191a8;
572
+ font-size: 13px;
573
+ text-align: center;
574
+ }
575
+
576
+ .demo-shell__spinner {
577
+ width: 14px;
578
+ height: 14px;
579
+ border: 2px solid #c8d9ee;
580
+ border-top-color: #3678bf;
581
+ border-radius: 50%;
582
+ animation: demo-shell-spin 0.7s linear infinite;
583
+ }
584
+
585
+ @keyframes demo-shell-spin {
586
+ to {
587
+ transform: rotate(1turn);
588
+ }
589
+ }
590
+
591
+ .demo-shell__error {
592
+ margin: 0;
593
+ padding: 20px;
594
+ border: 1px solid #efcccc;
595
+ border-radius: 10px;
596
+ background: #fff8f8;
597
+ color: #874242;
268
598
  font-size: 13px;
269
- line-height: 1.6;
599
+ line-height: 1.7;
270
600
  }
271
601
 
272
- .demo-shell__blurb strong {
273
- color: #17233d;
602
+ .demo-shell__error strong {
603
+ color: #9c2f2f;
274
604
  font-size: 14px;
275
605
  }
276
606
 
277
- .demo-shell__notice {
278
- padding: 2px 8px;
279
- border-radius: 999px;
280
- background: #eaf2fc;
281
- color: #2f5e94;
282
- font-size: 11px;
607
+ .demo-shell__error p {
608
+ margin: 6px 0 12px;
609
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
610
+ font-size: 12px;
611
+ overflow-wrap: anywhere;
283
612
  }
284
613
 
285
- .demo-shell__doc {
286
- color: #3678bf;
614
+ .demo-shell__retry {
615
+ padding: 6px 13px;
616
+ border: 1px solid #b84949;
617
+ border-radius: 999px;
618
+ background: #b84949;
619
+ color: #fff;
287
620
  font-size: 12px;
288
- text-decoration: none;
621
+ cursor: pointer;
289
622
  }
290
623
 
291
- .demo-shell__doc:hover {
292
- text-decoration: underline;
624
+ .demo-shell__retry:hover {
625
+ background: #9e3939;
293
626
  }
294
627
 
295
- .demo-shell__keys {
296
- margin-left: auto;
297
- padding: 1px 8px;
298
- border: 1px solid #e2e8f1;
299
- border-radius: 999px;
300
- color: #9aa7bc;
628
+ .demo-shell__error-hint {
629
+ margin-left: 10px;
630
+ color: #a46d6d;
301
631
  font-size: 11px;
302
- white-space: nowrap;
303
- }
304
-
305
- .demo-shell__loading {
306
- margin: 0;
307
- padding: 28px;
308
- border: 1px dashed #d6e0ec;
309
- border-radius: 10px;
310
- background: #fff;
311
- color: #8191a8;
312
- font-size: 13px;
313
- text-align: center;
314
632
  }
315
633
 
316
634
  @media (max-width: 760px) {
317
- .demo-shell__sticky {
318
- position: static;
319
- margin: -4px 0 9px;
320
- }
321
-
322
- .demo-shell__tabs {
635
+ .demo-shell__sticky {
636
+ position: static;
637
+ margin: -4px 0 9px;
638
+ }
639
+
640
+ .demo-shell__groups {
323
641
  flex-wrap: nowrap;
324
642
  width: 100%;
325
643
  overflow-x: auto;
326
644
  padding-bottom: 3px;
327
645
  }
328
646
 
329
- .demo-shell__group {
647
+ .demo-shell__group-tab {
330
648
  flex: none;
331
649
  }
332
650
 
333
- .demo-shell__keys {
334
- margin-left: 0;
335
- }
336
- }
337
- </style>
651
+ .demo-shell__tabs {
652
+ flex-wrap: nowrap;
653
+ width: 100%;
654
+ overflow-x: auto;
655
+ padding-bottom: 3px;
656
+ }
657
+
658
+ .demo-shell__group {
659
+ flex: none;
660
+ }
661
+
662
+ .demo-shell__keys {
663
+ margin-left: 0;
664
+ }
665
+ }
666
+ </style>