diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/UiKit.module.css | 14 | ||||
| -rw-r--r-- | src/UiKit.tsx | 294 | ||||
| -rw-r--r-- | src/routes.ts | 8 |
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 }, |
