feat: stash-ui 0.1.0(40 个组件 + 文档站 + 主题机制)
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Drawer,
|
||||
Grid,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
ItemBadge,
|
||||
Modal,
|
||||
Notice,
|
||||
Pagination,
|
||||
Paragraph,
|
||||
ScrollTop,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space,
|
||||
Spinner,
|
||||
Switch,
|
||||
Tag,
|
||||
Text,
|
||||
Title,
|
||||
Toaster,
|
||||
toast,
|
||||
} from '../../src'
|
||||
import styles from './demo.module.css'
|
||||
|
||||
/**
|
||||
* 拼装示例:用组件库搭一个完整的三栏内容页。
|
||||
*
|
||||
* 只用 `@stash/ui` 的组件 + 这一页自己的布局样式,
|
||||
* 没有引入任何业务组件;浅色/暗色由 `<html data-scheme>` 切换。
|
||||
*/
|
||||
|
||||
const NAV = ['工作台', '常用推荐', '推荐电影', 'CES 现场报道']
|
||||
|
||||
const HOT_POSTS = [
|
||||
'iPhone 17e 最全汇总:最便宜的苹果 AI 手机',
|
||||
'二十年磨一剑,微软量子芯片碾压全球超算',
|
||||
'做饭将被 AI 彻底改变!AI 厨电成新风口',
|
||||
'对未来讲话?从 AWE 2024 看未来趋势',
|
||||
]
|
||||
|
||||
const POST_TAGS = ['未分类', '苹果新品', '闪电接口']
|
||||
|
||||
const GALLERY = [1, 2, 3, 4, 5, 6]
|
||||
|
||||
function useTheme() {
|
||||
// 支持 ?scheme=dark 直接进深色,方便截图和分享
|
||||
const [scheme, setScheme] = useState<'light' | 'dark'>(() =>
|
||||
new URLSearchParams(window.location.search).get('scheme') === 'dark' ? 'dark' : 'light',
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.scheme = scheme
|
||||
}, [scheme])
|
||||
|
||||
return {
|
||||
scheme,
|
||||
toggle: () => setScheme((current) => (current === 'light' ? 'dark' : 'light')),
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新时的占位卡片:结构和真实卡片一致,避免加载完成后跳动 */
|
||||
function SkeletonPost() {
|
||||
return (
|
||||
<Card padding="lg" aria-busy="true">
|
||||
<Space gap="sm">
|
||||
<Skeleton variant="circle" width={36} height={36} />
|
||||
<Space direction="column" gap="xs" align="start">
|
||||
<Skeleton width={96} />
|
||||
<Skeleton width={64} />
|
||||
</Space>
|
||||
</Space>
|
||||
<div style={{ marginTop: 'var(--su-space-4)' }}>
|
||||
<Skeleton lines={2} />
|
||||
</div>
|
||||
<div style={{ marginTop: 'var(--su-space-4)' }}>
|
||||
<Skeleton variant="rect" height={220} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function Page() {
|
||||
const { scheme, toggle } = useTheme()
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [removed, setRemoved] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [autoSync, setAutoSync] = useState(true)
|
||||
const [showCover, setShowCover] = useState(true)
|
||||
const [notify, setNotify] = useState(false)
|
||||
const [sort, setSort] = useState('new')
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
|
||||
const refreshFeed = () => {
|
||||
setLoading(true)
|
||||
window.setTimeout(() => setLoading(false), 1200)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => setScrolled(window.scrollY > 40)
|
||||
window.addEventListener('scroll', sync, { passive: true })
|
||||
return () => window.removeEventListener('scroll', sync)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.glow} aria-hidden="true" />
|
||||
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerInner}>
|
||||
<a className={styles.brand} href="/demo.html">
|
||||
<span className={styles.brandMark}>S</span>
|
||||
<span className={styles.brandName}>STASH</span>
|
||||
</a>
|
||||
|
||||
<nav className={styles.nav} aria-label="主导航">
|
||||
{NAV.map((item, index) => (
|
||||
<a
|
||||
key={item}
|
||||
href="#top"
|
||||
className={`${styles.navLink}${index === 0 ? ` ${styles.navLinkActive}` : ''}`}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className={styles.headerActions}>
|
||||
<span className={styles.onlyMobile}>
|
||||
<IconButton
|
||||
label="打开导航"
|
||||
icon={<Icon name="menu_line" />}
|
||||
onClick={() => setNavOpen(true)}
|
||||
/>
|
||||
</span>
|
||||
<IconButton label="搜索" icon={<Icon name="search_3_line" />} />
|
||||
<IconButton
|
||||
label="刷新列表"
|
||||
icon={<Icon name="refresh_3_line" />}
|
||||
spinning={loading}
|
||||
onClick={refreshFeed}
|
||||
/>
|
||||
<IconButton
|
||||
label={scheme === 'light' ? '切换到深色' : '切换到浅色'}
|
||||
icon={<Icon name={scheme === 'light' ? 'moon_line' : 'sun_line'} />}
|
||||
onClick={toggle}
|
||||
/>
|
||||
<IconButton
|
||||
label="设置"
|
||||
icon={<Icon name="settings_1_line" />}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
/>
|
||||
<IconButton label="账号" icon={<Icon name="user_3_line" />} variant="soft" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="top" className={styles.layout}>
|
||||
<aside className={styles.rail}>
|
||||
<Card padding="lg" className={styles.profile}>
|
||||
<Avatar size="lg" fallback="S" verified />
|
||||
<h1 className={styles.profileName}>Stash 工作台</h1>
|
||||
<Tag shape="rounded">管理员</Tag>
|
||||
<p className={styles.profileBio}>
|
||||
本地静态页面托管工具,内容库驱动分类、分组与条目;这一页全部由基础组件拼成。
|
||||
</p>
|
||||
<span className={styles.profileMeta}>IP 归属地:局域网</span>
|
||||
</Card>
|
||||
|
||||
<Grid columns={2} gap="sm">
|
||||
<Card padding="sm" hoverable>
|
||||
<span className={styles.statLabel}>文章</span>
|
||||
<strong className={styles.statValue}>16</strong>
|
||||
</Card>
|
||||
<Card padding="sm" hoverable>
|
||||
<span className={styles.statLabel}>评论</span>
|
||||
<strong className={styles.statValue}>0</strong>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Card padding="sm">
|
||||
<h2 className={styles.cardTitle}>🔗 社交媒体</h2>
|
||||
<Space gap="md">
|
||||
<IconButton label="链接" icon={<Icon name="link_line" />} size="xs" variant="soft" />
|
||||
<IconButton label="分享" icon={<Icon name="share_3_line" />} size="xs" variant="soft" />
|
||||
<IconButton label="浏览" icon={<Icon name="eye_line" />} size="xs" variant="soft" />
|
||||
<IconButton label="账号" icon={<Icon name="user_3_line" />} size="xs" variant="soft" />
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card padding="md">
|
||||
<h2 className={styles.cardTitle}>⚙️ 偏好</h2>
|
||||
<Space direction="column" gap="md" align="stretch">
|
||||
<Space justify="between">
|
||||
<Text>自动同步</Text>
|
||||
<Switch checked={autoSync} onChange={setAutoSync} label="自动同步" />
|
||||
</Space>
|
||||
<Space justify="between">
|
||||
<Text>显示封面</Text>
|
||||
<Switch checked={showCover} onChange={setShowCover} label="显示封面" />
|
||||
</Space>
|
||||
<Checkbox checked={notify} onChange={setNotify} label="每周摘要提醒" />
|
||||
{autoSync && (
|
||||
<Space gap="sm">
|
||||
<Spinner size="sm" label="同步中" />
|
||||
<Text size="xs" tone="muted">
|
||||
正在同步内容库…
|
||||
</Text>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
<main className={styles.feed}>
|
||||
{removed && (
|
||||
<Notice
|
||||
title="已删除《本地内容库改版》(示例)"
|
||||
action={{ label: '撤销', onClick: () => setRemoved(false) }}
|
||||
>
|
||||
这里只是演示确认流程,没有真的删数据。
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
<Breadcrumb
|
||||
items={[{ label: '首页', href: '#top' }, { label: '工作台' }, { label: '常用推荐' }]}
|
||||
/>
|
||||
|
||||
<Card padding="sm">
|
||||
<Space justify="between" wrap>
|
||||
<Space gap="sm" wrap>
|
||||
<Tag variant="solid" tone="primary" icon={<Icon name="search_3_line" />}>
|
||||
全部
|
||||
</Tag>
|
||||
<Tag>🛸 前沿科技</Tag>
|
||||
<Tag>🏠 建筑力学</Tag>
|
||||
<Tag>玩物志趣</Tag>
|
||||
</Space>
|
||||
<Space gap="sm" wrap>
|
||||
<Select
|
||||
value={sort}
|
||||
onChange={setSort}
|
||||
options={[
|
||||
{ value: 'new', label: '最近更新' },
|
||||
{ value: 'hot', label: '最热门' },
|
||||
{ value: 'name', label: '按名称' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
icon="search_3_line"
|
||||
placeholder="筛选内容…"
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
onClear={() => setKeyword('')}
|
||||
clearable
|
||||
style={{ width: 220, maxWidth: '100%' }}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{loading ? (
|
||||
<>
|
||||
<SkeletonPost />
|
||||
<SkeletonPost />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Card padding="lg">
|
||||
<Space gap="sm">
|
||||
<Avatar size="md" fallback="S" verified />
|
||||
<Space direction="column" gap="xs" align="start">
|
||||
<Text strong>Stash</Text>
|
||||
<Text size="xs" tone="muted">
|
||||
2 年前 · 局域网
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Title level={3} className={styles.postTitle}>
|
||||
<ItemBadge tone="sticky">置顶</ItemBadge>
|
||||
本地内容库改版:分区、分组与条目的三层结构
|
||||
</Title>
|
||||
|
||||
<Paragraph lines={2}>
|
||||
侧栏由内容树派生,首页卡片由 section_block 决定展示哪个节点;
|
||||
条目只存 node_key,所属分区由它推导,改编码时自动跟随。
|
||||
</Paragraph>
|
||||
|
||||
{showCover && (
|
||||
<Grid columns={2} gap="sm" className={styles.media}>
|
||||
<span className={`${styles.tile} ${styles.tileA}`} />
|
||||
<span className={`${styles.tile} ${styles.tileB}`} />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Divider spacing="sm" />
|
||||
|
||||
<Space justify="between" wrap>
|
||||
<Space gap="sm">
|
||||
{POST_TAGS.map((tag, index) => (
|
||||
<Tag key={tag} variant="solid" tone={index === 0 ? 'primary' : 'danger'}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<Space gap="md">
|
||||
<span className={styles.action}>
|
||||
<Icon name="eye_line" />
|
||||
1,599
|
||||
</span>
|
||||
<span className={styles.action}>
|
||||
<Icon name="thumb_up_2_line" />
|
||||
22
|
||||
</span>
|
||||
<span className={styles.action}>
|
||||
<Icon name="share_3_line" />
|
||||
6
|
||||
</span>
|
||||
<IconButton
|
||||
size="xs"
|
||||
label="删除这篇内容"
|
||||
icon={<Icon name="delete_2_line" />}
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card padding="lg">
|
||||
<Space gap="sm">
|
||||
<Avatar size="md" fallback="S" />
|
||||
<Space direction="column" gap="xs" align="start">
|
||||
<Text strong>Stash</Text>
|
||||
<Text size="xs" tone="muted">
|
||||
3 天前 · 局域网
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Title level={3} className={styles.postTitle}>
|
||||
<ItemBadge tone="paid">付费</ItemBadge>
|
||||
主题与令牌:为什么把颜色收进 CSS 变量
|
||||
</Title>
|
||||
|
||||
<Paragraph lines={2}>
|
||||
组件只消费语义令牌(bg-card / text-main / border),
|
||||
换主题时只覆盖变量——暗色主题就是这么来的。
|
||||
</Paragraph>
|
||||
|
||||
<Notice
|
||||
title="付费内容"
|
||||
action={{ label: '登录后查看', onClick: () => undefined }}
|
||||
>
|
||||
本内容为付费内容,登录后可阅读全文。
|
||||
</Notice>
|
||||
|
||||
<Divider spacing="sm" />
|
||||
|
||||
<Space justify="between" wrap>
|
||||
<Space gap="sm">
|
||||
<Tag>设计系统</Tag>
|
||||
<Tag tone="success">已发布</Tag>
|
||||
</Space>
|
||||
<Space gap="md">
|
||||
<span className={styles.action}>
|
||||
<Icon name="eye_line" />
|
||||
842
|
||||
</span>
|
||||
<span className={styles.action}>
|
||||
<Icon name="thumb_up_2_line" />
|
||||
31
|
||||
</span>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Card padding="sm">
|
||||
<Space justify="between" wrap>
|
||||
<Space gap="sm">
|
||||
<Text size="sm" tone="muted">
|
||||
共 16 篇内容
|
||||
</Text>
|
||||
<Tag tone="neutral">第 {page} / 3 页</Tag>
|
||||
</Space>
|
||||
<Pagination page={page} total={16} pageSize={6} onChange={setPage} />
|
||||
</Space>
|
||||
</Card>
|
||||
</main>
|
||||
|
||||
<aside className={`${styles.rail} ${styles.railRight}`}>
|
||||
<Card padding="md">
|
||||
<h2 className={styles.cardTitle}>
|
||||
🔥 热门文章
|
||||
<Badge count={HOT_POSTS.length} tone="neutral" />
|
||||
</h2>
|
||||
|
||||
<ul className={styles.hotList}>
|
||||
{HOT_POSTS.map((title, index) => (
|
||||
<li key={title}>
|
||||
<a
|
||||
className={styles.hotItem}
|
||||
href="#top"
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<span className={styles.hotTitle}>{title}</span>
|
||||
<ItemBadge tone="hot">HOT</ItemBadge>
|
||||
</a>
|
||||
{index < HOT_POSTS.length - 1 && <Divider spacing="sm" />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
<Card padding="md">
|
||||
<h2 className={styles.cardTitle}>🖼 图集</h2>
|
||||
<Grid columns={3} gap="sm">
|
||||
{GALLERY.map((item) => (
|
||||
<span key={item} className={`${styles.thumb} ${styles[`thumb${item}`]}`} />
|
||||
))}
|
||||
</Grid>
|
||||
</Card>
|
||||
|
||||
<Notice title="这一页怎么来的">
|
||||
全部由 Card / Tag / ItemBadge / Avatar / Grid / Divider / Button / Input / Modal /
|
||||
Pagination 拼装,没有任何业务组件;右上角可以切换深浅色。
|
||||
</Notice>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={confirmOpen}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
title="删除这篇内容?"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setConfirmOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setConfirmOpen(false)
|
||||
setRemoved(true)
|
||||
toast.success('已删除《本地内容库改版》')
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
删除后不可恢复,占位数据会被一起清掉。
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
title="设置"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setDrawerOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDrawerOpen(false)
|
||||
toast.success('设置已保存')
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Space direction="column" gap="lg" align="stretch">
|
||||
<Space justify="between">
|
||||
<Text>自动同步</Text>
|
||||
<Switch checked={autoSync} onChange={setAutoSync} label="自动同步" />
|
||||
</Space>
|
||||
<Space justify="between">
|
||||
<Text>显示封面</Text>
|
||||
<Switch checked={showCover} onChange={setShowCover} label="显示封面" />
|
||||
</Space>
|
||||
<Checkbox checked={notify} onChange={setNotify} label="每周摘要提醒" />
|
||||
<Space direction="column" gap="sm" align="stretch">
|
||||
<Text size="sm" tone="sub">
|
||||
默认排序
|
||||
</Text>
|
||||
<Select
|
||||
value={sort}
|
||||
onChange={setSort}
|
||||
options={[
|
||||
{ value: 'new', label: '最近更新' },
|
||||
{ value: 'hot', label: '最热门' },
|
||||
{ value: 'name', label: '按名称' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
</Drawer>
|
||||
|
||||
<Drawer open={navOpen} onClose={() => setNavOpen(false)} placement="left" width={280} title="导航">
|
||||
<Space direction="column" gap="sm" align="stretch">
|
||||
{NAV.map((item, index) => (
|
||||
<a
|
||||
key={item}
|
||||
href="#top"
|
||||
className={`${styles.drawerNav}${index === 0 ? ` ${styles.drawerNavActive}` : ''}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
setNavOpen(false)
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
<Divider spacing="sm" />
|
||||
<Text size="sm" tone="muted">
|
||||
窄屏下顶部导航会收进这个抽屉里;桌面端直接显示在页头。
|
||||
</Text>
|
||||
</Space>
|
||||
</Drawer>
|
||||
|
||||
<Toaster />
|
||||
|
||||
<ScrollTop visible={scrolled} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user