@wsxjs/wsx-press 0.0.18
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/dist/client.cjs +1 -0
- package/dist/client.js +1256 -0
- package/dist/index-ChO3PMD5.js +461 -0
- package/dist/index-uNJnOC7n.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +8 -0
- package/dist/node.cjs +47 -0
- package/dist/node.js +1708 -0
- package/package.json +90 -0
- package/src/client/components/DocLayout.css +49 -0
- package/src/client/components/DocLayout.wsx +92 -0
- package/src/client/components/DocPage.css +56 -0
- package/src/client/components/DocPage.wsx +480 -0
- package/src/client/components/DocSearch.css +113 -0
- package/src/client/components/DocSearch.wsx +328 -0
- package/src/client/components/DocSidebar.css +97 -0
- package/src/client/components/DocSidebar.wsx +173 -0
- package/src/client/components/DocTOC.css +105 -0
- package/src/client/components/DocTOC.wsx +262 -0
- package/src/client/index.ts +32 -0
- package/src/client/styles/code.css +242 -0
- package/src/client/styles/index.css +12 -0
- package/src/client/styles/reset.css +116 -0
- package/src/client/styles/theme.css +171 -0
- package/src/client/styles/typography.css +239 -0
- package/src/index.ts +26 -0
- package/src/node/index.ts +16 -0
- package/src/node/metadata.ts +113 -0
- package/src/node/plugin.ts +223 -0
- package/src/node/search.ts +53 -0
- package/src/node/toc.ts +148 -0
- package/src/node/typedoc.ts +96 -0
- package/src/types/wsx.d.ts +11 -0
- package/src/types.test.ts +118 -0
- package/src/types.ts +150 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jsxImportSource @wsxjs/wsx-core
|
|
3
|
+
* DocPage Component
|
|
4
|
+
*
|
|
5
|
+
* A component that dynamically loads and displays documentation pages.
|
|
6
|
+
* Supports loading states, error handling, race condition prevention, and metadata caching.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LightComponent, autoRegister, state } from "@wsxjs/wsx-core";
|
|
10
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
11
|
+
// Import Markdown component to register it as a custom element
|
|
12
|
+
import "@wsxjs/wsx-marked-components";
|
|
13
|
+
import { RouterUtils } from "@wsxjs/wsx-router";
|
|
14
|
+
import type { DocMetadata, LoadingState } from "../../types";
|
|
15
|
+
import { DocumentLoadError } from "../../types";
|
|
16
|
+
import styles from "./DocPage.css?inline";
|
|
17
|
+
|
|
18
|
+
const logger = createLogger("DocPage");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 元数据缓存(全局共享)
|
|
22
|
+
* 导出以便测试时重置
|
|
23
|
+
*/
|
|
24
|
+
export const metadataCache: {
|
|
25
|
+
data: Record<string, DocMetadata> | null;
|
|
26
|
+
promise: Promise<Record<string, DocMetadata>> | null;
|
|
27
|
+
} = {
|
|
28
|
+
data: null,
|
|
29
|
+
promise: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 创建带超时的 fetch 请求(工具函数)
|
|
34
|
+
* 支持合并传入的 signal 和超时 signal,确保两者都能正确取消请求
|
|
35
|
+
*/
|
|
36
|
+
async function fetchWithTimeout(
|
|
37
|
+
url: string,
|
|
38
|
+
options: RequestInit = {},
|
|
39
|
+
timeout: number = 10000
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
// 创建超时 AbortController
|
|
42
|
+
const timeoutController = new AbortController();
|
|
43
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
44
|
+
|
|
45
|
+
// 如果传入了 signal,需要合并两个 signal
|
|
46
|
+
let finalSignal: AbortSignal;
|
|
47
|
+
if (options.signal) {
|
|
48
|
+
// 创建一个新的 AbortController 来合并两个 signal
|
|
49
|
+
const mergedController = new AbortController();
|
|
50
|
+
|
|
51
|
+
// 监听传入的 signal,如果被取消则取消合并的 controller
|
|
52
|
+
if (options.signal.aborted) {
|
|
53
|
+
mergedController.abort();
|
|
54
|
+
} else {
|
|
55
|
+
options.signal.addEventListener("abort", () => {
|
|
56
|
+
mergedController.abort();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 监听超时 signal,如果被取消则取消合并的 controller
|
|
61
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
62
|
+
mergedController.abort();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
finalSignal = mergedController.signal;
|
|
66
|
+
} else {
|
|
67
|
+
// 如果没有传入 signal,直接使用超时 signal
|
|
68
|
+
finalSignal = timeoutController.signal;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
...options,
|
|
74
|
+
signal: finalSignal,
|
|
75
|
+
});
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
return response;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
81
|
+
// 检查是哪个 signal 触发的取消
|
|
82
|
+
const isTimeout = timeoutController.signal.aborted;
|
|
83
|
+
const isUserCancel = options.signal?.aborted;
|
|
84
|
+
|
|
85
|
+
if (isUserCancel) {
|
|
86
|
+
throw new Error("Request was cancelled");
|
|
87
|
+
} else if (isTimeout) {
|
|
88
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error("Request was aborted");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 加载元数据(带缓存和超时)
|
|
99
|
+
*/
|
|
100
|
+
async function loadMetadata(): Promise<Record<string, DocMetadata>> {
|
|
101
|
+
// 如果已有缓存,直接返回
|
|
102
|
+
if (metadataCache.data) {
|
|
103
|
+
return metadataCache.data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 如果正在加载,等待现有请求
|
|
107
|
+
if (metadataCache.promise) {
|
|
108
|
+
return metadataCache.promise;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 创建新的加载请求(带超时)
|
|
112
|
+
metadataCache.promise = (async () => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetchWithTimeout("/.wsx-press/docs-meta.json", {}, 5000);
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Failed to load metadata: ${response.statusText}`);
|
|
117
|
+
}
|
|
118
|
+
const data = (await response.json()) as Record<string, DocMetadata>;
|
|
119
|
+
metadataCache.data = data;
|
|
120
|
+
metadataCache.promise = null;
|
|
121
|
+
return data;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
metadataCache.promise = null;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
|
|
128
|
+
return metadataCache.promise;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* DocPage Component
|
|
133
|
+
*
|
|
134
|
+
* Displays a documentation page by dynamically loading markdown content.
|
|
135
|
+
*/
|
|
136
|
+
@autoRegister({ tagName: "wsx-doc-page" })
|
|
137
|
+
export default class DocPage extends LightComponent {
|
|
138
|
+
@state private loadingState: LoadingState = "idle";
|
|
139
|
+
@state private markdown: string = "";
|
|
140
|
+
@state private metadata: DocMetadata | null = null;
|
|
141
|
+
@state private error: DocumentLoadError | null = null;
|
|
142
|
+
|
|
143
|
+
private currentLoadingPath: string | null = null;
|
|
144
|
+
private routeChangeUnsubscribe: (() => void) | null = null;
|
|
145
|
+
private loadingAbortController: AbortController | null = null;
|
|
146
|
+
private isLoading: boolean = false;
|
|
147
|
+
private visibilityChangeHandler: (() => void) | null = null;
|
|
148
|
+
|
|
149
|
+
constructor() {
|
|
150
|
+
super({
|
|
151
|
+
styles,
|
|
152
|
+
styleName: "wsx-doc-page",
|
|
153
|
+
});
|
|
154
|
+
logger.debug("DocPage initialized");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
protected onConnected() {
|
|
158
|
+
// 监听路由变化(当路由参数更新时重新加载文档)
|
|
159
|
+
// 注意:使用 onRouteChange 确保在路由匹配完成后再加载文档
|
|
160
|
+
this.routeChangeUnsubscribe = RouterUtils.onRouteChange(() => {
|
|
161
|
+
// 路由切换时,重置加载状态并重新加载
|
|
162
|
+
this.cancelLoading();
|
|
163
|
+
this.loadDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 监听标签页/视图切换
|
|
167
|
+
this.visibilityChangeHandler = () => {
|
|
168
|
+
if (document.hidden) {
|
|
169
|
+
// 标签页隐藏时,取消正在进行的加载
|
|
170
|
+
this.cancelLoading();
|
|
171
|
+
} else {
|
|
172
|
+
// 标签页重新可见时,如果当前是 loading 状态,重新加载
|
|
173
|
+
if (this.loadingState === "loading" && this.currentLoadingPath) {
|
|
174
|
+
logger.debug("Tab became visible, reloading document");
|
|
175
|
+
this.loadDocument();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
180
|
+
|
|
181
|
+
// 延迟加载,确保路由已经初始化
|
|
182
|
+
// 使用双重 requestAnimationFrame 确保路由匹配完成
|
|
183
|
+
requestAnimationFrame(() => {
|
|
184
|
+
requestAnimationFrame(() => {
|
|
185
|
+
// 从 RouterUtils 获取路由参数(由 wsx-router 包维护)
|
|
186
|
+
this.loadDocument();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected onDisconnected() {
|
|
192
|
+
// 清理路由变化监听器
|
|
193
|
+
if (this.routeChangeUnsubscribe) {
|
|
194
|
+
this.routeChangeUnsubscribe();
|
|
195
|
+
this.routeChangeUnsubscribe = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 清理标签页切换监听器
|
|
199
|
+
if (this.visibilityChangeHandler) {
|
|
200
|
+
document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
201
|
+
this.visibilityChangeHandler = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 取消正在进行的加载请求
|
|
205
|
+
this.cancelLoading();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 取消正在进行的加载请求
|
|
210
|
+
*/
|
|
211
|
+
private cancelLoading(): void {
|
|
212
|
+
if (this.loadingAbortController) {
|
|
213
|
+
this.loadingAbortController.abort();
|
|
214
|
+
this.loadingAbortController = null;
|
|
215
|
+
}
|
|
216
|
+
this.isLoading = false;
|
|
217
|
+
// 如果当前是 loading 状态,重置为 idle
|
|
218
|
+
if (this.loadingState === "loading") {
|
|
219
|
+
this.loadingState = "idle";
|
|
220
|
+
}
|
|
221
|
+
this.currentLoadingPath = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
protected onAttributeChanged(name: string, _oldValue: string, _newValue: string) {
|
|
225
|
+
// params 属性变化时也重新加载(向后兼容,但主要依赖 RouterUtils)
|
|
226
|
+
if (name === "params") {
|
|
227
|
+
this.loadDocument();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 创建带超时的 fetch 请求(实例方法)
|
|
233
|
+
*/
|
|
234
|
+
private async fetchWithTimeout(
|
|
235
|
+
url: string,
|
|
236
|
+
options: RequestInit = {},
|
|
237
|
+
timeout: number = 10000
|
|
238
|
+
): Promise<Response> {
|
|
239
|
+
return fetchWithTimeout(url, options, timeout);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 从 RouterUtils 获取当前路由参数并加载文档
|
|
244
|
+
*/
|
|
245
|
+
private async loadDocument(): Promise<void> {
|
|
246
|
+
// 取消之前的加载请求(如果有)
|
|
247
|
+
this.cancelLoading();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// 创建新的 AbortController
|
|
251
|
+
this.loadingAbortController = new AbortController();
|
|
252
|
+
|
|
253
|
+
// 从 RouterUtils 获取当前路由信息(包含参数)
|
|
254
|
+
const routeInfo = RouterUtils.getCurrentRoute();
|
|
255
|
+
const params = routeInfo.params as { category?: string; page?: string };
|
|
256
|
+
|
|
257
|
+
// 验证参数
|
|
258
|
+
if (!params.category || !params.page) {
|
|
259
|
+
logger.warn("Missing route parameters:", { params, routeInfo });
|
|
260
|
+
// 如果参数为空,可能是路由还未初始化,保持 idle 状态
|
|
261
|
+
if (Object.keys(params).length === 0) {
|
|
262
|
+
logger.debug("Route params not yet initialized, keeping idle state");
|
|
263
|
+
this.loadingState = "idle";
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.loadingState = "error";
|
|
267
|
+
this.error = new DocumentLoadError(
|
|
268
|
+
"Missing route parameters: category and page are required",
|
|
269
|
+
"INVALID_PARAMS"
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const docPath = `${params.category}/${params.page}`;
|
|
275
|
+
|
|
276
|
+
// 防止竞态条件:记录当前加载路径
|
|
277
|
+
this.currentLoadingPath = docPath;
|
|
278
|
+
this.isLoading = true;
|
|
279
|
+
|
|
280
|
+
// 重置状态
|
|
281
|
+
this.loadingState = "loading";
|
|
282
|
+
this.markdown = "";
|
|
283
|
+
this.error = null;
|
|
284
|
+
this.metadata = null;
|
|
285
|
+
|
|
286
|
+
// 加载元数据(内部已有超时处理)
|
|
287
|
+
const metadataMap = await loadMetadata();
|
|
288
|
+
|
|
289
|
+
// 检查是否已切换文档(防止竞态条件)
|
|
290
|
+
if (this.currentLoadingPath !== docPath || this.loadingAbortController.signal.aborted) {
|
|
291
|
+
logger.debug(`Document switched after metadata, ignoring result for ${docPath}`);
|
|
292
|
+
this.isLoading = false;
|
|
293
|
+
this.loadingState = "idle";
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 查找文档元数据
|
|
298
|
+
const meta = metadataMap[docPath];
|
|
299
|
+
if (!meta) {
|
|
300
|
+
// 检查是否已切换文档
|
|
301
|
+
if (
|
|
302
|
+
this.currentLoadingPath !== docPath ||
|
|
303
|
+
this.loadingAbortController.signal.aborted
|
|
304
|
+
) {
|
|
305
|
+
this.isLoading = false;
|
|
306
|
+
this.loadingState = "idle";
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.loadingState = "error";
|
|
310
|
+
this.error = new DocumentLoadError(`Document not found: ${docPath}`, "NOT_FOUND");
|
|
311
|
+
this.isLoading = false;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.metadata = meta;
|
|
316
|
+
|
|
317
|
+
// 加载 Markdown 内容(带超时和取消支持)
|
|
318
|
+
const markdownPath = `/docs/${docPath}.md`;
|
|
319
|
+
const response = await this.fetchWithTimeout(
|
|
320
|
+
markdownPath,
|
|
321
|
+
{ signal: this.loadingAbortController.signal },
|
|
322
|
+
10000
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// 再次检查是否已切换文档
|
|
326
|
+
if (this.currentLoadingPath !== docPath || this.loadingAbortController.signal.aborted) {
|
|
327
|
+
logger.debug(`Document switched during fetch, ignoring result for ${docPath}`);
|
|
328
|
+
this.isLoading = false;
|
|
329
|
+
this.loadingState = "idle";
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
if (response.status === 404) {
|
|
335
|
+
this.loadingState = "error";
|
|
336
|
+
this.error = new DocumentLoadError(
|
|
337
|
+
`Document file not found: ${markdownPath}`,
|
|
338
|
+
"NOT_FOUND"
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
this.loadingState = "error";
|
|
342
|
+
this.error = new DocumentLoadError(
|
|
343
|
+
`Failed to load document: ${response.statusText}`,
|
|
344
|
+
"NETWORK_ERROR",
|
|
345
|
+
{ status: response.status }
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
this.isLoading = false;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const markdownContent = await response.text();
|
|
353
|
+
|
|
354
|
+
// 最后一次检查是否已切换文档
|
|
355
|
+
if (this.currentLoadingPath !== docPath || this.loadingAbortController.signal.aborted) {
|
|
356
|
+
logger.debug(`Document switched after fetch, ignoring result for ${docPath}`);
|
|
357
|
+
this.isLoading = false;
|
|
358
|
+
this.loadingState = "idle";
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.markdown = markdownContent;
|
|
363
|
+
this.loadingState = "success";
|
|
364
|
+
this.isLoading = false;
|
|
365
|
+
|
|
366
|
+
// 关键修复:确保状态变化触发重新渲染
|
|
367
|
+
// 问题:当 loadingState = "loading" 时,scheduleRerender() 设置 _isRendering = true
|
|
368
|
+
// _rerender() 使用嵌套的 requestAnimationFrame,_isRendering 在第二个 RAF 后才清除
|
|
369
|
+
// 如果 loadingState = "success" 在第一个渲染完成前设置,scheduleRerender() 会被跳过
|
|
370
|
+
// 解决方案:使用双重 requestAnimationFrame 确保在 _isRendering 清除后调用
|
|
371
|
+
requestAnimationFrame(() => {
|
|
372
|
+
requestAnimationFrame(() => {
|
|
373
|
+
if (this.connected) {
|
|
374
|
+
this.scheduleRerender();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.isLoading = false;
|
|
380
|
+
|
|
381
|
+
// 如果是取消请求,重置为 idle 状态,不显示错误
|
|
382
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
383
|
+
logger.debug("Document loading was cancelled");
|
|
384
|
+
// 如果当前路径还存在,说明是用户切换了路由,保持 idle 状态
|
|
385
|
+
// 如果当前路径已被清除,说明组件已卸载,不需要更新状态
|
|
386
|
+
if (this.currentLoadingPath) {
|
|
387
|
+
this.loadingState = "idle";
|
|
388
|
+
this.currentLoadingPath = null;
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 检查是否已切换文档(使用当前加载路径)
|
|
394
|
+
if (!this.currentLoadingPath) {
|
|
395
|
+
// 如果没有当前加载路径,说明参数获取失败或已切换
|
|
396
|
+
this.loadingState = "error";
|
|
397
|
+
this.error = new DocumentLoadError(
|
|
398
|
+
`Failed to load document: ${error instanceof Error ? error.message : String(error)}`,
|
|
399
|
+
"NETWORK_ERROR",
|
|
400
|
+
error
|
|
401
|
+
);
|
|
402
|
+
logger.error("Failed to load document (no current path):", error);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const docPath = this.currentLoadingPath;
|
|
407
|
+
// 如果路径已改变,说明用户切换了路由,不显示错误
|
|
408
|
+
if (this.currentLoadingPath !== docPath) {
|
|
409
|
+
this.loadingState = "idle";
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 显示错误信息
|
|
414
|
+
this.loadingState = "error";
|
|
415
|
+
this.error = new DocumentLoadError(
|
|
416
|
+
`Failed to load document: ${error instanceof Error ? error.message : String(error)}`,
|
|
417
|
+
"NETWORK_ERROR",
|
|
418
|
+
error
|
|
419
|
+
);
|
|
420
|
+
logger.error("Failed to load document", error);
|
|
421
|
+
|
|
422
|
+
// 确保错误状态触发重新渲染(使用双重 RAF 确保在 _isRendering 清除后调用)
|
|
423
|
+
requestAnimationFrame(() => {
|
|
424
|
+
requestAnimationFrame(() => {
|
|
425
|
+
if (this.connected) {
|
|
426
|
+
this.scheduleRerender();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
render() {
|
|
434
|
+
if (this.loadingState === "loading") {
|
|
435
|
+
return (
|
|
436
|
+
<div class="doc-page">
|
|
437
|
+
<div class="doc-loading">加载文档中...</div>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (this.loadingState === "error") {
|
|
443
|
+
return (
|
|
444
|
+
<div class="doc-page">
|
|
445
|
+
<div class="doc-error">
|
|
446
|
+
<h2>加载失败</h2>
|
|
447
|
+
<p>{this.error?.message || "未知错误"}</p>
|
|
448
|
+
{this.error?.code === "NOT_FOUND" && (
|
|
449
|
+
<p>文档不存在,请检查路径是否正确。</p>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (this.loadingState === "success" && this.markdown) {
|
|
457
|
+
return (
|
|
458
|
+
<div class="doc-page">
|
|
459
|
+
{this.metadata && (
|
|
460
|
+
<div class="doc-header">
|
|
461
|
+
<h1 class="doc-title">{this.metadata.title}</h1>
|
|
462
|
+
{this.metadata.description && (
|
|
463
|
+
<p class="doc-description">{this.metadata.description}</p>
|
|
464
|
+
)}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
<div class="doc-content">
|
|
468
|
+
<wsx-markdown markdown={this.markdown} />
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<div class="doc-page">
|
|
476
|
+
<div class="doc-empty">请选择文档</div>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
.wsx-doc-search {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
max-width: 600px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.search-input-wrapper {
|
|
8
|
+
position: relative;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.search-input {
|
|
12
|
+
width: 100%;
|
|
13
|
+
padding: 0.75rem 1rem;
|
|
14
|
+
font-size: 1rem;
|
|
15
|
+
border: 1px solid var(--wsx-press-border, #ddd);
|
|
16
|
+
border-radius: 4px;
|
|
17
|
+
outline: none;
|
|
18
|
+
transition: border-color 0.2s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.search-input:focus {
|
|
22
|
+
border-color: var(--wsx-press-primary, #007bff);
|
|
23
|
+
box-shadow: 0 0 0 3px var(--wsx-press-primary-alpha, rgba(0, 123, 255, 0.1));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.search-loading {
|
|
27
|
+
position: absolute;
|
|
28
|
+
right: 1rem;
|
|
29
|
+
top: 50%;
|
|
30
|
+
transform: translateY(-50%);
|
|
31
|
+
color: var(--wsx-press-text-secondary, #666);
|
|
32
|
+
font-size: 0.875rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.search-results {
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 100%;
|
|
38
|
+
left: 0;
|
|
39
|
+
right: 0;
|
|
40
|
+
margin-top: 0.5rem;
|
|
41
|
+
background: var(--wsx-press-bg, #fff);
|
|
42
|
+
border: 1px solid var(--wsx-press-border, #ddd);
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
45
|
+
max-height: 400px;
|
|
46
|
+
overflow-y: auto;
|
|
47
|
+
z-index: 1000;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.search-result {
|
|
51
|
+
padding: 1rem;
|
|
52
|
+
border-bottom: 1px solid var(--wsx-press-border-light, #f0f0f0);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
transition: background-color 0.2s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.search-result:last-child {
|
|
58
|
+
border-bottom: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.search-result:hover,
|
|
62
|
+
.search-result.selected {
|
|
63
|
+
background-color: var(--wsx-press-hover-bg, #f5f5f5);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.result-title {
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
color: var(--wsx-press-text-primary, #333);
|
|
69
|
+
margin-bottom: 0.25rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.result-title mark {
|
|
73
|
+
background-color: var(--wsx-press-highlight-bg, #ffeb3b);
|
|
74
|
+
color: var(--wsx-press-highlight-text, #000);
|
|
75
|
+
padding: 0 2px;
|
|
76
|
+
border-radius: 2px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.result-category {
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
color: var(--wsx-press-text-secondary, #666);
|
|
82
|
+
margin-bottom: 0.5rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.result-snippet {
|
|
86
|
+
font-size: 0.875rem;
|
|
87
|
+
color: var(--wsx-press-text-secondary, #666);
|
|
88
|
+
line-height: 1.4;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.result-snippet mark {
|
|
92
|
+
background-color: var(--wsx-press-highlight-bg, #ffeb3b);
|
|
93
|
+
color: var(--wsx-press-highlight-text, #000);
|
|
94
|
+
padding: 0 2px;
|
|
95
|
+
border-radius: 2px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.search-no-results {
|
|
99
|
+
position: absolute;
|
|
100
|
+
top: 100%;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
margin-top: 0.5rem;
|
|
104
|
+
padding: 1rem;
|
|
105
|
+
background: var(--wsx-press-bg, #fff);
|
|
106
|
+
border: 1px solid var(--wsx-press-border, #ddd);
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
109
|
+
text-align: center;
|
|
110
|
+
color: var(--wsx-press-text-secondary, #666);
|
|
111
|
+
z-index: 1000;
|
|
112
|
+
}
|
|
113
|
+
|