aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
authorshu <nvcsadjkojf@outlook.com>2026-09-06 17:52:06 +0800
committershu <nvcsadjkojf@outlook.com>2026-09-06 17:52:06 +0800
commita0df82a3e4b42334793a6c3d7139c9f24ee22e0d (patch)
tree5b03f163c37f5fa9d0cf244f15b016b892d604e8 /src
parentfb14fbe0a3ecc801f010a7a429e30ddc57d9f2e4 (diff)
新增 UI Kit 页面并展示 Mantine 常用组件交互
- 添加 /ui-kit 懒加载路由与菜单入口,按类别组织组件示例 - 展示按钮、Alert、Tabs、Menubar、加载动画和加载遮罩 - 展示页面内 Notification 和独立通知容器,支持成功、错误提示与关闭 - 添加 Drawer、Dialog、Modal、Popover、Tooltip 的打开、关闭及操作示例 - 展示可收藏 Card、Badge、DataList 和 Timeline - 添加示例数据表格及分页器,切换页码时更新实际展示记录 - 使用 CSS Modules 管理页面样式,适配移动端抽屉、弹层和表格滚动 验证:pnpm build、pnpm lint、git diff --check 均通过。 浏览器检查通过:标签切换、菜单操作、通知、收藏、分页、加载结束、弹层交互、键盘 Tooltip 及移动端溢出检查;未发现运行时异常。
Diffstat (limited to 'src')
-rw-r--r--src/UiKit.module.css14
-rw-r--r--src/UiKit.tsx294
-rw-r--r--src/routes.ts8
3 files changed, 315 insertions, 1 deletions
diff --git a/src/UiKit.module.css b/src/UiKit.module.css
new file mode 100644
index 0000000..a0df9f0
--- /dev/null
+++ b/src/UiKit.module.css
@@ -0,0 +1,14 @@
+.section {
+ min-width: 0;
+}
+
+.loadingExample {
+ position: relative;
+ padding: var(--mantine-spacing-md);
+ border: 1px solid var(--mantine-color-default-border);
+ border-radius: var(--mantine-radius-default);
+}
+
+.dialog {
+ max-width: calc(100vw - 2 * var(--mantine-spacing-md));
+}
diff --git a/src/UiKit.tsx b/src/UiKit.tsx
new file mode 100644
index 0000000..8d89788
--- /dev/null
+++ b/src/UiKit.tsx
@@ -0,0 +1,294 @@
+import { useState } from 'react'
+import type { ReactNode } from 'react'
+import {
+ Alert, Badge, Box, Button, Card, DataList, Dialog, Drawer, Group,
+ Loader, LoadingOverlay, Menu, Menubar, Modal, Notification, Pagination,
+ Paper, Popover, SimpleGrid, Skeleton, Stack, Table, Tabs, Text,
+ TextInput, Timeline, Title, Tooltip, useMatches,
+} from '@mantine/core'
+import { useTimeout } from '@mantine/hooks'
+import { createNotificationsStore, Notifications, notifications } from '@mantine/notifications'
+import { CheckIcon, HeartIcon, InfoIcon, PlusIcon, WarningIcon } from '@phosphor-icons/react'
+import '@mantine/notifications/styles.css'
+import classes from './UiKit.module.css'
+
+function Section({ title, children }: { title: string; children: ReactNode }) {
+ return (
+ <Paper component="section" withBorder p="md" className={classes.section}>
+ <Stack gap="md">
+ <Title order={2} size="h4">{title}</Title>
+ {children}
+ </Stack>
+ </Paper>
+ )
+}
+
+const records = Array.from({ length: 13 }, (_, index) => ({
+ id: index + 1,
+ name: `示例项目 ${String(index + 1).padStart(2, '0')}`,
+ owner: ['林晨', '陈宁', '李明'][index % 3],
+ status: ['进行中', '已完成', '待开始'][index % 3],
+}))
+const statusColors: Record<string, string> = { 进行中: 'blue', 已完成: 'green', 待开始: 'gray' }
+const pageSize = 5
+
+export default function UiKit() {
+ const [store] = useState(createNotificationsStore)
+ const [overlay, setOverlay] = useState<'drawer' | 'dialog' | 'modal' | null>(null)
+ const [popoverOpened, setPopoverOpened] = useState(false)
+ const [showAlert, setShowAlert] = useState(true)
+ const [showNotification, setShowNotification] = useState(true)
+ const [loading, setLoading] = useState(false)
+ const [liked, setLiked] = useState(false)
+ const [showStatus, setShowStatus] = useState(true)
+ const [action, setAction] = useState('尚未执行操作')
+ const [page, setPage] = useState(1)
+ const [draftName, setDraftName] = useState('我的示例项目')
+ const drawerSize = useMatches({ base: '100%', sm: 'md' })
+ const { start: finishLoading } = useTimeout(() => setLoading(false), 1500)
+ const visibleRecords = records.slice((page - 1) * pageSize, page * pageSize)
+
+ function notify(message: string, color = 'blue') {
+ notifications.show({
+ title: '示例通知', message, color,
+ closeButtonProps: { 'aria-label': '关闭浮动通知' },
+ }, store)
+ }
+
+ function runAction(message: string) {
+ setAction(message)
+ notify(message)
+ }
+
+ return (
+ <Stack gap="lg">
+ <Notifications store={store} position="top-right" limit={3} />
+ <div>
+ <Title order={1}>UI Kit</Title>
+ <Text c="dimmed">常用界面组件与交互示例,所有操作仅作用于当前页面。</Text>
+ </div>
+
+ <Section title="按钮 Button">
+ <Group>
+ <Button leftSection={<PlusIcon aria-hidden="true" />} onClick={() => runAction('已点击主要按钮')}>主要按钮</Button>
+ <Button variant="light" onClick={() => runAction('已点击浅色按钮')}>浅色按钮</Button>
+ <Button variant="outline" onClick={() => runAction('已点击描边按钮')}>描边按钮</Button>
+ <Button variant="subtle" onClick={() => runAction('已点击文字按钮')}>文字按钮</Button>
+ <Button disabled>禁用按钮</Button>
+ <Button loading>加载中</Button>
+ </Group>
+ <Text size="sm" c="dimmed" role="status">{action}</Text>
+ </Section>
+
+ <SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+ <Section title="提示 Alert">
+ <Alert title="提示信息" icon={<InfoIcon />} color="blue">可以在这里展示操作说明。</Alert>
+ <Alert title="操作成功" icon={<CheckIcon />} color="green">示例内容已保存。</Alert>
+ {showAlert ? (
+ <Alert title="请留意" icon={<WarningIcon />} color="orange" withCloseButton
+ closeButtonLabel="关闭警告" onClose={() => setShowAlert(false)}>
+ 这是一条可以关闭的警告提示。
+ </Alert>
+ ) : <Button variant="light" onClick={() => setShowAlert(true)}>重新显示警告</Button>}
+ </Section>
+
+ <Section title="通知 Notification">
+ {showNotification ? (
+ <Notification title="收到新消息" icon={<CheckIcon />} color="teal"
+ onClose={() => setShowNotification(false)} closeButtonProps={{ 'aria-label': '关闭示例通知' }}>
+ 这是一条嵌入页面的通知。
+ </Notification>
+ ) : <Button variant="light" onClick={() => setShowNotification(true)}>恢复示例通知</Button>}
+ <Group>
+ <Button onClick={() => notify('欢迎体验浮动通知。')}>显示通知</Button>
+ <Button color="green" variant="light" onClick={() => notify('操作成功完成。', 'green')}>成功通知</Button>
+ <Button color="red" variant="light" onClick={() => notify('这是错误通知示例,请重试。', 'red')}>错误通知</Button>
+ </Group>
+ </Section>
+ </SimpleGrid>
+
+ <Section title="标签页 Tabs">
+ <Tabs defaultValue="overview">
+ <Tabs.List aria-label="示例标签页">
+ <Tabs.Tab value="overview">概览</Tabs.Tab>
+ <Tabs.Tab value="activity">动态</Tabs.Tab>
+ <Tabs.Tab value="settings">设置</Tabs.Tab>
+ <Tabs.Tab value="disabled" disabled>不可用</Tabs.Tab>
+ </Tabs.List>
+ <Tabs.Panel value="overview" pt="md">这是概览面板,可以放置摘要信息。</Tabs.Panel>
+ <Tabs.Panel value="activity" pt="md">暂无新动态,所有示例数据均为本地数据。</Tabs.Panel>
+ <Tabs.Panel value="settings" pt="md">
+ <Button variant="light" onClick={() => notify('示例设置已恢复。')}>恢复示例设置</Button>
+ </Tabs.Panel>
+ </Tabs>
+ </Section>
+
+ <Section title="菜单栏 Menubar">
+ <Menubar aria-label="示例菜单栏">
+ <Menubar.Menu>
+ <Menubar.Target>文件</Menubar.Target>
+ <Menubar.Dropdown>
+ <Menu.Item onClick={() => runAction('已新建示例文件')}>新建文件</Menu.Item>
+ <Menu.Item onClick={() => runAction('已保存示例文件')}>保存文件</Menu.Item>
+ <Menu.Divider />
+ <Menu.Item disabled>导出(不可用)</Menu.Item>
+ </Menubar.Dropdown>
+ </Menubar.Menu>
+ <Menubar.Menu>
+ <Menubar.Target>视图</Menubar.Target>
+ <Menubar.Dropdown>
+ <Menu.CheckboxItem checked={showStatus} onChange={setShowStatus}>显示状态栏</Menu.CheckboxItem>
+ </Menubar.Dropdown>
+ </Menubar.Menu>
+ <Menubar.Menu>
+ <Menubar.Target>帮助</Menubar.Target>
+ <Menubar.Dropdown>
+ <Menu.Item onClick={() => notify('可使用方向键切换菜单,按 Escape 关闭。')}>键盘操作说明</Menu.Item>
+ </Menubar.Dropdown>
+ </Menubar.Menu>
+ </Menubar>
+ {showStatus && <Text size="sm" c="dimmed">状态栏:准备就绪</Text>}
+ </Section>
+
+ <Section title="加载状态 Loading">
+ <Group gap="xl">
+ <Loader type="oval" aria-label="圆环加载动画" />
+ <Loader type="dots" aria-label="圆点加载动画" />
+ <Loader type="bars" aria-label="条形加载动画" />
+ <Button variant="light" disabled={loading} onClick={() => { setLoading(true); finishLoading() }}>
+ 模拟加载
+ </Button>
+ </Group>
+ <Box className={classes.loadingExample} aria-busy={loading}>
+ <LoadingOverlay visible={loading} zIndex={10} loaderProps={{ 'aria-label': '示例内容加载中' }} />
+ <Stack gap="xs" inert={loading}>
+ <Text size="sm">点击按钮展示加载遮罩,约 1.5 秒后恢复。</Text>
+ <Skeleton height="var(--mantine-spacing-md)" />
+ <Skeleton height="var(--mantine-spacing-md)" width="70%" />
+ </Stack>
+ </Box>
+ </Section>
+
+ <Section title="弹层 Drawer / Dialog / Modal / Popover / Tooltip">
+ <Group>
+ <Button onClick={() => setOverlay('drawer')}>打开 Drawer</Button>
+ <Button variant="light" onClick={() => setOverlay('dialog')}>打开 Dialog</Button>
+ <Button variant="outline" onClick={() => setOverlay('modal')}>打开 Modal</Button>
+ <Popover opened={popoverOpened} onChange={setPopoverOpened} withArrow trapFocus width={260}>
+ <Popover.Target>
+ <Button variant="default" onClick={() => setPopoverOpened((value) => !value)}>打开 Popover</Button>
+ </Popover.Target>
+ <Popover.Dropdown>
+ <Stack gap="xs">
+ <Text fw={600}>快捷提示</Text>
+ <Text size="sm">气泡内可以放置文字与操作按钮。</Text>
+ <Button size="xs" onClick={() => setPopoverOpened(false)}>知道了</Button>
+ </Stack>
+ </Popover.Dropdown>
+ </Popover>
+ <Tooltip label="悬停或键盘聚焦时显示提示" withArrow events={{ hover: true, focus: true, touch: true }}>
+ <Button variant="subtle">Tooltip 提示</Button>
+ </Tooltip>
+ </Group>
+ </Section>
+
+ <SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+ <Section title="卡片 Card 与徽章 Badge">
+ <Card withBorder padding="md">
+ <Stack gap="sm">
+ <Group justify="space-between">
+ <Text fw={600}>组件示例项目</Text>
+ <Badge color="green">进行中</Badge>
+ </Group>
+ <Text size="sm" c="dimmed">卡片组合标题、说明、状态与操作,适合展示独立内容。</Text>
+ <Group>
+ <Badge variant="light">React</Badge>
+ <Badge variant="outline">Mantine</Badge>
+ <Badge variant="dot" color="orange">示例</Badge>
+ </Group>
+ <Button variant={liked ? 'filled' : 'light'} aria-pressed={liked}
+ leftSection={<HeartIcon weight={liked ? 'fill' : 'regular'} aria-hidden="true" />}
+ onClick={() => setLiked((value) => !value)}>
+ {liked ? '已收藏' : '收藏卡片'}
+ </Button>
+ </Stack>
+ </Card>
+ </Section>
+
+ <Section title="数据列表 DataList">
+ <DataList withDivider>
+ <DataList.Item><DataList.ItemLabel>项目</DataList.ItemLabel><DataList.ItemValue>Template React</DataList.ItemValue></DataList.Item>
+ <DataList.Item><DataList.ItemLabel>用途</DataList.ItemLabel><DataList.ItemValue>界面组件演示</DataList.ItemValue></DataList.Item>
+ <DataList.Item><DataList.ItemLabel>状态</DataList.ItemLabel><DataList.ItemValue><Badge color="green" variant="light">可用</Badge></DataList.ItemValue></DataList.Item>
+ <DataList.Item><DataList.ItemLabel>维护者</DataList.ItemLabel><DataList.ItemValue>示例团队</DataList.ItemValue></DataList.Item>
+ </DataList>
+ </Section>
+ </SimpleGrid>
+
+ <Section title="时间线 Timeline">
+ <Timeline active={1}>
+ <Timeline.Item title="创建项目"><Text size="sm" c="dimmed">项目初始化完成。</Text></Timeline.Item>
+ <Timeline.Item title="开发组件"><Text size="sm" c="dimmed">正在搭建布局和交互示例。</Text></Timeline.Item>
+ <Timeline.Item title="完成验收"><Text size="sm" c="dimmed">等待检查和发布。</Text></Timeline.Item>
+ </Timeline>
+ </Section>
+
+ <Section title="表格 Table 与分页器 Pagination">
+ <Table.ScrollContainer minWidth={500}>
+ <Table striped highlightOnHover withTableBorder>
+ <Table.Caption>项目列表(示例数据)</Table.Caption>
+ <Table.Thead><Table.Tr>
+ <Table.Th scope="col">编号</Table.Th><Table.Th scope="col">项目名称</Table.Th>
+ <Table.Th scope="col">负责人</Table.Th><Table.Th scope="col">状态</Table.Th>
+ </Table.Tr></Table.Thead>
+ <Table.Tbody>
+ {visibleRecords.map((record) => (
+ <Table.Tr key={record.id}>
+ <Table.Td>{record.id}</Table.Td><Table.Td>{record.name}</Table.Td>
+ <Table.Td>{record.owner}</Table.Td>
+ <Table.Td><Badge color={statusColors[record.status]} variant="light">{record.status}</Badge></Table.Td>
+ </Table.Tr>
+ ))}
+ </Table.Tbody>
+ </Table>
+ </Table.ScrollContainer>
+ <Group justify="space-between">
+ <Text size="sm" c="dimmed" aria-live="polite">
+ 共 {records.length} 条,第 {(page - 1) * pageSize + 1}–{Math.min(page * pageSize, records.length)} 条
+ </Text>
+ <Pagination total={Math.ceil(records.length / pageSize)} value={page} onChange={setPage}
+ getItemProps={(value) => ({ 'aria-label': `第 ${value} 页` })}
+ getControlProps={(control) => ({ 'aria-label': ({ first: '首页', previous: '上一页', next: '下一页', last: '末页' })[control] })} />
+ </Group>
+ </Section>
+
+ <Drawer opened={overlay === 'drawer'} onClose={() => setOverlay(null)} title="Drawer 抽屉示例"
+ position="right" size={drawerSize} closeButtonProps={{ 'aria-label': '关闭 Drawer' }}>
+ <Stack>
+ <Text>抽屉适合展示详情或辅助操作。</Text>
+ <Button onClick={() => setOverlay(null)}>关闭抽屉</Button>
+ </Stack>
+ </Drawer>
+
+ <Dialog opened={overlay === 'dialog'} onClose={() => setOverlay(null)} withCloseButton
+ role="dialog" aria-labelledby="ui-kit-dialog-title" className={classes.dialog}>
+ <Stack gap="xs">
+ <Text id="ui-kit-dialog-title" fw={600}>Dialog 提示框</Text>
+ <Text size="sm">无需遮罩的轻量提示,你仍可以操作页面。</Text>
+ <Button size="xs" onClick={() => setOverlay(null)}>关闭提示</Button>
+ </Stack>
+ </Dialog>
+
+ <Modal opened={overlay === 'modal'} onClose={() => setOverlay(null)} title="Modal 弹窗示例"
+ centered closeButtonProps={{ 'aria-label': '关闭 Modal' }}>
+ <Stack>
+ <TextInput label="项目名称" value={draftName} onChange={(event) => setDraftName(event.currentTarget.value)} data-autofocus />
+ <Group justify="flex-end">
+ <Button variant="default" onClick={() => setOverlay(null)}>取消</Button>
+ <Button disabled={!draftName.trim()} onClick={() => { setOverlay(null); notify(`已保存:${draftName.trim()}`, 'green') }}>保存</Button>
+ </Group>
+ </Stack>
+ </Modal>
+ </Stack>
+ )
+}
diff --git a/src/routes.ts b/src/routes.ts
index f0a0ef8..92eced8 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -1,4 +1,4 @@
-import { ClipboardTextIcon, HouseIcon, InfoIcon, UserIcon } from '@phosphor-icons/react'
+import { ClipboardTextIcon, HouseIcon, InfoIcon, SquaresFourIcon, UserIcon } from '@phosphor-icons/react'
import type { Icon } from '@phosphor-icons/react'
import type { RouteObject } from 'react-router'
import App from './App.tsx'
@@ -24,6 +24,12 @@ export const pageRoutes = [
handle: { title: '表单示例', icon: ClipboardTextIcon },
},
{
+ id: 'ui-kit',
+ path: '/ui-kit',
+ lazy: async () => ({ Component: (await import('./UiKit.tsx')).default }),
+ handle: { title: 'UI Kit', icon: SquaresFourIcon },
+ },
+ {
id: 'about',
path: '/about',
handle: { title: '关于', icon: InfoIcon },