Skip to main content

yggdrasil/pages/components_showcase/
business_previews.rs

1//! Local examples for business components. The production controllers stay in components/.
2use leptos::prelude::*;
3
4use crate::api::database::sql_console::SqlResult;
5use crate::components::assets::asset_picker::{PickerAsset, PickerGallery, PickerSelectionFooter};
6use crate::components::assets::asset_upload::{
7    UploadDropZoneContent, UploadItem, UploadPanel, UploadStatus,
8};
9use crate::components::assets::AssetSelection;
10use crate::components::code_runner::runner::RunnerExample;
11use crate::components::code_runner::CodeRunner;
12use crate::components::sql_result_table::SqlResultTable;
13use crate::components::ui::{
14    Button, ButtonSize, ButtonVariant, Icon, IconButton, IconName, ModalShell, SegmentedControl,
15    SegmentedItem, TablePager,
16};
17
18pub(super) fn preview(slug: &str, detail: bool) -> Option<AnyView> {
19    Some(match slug {
20        "asset-picker-modal" => view! { <PickerDemo detail=detail/> }.into_any(),
21        "asset-upload-modal" => view! { <UploadDemo detail=detail/> }.into_any(),
22        "code-runner" => view! { <RunnerDemo detail=detail/> }.into_any(),
23        "sql-result-table" => view! { <SqlTableDemo detail=detail/> }.into_any(),
24        _ => return None,
25    })
26}
27
28const IMAGES: [(&str, &str, &str); 5] = [
29    (
30        "/images/samples/clay-circle.webp",
31        "纸面与陶土.webp",
32        "暖白纸面上的一轮陶土色圆",
33    ),
34    (
35        "/images/samples/hairline-grid.webp",
36        "细线网格.webp",
37        "细线网格上连起的几个点",
38    ),
39    (
40        "/images/samples/ink-dusk.webp",
41        "夜色弧线.webp",
42        "深色背景上层层展开的陶土色弧线",
43    ),
44    (
45        "/images/samples/stacked.webp",
46        "叠放的卡片.webp",
47        "两张叠放的卡片",
48    ),
49    (
50        "/images/samples/contours.webp",
51        "等高线.webp",
52        "起伏的等高线",
53    ),
54];
55
56fn picker_page(query: &str, page: i32) -> (Vec<PickerAsset>, i64) {
57    let query = query.trim().to_lowercase();
58    let matches: Vec<PickerAsset> = IMAGES
59        .iter()
60        .filter(|(_, name, alt)| {
61            query.is_empty()
62                || name.to_lowercase().contains(&query)
63                || alt.to_lowercase().contains(&query)
64        })
65        .map(|(url, name, alt)| PickerAsset {
66            url: (*url).to_string(),
67            thumbnail: (*url).to_string(),
68            filename: (*name).to_string(),
69            alt: Some((*alt).to_string()),
70            fresh: false,
71        })
72        .collect();
73    let total = matches.len() as i64;
74    let start = ((page - 1).max(0) as usize) * 3;
75    (matches.into_iter().skip(start).take(3).collect(), total)
76}
77
78fn sample_selected() -> Vec<AssetSelection> {
79    IMAGES[..2]
80        .iter()
81        .map(|(url, _, alt)| AssetSelection {
82            url: (*url).to_string(),
83            alt: Some((*alt).to_string()),
84        })
85        .collect()
86}
87
88/// 演示弹窗头部:与正式弹窗同为 `.asset-modal-head`,关闭前多一个「恢复默认」。
89#[component]
90fn DemoModalHead(
91    title: &'static str,
92    #[prop(into)] on_reset: Callback<()>,
93    #[prop(into)] on_close: Callback<()>,
94) -> impl IntoView {
95    view! {
96        <div class="asset-modal-head">
97            <h2>{title}</h2>
98            <div class="flex items-center gap-1">
99                <Button
100                    variant=ButtonVariant::Ghost
101                    size=ButtonSize::Sm
102                    icon=IconName::RotateCcw
103                    onclick=move |_| on_reset.run(())
104                >
105                    "恢复默认"
106                </Button>
107                <IconButton
108                    icon=IconName::X
109                    label="关闭"
110                    size=ButtonSize::Sm
111                    onclick=move |_| on_close.run(())
112                />
113            </div>
114        </div>
115    }
116}
117
118#[component]
119fn PickerDemo(detail: bool) -> impl IntoView {
120    let visible = RwSignal::new(false);
121    let closing = RwSignal::new(false);
122    let selected = RwSignal::new(sample_selected());
123    let multi = RwSignal::new(true);
124    let query = RwSignal::new(String::new());
125    let page = RwSignal::new(1_i32);
126    let confirmed: RwSignal<Vec<AssetSelection>> = RwSignal::new(Vec::new());
127    let result_label = move || {
128        confirmed.with(|items| {
129            items
130                .iter()
131                .map(|item| item.url.clone())
132                .collect::<Vec<_>>()
133                .join("、")
134        })
135    };
136    let root_class = if detail {
137        "showcase-picker-demo showcase-picker-detail"
138    } else {
139        "showcase-picker-demo"
140    };
141    view! {
142        <div class=root_class>
143            {detail
144                .then(|| {
145                    view! {
146                        <div class="showcase-demo-bar">
147                            <Button
148                                variant=ButtonVariant::Secondary
149                                size=ButtonSize::Sm
150                                onclick=move |_| {
151                                    selected.set(Vec::new());
152                                    page.set(1);
153                                    visible.set(true);
154                                }
155                            >
156                                "打开素材选择弹窗"
157                            </Button>
158                            <span>"本地演示,选择不会写入文章。"</span>
159                        </div>
160                    }
161                })}
162            {move || {
163                (!visible.get())
164                    .then(|| {
165                        view! {
166                            <div class="showcase-picker-panel">
167                                <PickerDemoContent
168                                    detail=detail
169                                    selected=selected
170                                    multi=multi
171                                    query=query
172                                    page=page
173                                    confirmed=confirmed
174                                    visible=visible
175                                />
176                            </div>
177                        }
178                    })
179            }}
180            {detail
181                .then(|| {
182                    view! {
183                        <ModalShell
184                            visible=visible
185                            closing=closing
186                            title="选择图片"
187                            overlay_padding="p-4 sm:p-6"
188                            panel_class="asset-modal asset-picker"
189                        >
190                            <DemoModalHead
191                                title="选择图片"
192                                on_reset=move |_| {
193                                    selected.set(sample_selected());
194                                    multi.set(true);
195                                    query.set(String::new());
196                                    page.set(1);
197                                    confirmed.set(Vec::new());
198                                    closing.set(true);
199                                    visible.set(false);
200                                }
201                                on_close=move |_| {
202                                    closing.set(true);
203                                    visible.set(false);
204                                }
205                            />
206                            <PickerDemoContent
207                                detail=true
208                                selected=selected
209                                multi=multi
210                                query=query
211                                page=page
212                                confirmed=confirmed
213                                visible=visible
214                            />
215                        </ModalShell>
216                    }
217                })}
218            {move || {
219                (detail && !confirmed.with(Vec::is_empty))
220                    .then(|| {
221                        view! {
222                            <p class="showcase-demo-result">
223                                {move || format!("已确认:{}", result_label())}
224                            </p>
225                        }
226                    })
227            }}
228        </div>
229    }
230}
231
232#[component]
233fn PickerDemoContent(
234    detail: bool,
235    selected: RwSignal<Vec<AssetSelection>>,
236    multi: RwSignal<bool>,
237    query: RwSignal<String>,
238    page: RwSignal<i32>,
239    confirmed: RwSignal<Vec<AssetSelection>>,
240    visible: RwSignal<bool>,
241) -> impl IntoView {
242    let items = Signal::derive(move || picker_page(&query.get(), page.get()).0);
243    let total = Signal::derive(move || picker_page(&query.get(), page.get()).1);
244    let mode = Signal::derive(move || if multi.get() { "multi" } else { "single" }.to_string());
245    let on_pick = Callback::new(move |picks: Vec<AssetSelection>| {
246        confirmed.set(picks);
247        visible.set(false);
248    });
249    // 不带外框:卡片预览外包 .showcase-picker-panel,弹窗里直接排进 .asset-modal。
250    view! {
251        <div class="asset-modal-bar">
252            <label class="field field-sm admin-filter-search asset-modal-search">
253                <Icon name=IconName::Search size=14/>
254                <span class="sr-only">"搜索素材"</span>
255                <input
256                    type="search"
257                    placeholder="文件名或替代文本"
258                    prop:value=move || query.get()
259                    on:input=move |e| {
260                        query.set(event_target_value(&e));
261                        page.set(1);
262                    }
263                />
264            </label>
265            {detail
266                .then(|| {
267                    view! {
268                        <SegmentedControl
269                            items=vec![
270                                SegmentedItem::new("single".to_string(), "单选"),
271                                SegmentedItem::new("multi".to_string(), "多选"),
272                            ]
273                            value=mode
274                            on_change=move |value: String| {
275                                multi.set(value == "multi");
276                                selected.set(Vec::new());
277                            }
278                            aria_label="选择方式"
279                        />
280                    }
281                })}
282        </div>
283        {move || {
284            let multi = multi.get();
285            view! {
286                <PickerGallery
287                    items=items
288                    selected=selected
289                    multi=multi
290                    loading=false
291                    uploading=false
292                    error=Signal::stored(None::<String>)
293                    uploading_preview=Signal::stored(None::<String>)
294                    on_pick=on_pick
295                />
296            }
297        }}
298        {move || {
299            (total.get() > 3)
300                .then(|| {
301                    view! {
302                        <div class="asset-modal-pager">
303                            <TablePager
304                                page=page
305                                total=total
306                                per_page=3
307                                unit="张"
308                                on_change=move |next: i32| {
309                                    page.set(next.clamp(1, ((total.get_untracked() + 2) / 3) as i32))
310                                }
311                            />
312                        </div>
313                    }
314                })
315        }}
316        {move || {
317            multi.get().then(|| view! { <PickerSelectionFooter selected=selected on_pick=on_pick/> })
318        }}
319    }
320}
321
322#[component]
323fn UploadDemo(detail: bool) -> impl IntoView {
324    let visible = RwSignal::new(false);
325    let closing = RwSignal::new(false);
326    let items = RwSignal::new(sample_upload_items());
327    let generation = RwSignal::new(0_u64);
328    let root_class = if detail {
329        "showcase-upload-demo showcase-upload-detail"
330    } else {
331        "showcase-upload-demo"
332    };
333    view! {
334        <div class=root_class>
335            {detail
336                .then(|| {
337                    view! {
338                        <div class="showcase-demo-bar">
339                            <Button
340                                variant=ButtonVariant::Secondary
341                                size=ButtonSize::Sm
342                                onclick=move |_| visible.set(true)
343                            >
344                                "打开上传弹窗"
345                            </Button>
346                            <Button
347                                variant=ButtonVariant::Ghost
348                                size=ButtonSize::Sm
349                                onclick=move |_| {
350                                    generation.update(|value| *value += 1);
351                                    items.set(Vec::new());
352                                }
353                            >
354                                "空状态"
355                            </Button>
356                            <Button
357                                variant=ButtonVariant::Ghost
358                                size=ButtonSize::Sm
359                                onclick=move |_| {
360                                    generation.update(|value| *value += 1);
361                                    items.set(sample_upload_items());
362                                }
363                            >
364                                "代表状态"
365                            </Button>
366                            <span>"本地演示,示例文件不会上传。"</span>
367                        </div>
368                    }
369                })}
370            {move || {
371                (!visible.get()).then(|| view! { <UploadDemoPanel items=items generation=generation/> })
372            }}
373            {detail
374                .then(|| {
375                    view! {
376                        <ModalShell
377                            visible=visible
378                            closing=closing
379                            title="上传素材"
380                            panel_class="asset-modal asset-uploader"
381                        >
382                            <DemoModalHead
383                                title="上传素材"
384                                on_reset=move |_| {
385                                    generation.update(|value| *value += 1);
386                                    items.set(sample_upload_items());
387                                    closing.set(true);
388                                    visible.set(false);
389                                }
390                                on_close=move |_| {
391                                    closing.set(true);
392                                    visible.set(false);
393                                }
394                            />
395                            <UploadDemoPanel items=items generation=generation/>
396                        </ModalShell>
397                    }
398                })}
399        </div>
400    }
401}
402
403fn sample_upload_items() -> Vec<UploadItem> {
404    [
405        ("封面.webp", "1.2 MB", UploadStatus::Done),
406        ("相册-02.png", "860 KB", UploadStatus::Uploading),
407        ("草稿插图.jpg", "420 KB", UploadStatus::Queued),
408        (
409            "超大原图.png",
410            "8.4 MB",
411            UploadStatus::Failed("大小超过 5MB 限制".to_string()),
412        ),
413    ]
414    .into_iter()
415    .enumerate()
416    .map(|(index, (name, size, status))| UploadItem {
417        id: index as u64 + 1,
418        name: name.to_string(),
419        size: size.to_string(),
420        status,
421        removing: false,
422    })
423    .collect()
424}
425
426#[component]
427fn UploadDemoPanel(items: RwSignal<Vec<UploadItem>>, generation: RwSignal<u64>) -> impl IntoView {
428    let on_retry = move |id: u64| {
429        let ticket = generation.get_untracked();
430        items.update(|list| {
431            if let Some(item) = list.iter_mut().find(|item| item.id == id) {
432                item.status = UploadStatus::Uploading;
433            }
434        });
435        leptos::task::spawn_local(async move {
436            crate::utils::time::sleep_ms(350).await;
437            if generation.try_get_untracked() != Some(ticket) {
438                return;
439            }
440            let _ = items.try_update(|list| {
441                if let Some(item) = list.iter_mut().find(|item| item.id == id) {
442                    item.status = UploadStatus::Done;
443                }
444            });
445        });
446    };
447    let on_remove = move |id: u64| items.update(|list| list.retain(|item| item.id != id));
448    view! {
449        <UploadPanel items=items on_retry=on_retry on_remove=on_remove>
450            <button
451                type="button"
452                class="asset-drop w-full border-0"
453                on:click=move |_| {
454                    items
455                        .update(|list| {
456                            let next = list.iter().map(|item| item.id).max().unwrap_or(0) + 1;
457                            list.push(UploadItem {
458                                id: next,
459                                name: format!("示例文件-{next}.webp"),
460                                size: "540 KB".to_string(),
461                                status: UploadStatus::Queued,
462                                removing: false,
463                            });
464                        })
465                }
466            >
467                <UploadDropZoneContent
468                    title="加入示例文件"
469                    hint="本地状态演示,不读取电脑文件或发起上传。"
470                />
471            </button>
472        </UploadPanel>
473    }
474}
475
476#[component]
477fn RunnerDemo(detail: bool) -> impl IntoView {
478    let failed = RwSignal::new(false);
479    let host_id = if detail {
480        "showcase-runner-detail"
481    } else {
482        "showcase-runner-card"
483    };
484    // 详情页直接加载;卡片预览等运行器进入视野后再加载编辑器和终端。
485    let near_viewport = crate::hooks::use_near_viewport(host_id.to_string(), "0px");
486    let visible = move || detail || near_viewport.get();
487    let root_class = if detail {
488        "showcase-runner-demo showcase-runner-detail"
489    } else {
490        "showcase-runner-demo"
491    };
492    view! {
493        <div class=root_class id=host_id>
494            {detail
495                .then(|| {
496                    view! {
497                        <div class="showcase-demo-bar">
498                            <SegmentedControl
499                                items=vec![
500                                    SegmentedItem::new("ok".to_string(), "正常输出"),
501                                    SegmentedItem::new("error".to_string(), "错误输出"),
502                                ]
503                                value=Signal::derive(move || {
504                                    if failed.get() { "error" } else { "ok" }.to_string()
505                                })
506                                on_change=move |value: String| failed.set(value == "error")
507                                aria_label="输出示例"
508                            />
509                            <span>"固定输出示例,与编辑后的代码无关;不会执行程序。"</span>
510                        </div>
511                    }
512                })}
513            {move || {
514                if visible() {
515                    let sample = if failed.get() {
516                        RunnerExample {
517                            stdout: "开始整理 3 个条目…\n".to_string(),
518                            stderr: "示例错误:第 2 个条目缺少标题\n".to_string(),
519                            status: "退出码: 1 · 输出示例".to_string(),
520                            error: "运行错误示例".to_string(),
521                        }
522                    } else {
523                        RunnerExample {
524                            stdout: "开始整理 3 个条目…\n✓ 已整理 3 个条目\n".to_string(),
525                            stderr: "".to_string(),
526                            status: "退出码: 0 · 输出示例".to_string(),
527                            error: String::new(),
528                        }
529                    };
530                    // 切换输出示例时整体重建运行器,让它按新的固定输出初始化。
531                    view! {
532                        <div>
533                            <CodeRunner
534                                source="print('让想法生根')\n".to_string()
535                                language="python".to_string()
536                                instance_id=if detail { 3101 } else { 3100 }
537                                example_output=sample
538                            />
539                        </div>
540                    }
541                        .into_any()
542                } else {
543                    view! {
544                        <div class="showcase-browser-placeholder">
545                            "运行器进入视野后加载编辑器和终端…"
546                        </div>
547                    }
548                        .into_any()
549                }
550            }}
551        </div>
552    }
553}
554
555#[derive(Clone, Copy, PartialEq, Eq)]
556enum SqlPreset {
557    Posts,
558    Comments,
559    Metrics,
560}
561
562fn sample_sql_result(preset: SqlPreset, detail: bool) -> (SqlResult, &'static str, &'static str) {
563    match preset {
564        SqlPreset::Posts => {
565            let columns = if detail {
566                vec![
567                    "id".to_string(),
568                    "title".to_string(),
569                    "slug".to_string(),
570                    "published".to_string(),
571                    "views".to_string(),
572                    "deleted_at".to_string(),
573                ]
574            } else {
575                vec![
576                    "id".to_string(),
577                    "title".to_string(),
578                    "published".to_string(),
579                    "views".to_string(),
580                ]
581            };
582            let mut rows = vec![
583                if detail {
584                    vec![
585                        serde_json::json!(108),
586                        serde_json::json!("让想法生根"),
587                        serde_json::json!("roots-of-thought"),
588                        serde_json::json!(true),
589                        serde_json::json!(1280),
590                        serde_json::Value::Null,
591                    ]
592                } else {
593                    vec![
594                        serde_json::json!(108),
595                        serde_json::json!("让想法生根"),
596                        serde_json::json!(true),
597                        serde_json::json!(1280),
598                    ]
599                },
600                if detail {
601                    vec![
602                        serde_json::json!(107),
603                        serde_json::json!("在时间的缝隙里种下文字"),
604                        serde_json::json!("writing-in-time"),
605                        serde_json::json!(true),
606                        serde_json::json!(856),
607                        serde_json::Value::Null,
608                    ]
609                } else {
610                    vec![
611                        serde_json::json!(107),
612                        serde_json::json!("在时间的缝隙里种下文字"),
613                        serde_json::json!(true),
614                        serde_json::json!(856),
615                    ]
616                },
617                if detail {
618                    vec![
619                        serde_json::json!(106),
620                        serde_json::json!("未命名草稿:微风与山岗"),
621                        serde_json::json!("draft-whispering-wind"),
622                        serde_json::json!(false),
623                        serde_json::json!(42),
624                        serde_json::Value::Null,
625                    ]
626                } else {
627                    vec![
628                        serde_json::json!(106),
629                        serde_json::json!("未命名草稿:微风与山岗"),
630                        serde_json::json!(false),
631                        serde_json::json!(42),
632                    ]
633                },
634            ];
635            if detail {
636                rows.push(vec![
637                    serde_json::json!(105),
638                    serde_json::json!("已归档随笔:初春记事"),
639                    serde_json::json!("archived-early-notes"),
640                    serde_json::json!(false),
641                    serde_json::json!(310),
642                    serde_json::json!("2026-09-01 14:20"),
643                ]);
644            }
645            let query = if detail {
646                "SELECT id, title, slug, published, views, deleted_at FROM posts ORDER BY id DESC LIMIT 4;"
647            } else {
648                "SELECT id, title, published, views FROM posts LIMIT 3;"
649            };
650            let stats = if detail {
651                "4 行 · 12ms"
652            } else {
653                "3 行 · 8ms"
654            };
655            (
656                SqlResult {
657                    columns,
658                    rows,
659                    statement_type: "SELECT".to_string(),
660                    ..Default::default()
661                },
662                query,
663                stats,
664            )
665        }
666        SqlPreset::Comments => {
667            let columns = vec![
668                "id".to_string(),
669                "post_id".to_string(),
670                "author".to_string(),
671                "content".to_string(),
672                "is_approved".to_string(),
673                "created_at".to_string(),
674            ];
675            let rows = vec![
676                vec![
677                    serde_json::json!(34),
678                    serde_json::json!(108),
679                    serde_json::json!("叶子"),
680                    serde_json::json!("风吹过的时候,树叶也在点头。"),
681                    serde_json::json!(true),
682                    serde_json::json!("2026-09-28 09:12"),
683                ],
684                vec![
685                    serde_json::json!(33),
686                    serde_json::json!(108),
687                    serde_json::json!("林中客"),
688                    serde_json::json!("文字很有力量,期待下一篇更新!"),
689                    serde_json::json!(true),
690                    serde_json::json!("2026-09-28 09:25"),
691                ],
692                vec![
693                    serde_json::json!(32),
694                    serde_json::json!(107),
695                    serde_json::json!("待审访客"),
696                    serde_json::json!(
697                        "这是一条待审核留言,用于验证长文本在表格中的截断展示与点击展开详情效果。"
698                    ),
699                    serde_json::json!(false),
700                    serde_json::json!("2026-09-27 22:40"),
701                ],
702            ];
703            (
704                SqlResult {
705                    columns,
706                    rows,
707                    statement_type: "SELECT".to_string(),
708                    ..Default::default()
709                },
710                "SELECT id, post_id, author, content, is_approved, created_at FROM comments ORDER BY id DESC;",
711                "3 行 · 9ms",
712            )
713        }
714        SqlPreset::Metrics => {
715            let columns = vec![
716                "metric".to_string(),
717                "category".to_string(),
718                "value".to_string(),
719                "unit".to_string(),
720                "healthy".to_string(),
721                "updated_at".to_string(),
722            ];
723            let rows = vec![
724                vec![
725                    serde_json::json!("db_pool_active"),
726                    serde_json::json!("database"),
727                    serde_json::json!(8),
728                    serde_json::json!("conns"),
729                    serde_json::json!(true),
730                    serde_json::json!("2026-09-28 09:50"),
731                ],
732                vec![
733                    serde_json::json!("cache_hit_rate"),
734                    serde_json::json!("memory"),
735                    serde_json::json!(94.6),
736                    serde_json::json!("%"),
737                    serde_json::json!(true),
738                    serde_json::json!("2026-09-28 09:50"),
739                ],
740                vec![
741                    serde_json::json!("p99_latency"),
742                    serde_json::json!("query"),
743                    serde_json::json!(18.2),
744                    serde_json::json!("ms"),
745                    serde_json::json!(true),
746                    serde_json::json!("2026-09-28 09:50"),
747                ],
748                vec![
749                    serde_json::json!("disk_usage_warn"),
750                    serde_json::json!("storage"),
751                    serde_json::json!(88.4),
752                    serde_json::json!("%"),
753                    serde_json::json!(false),
754                    serde_json::json!("2026-09-28 09:50"),
755                ],
756            ];
757            (
758                SqlResult {
759                    columns,
760                    rows,
761                    statement_type: "SELECT".to_string(),
762                    ..Default::default()
763                },
764                "SELECT metric, category, value, unit, healthy, updated_at FROM system_metrics;",
765                "4 行 · 15ms",
766            )
767        }
768    }
769}
770#[component]
771fn SqlTableDemo(detail: bool) -> impl IntoView {
772    let preset = RwSignal::new(SqlPreset::Posts);
773    let active = Signal::derive(move || {
774        match preset.get() {
775            SqlPreset::Posts => "posts",
776            SqlPreset::Comments => "comments",
777            SqlPreset::Metrics => "metrics",
778        }
779        .to_string()
780    });
781    let root_class = if detail {
782        "showcase-sql-demo showcase-sql-detail"
783    } else {
784        "showcase-sql-demo"
785    };
786
787    view! {
788        <div class=root_class>
789            {detail
790                .then(|| {
791                    view! {
792                        <div class="showcase-demo-bar">
793                            <SegmentedControl
794                                items=vec![
795                                    SegmentedItem::new("posts".to_string(), "文章"),
796                                    SegmentedItem::new("comments".to_string(), "评论"),
797                                    SegmentedItem::new("metrics".to_string(), "系统指标"),
798                                ]
799                                value=active
800                                on_change=move |value: String| {
801                                    preset
802                                        .set(
803                                            match value.as_str() {
804                                                "comments" => SqlPreset::Comments,
805                                                "metrics" => SqlPreset::Metrics,
806                                                _ => SqlPreset::Posts,
807                                            },
808                                        )
809                                }
810                                aria_label="示例查询"
811                            />
812                            <span>"点击任意行展开完整字段。"</span>
813                        </div>
814                    }
815                })}
816            // 与系统页 SQL 控制台的结果区同一结构;切换示例时整体重建,表格展开状态随之复位。
817            {move || {
818                let preset = preset.get();
819                let (result, query_text, stats_text) = sample_sql_result(preset, detail);
820                let initial_expanded = (detail && preset == SqlPreset::Posts).then_some(0_usize);
821                view! {
822                    <section class="admin-panel sys-section sys-result">
823                        <div class="sys-section-head">
824                            <code class="showcase-sql-query">{query_text}</code>
825                            <span>{stats_text}</span>
826                        </div>
827                        <div>
828                            <SqlResultTable result=result initial_expanded=initial_expanded/>
829                        </div>
830                    </section>
831                }
832            }}
833        </div>
834    }
835}