aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src/UiKit.tsx
blob: 8d89788399e2030e775e356c8521255581892359 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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>
  )
}