@deepstorm/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-schema.json +1 -1
- package/dist/registry.json +65 -5
- package/dist/skills/reef-gen-frontend/variants/react/steps.md +31 -0
- package/dist/skills/reef-gen-frontend/variants/vue/steps.md +34 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/examples/testing.md +283 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/quick-reference.md +152 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/examples/testing.md +457 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/quick-reference.md +186 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/examples/ui-components.md +257 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/quick-reference.md +166 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/examples/ui-components.md +353 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/quick-reference.md +267 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/code-wrapping.md +124 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/component-types-pipes.md +195 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/entity-types.md +127 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/forms-layer.md +251 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/service-routing.md +226 -0
- package/dist/skills/reef-style-frontend/variants/react/quick-reference.md +118 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/code-wrapping.md +162 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/component-types-pipes.md +270 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/entity-types.md +145 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/forms-layer.md +231 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/service-routing.md +224 -0
- package/dist/skills/reef-style-frontend/variants/vue/quick-reference.md +145 -0
- package/package.json +3 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Ant Design 组件代码示例
|
|
2
|
+
|
|
3
|
+
## 完整数据表格(搜索 + 分页 + 操作列)
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
7
|
+
import { Table, Button, Space, Input, Tag, Modal, message } from 'antd'
|
|
8
|
+
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
|
9
|
+
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'
|
|
10
|
+
import type { User, PageResponse } from '../types/user'
|
|
11
|
+
import { userService } from '../services/userService'
|
|
12
|
+
|
|
13
|
+
const UserManagement: React.FC = () => {
|
|
14
|
+
const [users, setUsers] = useState<User[]>([])
|
|
15
|
+
const [loading, setLoading] = useState(false)
|
|
16
|
+
const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 })
|
|
17
|
+
const [search, setSearch] = useState('')
|
|
18
|
+
|
|
19
|
+
const fetchUsers = async (page: number, size: number, keyword?: string) => {
|
|
20
|
+
setLoading(true)
|
|
21
|
+
try {
|
|
22
|
+
const data: PageResponse<User> = await userService.list({ page, size, search: keyword })
|
|
23
|
+
setUsers(data.content)
|
|
24
|
+
setPagination(prev => ({ ...prev, current: data.number, total: data.totalElements }))
|
|
25
|
+
} catch {
|
|
26
|
+
message.error('加载用户列表失败')
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
fetchUsers(pagination.current, pagination.pageSize, search)
|
|
34
|
+
}, [pagination.current, pagination.pageSize])
|
|
35
|
+
|
|
36
|
+
const handleSearch = (value: string) => {
|
|
37
|
+
setSearch(value)
|
|
38
|
+
setPagination(prev => ({ ...prev, current: 1 }))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleDelete = (id: string) => {
|
|
42
|
+
Modal.confirm({
|
|
43
|
+
title: '确认删除',
|
|
44
|
+
content: '删除后不可恢复,确定继续?',
|
|
45
|
+
okText: '删除',
|
|
46
|
+
okType: 'danger',
|
|
47
|
+
onOk: async () => {
|
|
48
|
+
await userService.delete(id)
|
|
49
|
+
message.success('删除成功')
|
|
50
|
+
fetchUsers(pagination.current, pagination.pageSize, search)
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const columns: ColumnsType<User> = [
|
|
56
|
+
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
|
57
|
+
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
|
58
|
+
{
|
|
59
|
+
title: '角色',
|
|
60
|
+
dataIndex: 'role',
|
|
61
|
+
key: 'role',
|
|
62
|
+
render: (role: string) => {
|
|
63
|
+
const color = role === 'ADMIN' ? 'red' : role === 'EDITOR' ? 'blue' : 'default'
|
|
64
|
+
return <Tag color={color}>{role}</Tag>
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: '状态',
|
|
69
|
+
dataIndex: 'active',
|
|
70
|
+
key: 'active',
|
|
71
|
+
render: (active: boolean) => (
|
|
72
|
+
<Tag color={active ? 'green' : 'default'}>{active ? '活跃' : '停用'}</Tag>
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: '操作',
|
|
77
|
+
key: 'action',
|
|
78
|
+
render: (_, record) => (
|
|
79
|
+
<Space>
|
|
80
|
+
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
|
81
|
+
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}>删除</Button>
|
|
82
|
+
</Space>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
|
90
|
+
<Input.Search
|
|
91
|
+
placeholder="搜索用户名/邮箱"
|
|
92
|
+
allowClear
|
|
93
|
+
onSearch={handleSearch}
|
|
94
|
+
style={{ width: 300 }}
|
|
95
|
+
/>
|
|
96
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
|
97
|
+
新增用户
|
|
98
|
+
</Button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<Table
|
|
102
|
+
columns={columns}
|
|
103
|
+
dataSource={users}
|
|
104
|
+
rowKey="id"
|
|
105
|
+
loading={loading}
|
|
106
|
+
pagination={{
|
|
107
|
+
...pagination,
|
|
108
|
+
showSizeChanger: true,
|
|
109
|
+
showTotal: (total) => `共 ${total} 条`,
|
|
110
|
+
}}
|
|
111
|
+
onChange={(pag: TablePaginationConfig) => {
|
|
112
|
+
setPagination(prev => ({
|
|
113
|
+
...prev,
|
|
114
|
+
current: pag.current ?? 1,
|
|
115
|
+
pageSize: pag.pageSize ?? 20,
|
|
116
|
+
}))
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 表单弹窗(创建/编辑)
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
interface UserFormModalProps {
|
|
128
|
+
open: boolean
|
|
129
|
+
editingUser?: User | null
|
|
130
|
+
onClose: () => void
|
|
131
|
+
onSuccess: () => void
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const UserFormModal: React.FC<UserFormModalProps> = ({ open, editingUser, onClose, onSuccess }) => {
|
|
135
|
+
const [form] = Form.useForm()
|
|
136
|
+
const [saving, setSaving] = useState(false)
|
|
137
|
+
const isEditing = !!editingUser
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (open) {
|
|
141
|
+
if (editingUser) {
|
|
142
|
+
form.setFieldsValue({
|
|
143
|
+
displayName: editingUser.displayName,
|
|
144
|
+
email: editingUser.email,
|
|
145
|
+
role: editingUser.role,
|
|
146
|
+
})
|
|
147
|
+
} else {
|
|
148
|
+
form.resetFields()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, [open, editingUser, form])
|
|
152
|
+
|
|
153
|
+
const handleSubmit = async () => {
|
|
154
|
+
try {
|
|
155
|
+
const values = await form.validateFields()
|
|
156
|
+
setSaving(true)
|
|
157
|
+
if (isEditing) {
|
|
158
|
+
await userService.update(editingUser!.id, values)
|
|
159
|
+
message.success('用户信息已更新')
|
|
160
|
+
} else {
|
|
161
|
+
await userService.create(values)
|
|
162
|
+
message.success('用户创建成功')
|
|
163
|
+
}
|
|
164
|
+
onSuccess()
|
|
165
|
+
onClose()
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// Form validateFields 校验失败会自动聚焦到错误字段,不需要额外处理
|
|
168
|
+
if (err instanceof Error) {
|
|
169
|
+
message.error(err.message)
|
|
170
|
+
}
|
|
171
|
+
} finally {
|
|
172
|
+
setSaving(false)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Modal
|
|
178
|
+
title={isEditing ? '编辑用户' : '新增用户'}
|
|
179
|
+
open={open}
|
|
180
|
+
onOk={handleSubmit}
|
|
181
|
+
onCancel={onClose}
|
|
182
|
+
confirmLoading={saving}
|
|
183
|
+
destroyOnClose
|
|
184
|
+
okText={isEditing ? '保存' : '创建'}
|
|
185
|
+
>
|
|
186
|
+
<Form
|
|
187
|
+
form={form}
|
|
188
|
+
layout="vertical"
|
|
189
|
+
autoComplete="off"
|
|
190
|
+
>
|
|
191
|
+
<Form.Item
|
|
192
|
+
name="displayName"
|
|
193
|
+
label="显示名称"
|
|
194
|
+
rules={[{ required: true, message: '请输入显示名称' }]}
|
|
195
|
+
>
|
|
196
|
+
<Input placeholder="请输入显示名称" />
|
|
197
|
+
</Form.Item>
|
|
198
|
+
|
|
199
|
+
<Form.Item
|
|
200
|
+
name="email"
|
|
201
|
+
label="邮箱"
|
|
202
|
+
rules={[
|
|
203
|
+
{ required: true, message: '请输入邮箱' },
|
|
204
|
+
{ type: 'email', message: '邮箱格式不正确' },
|
|
205
|
+
]}
|
|
206
|
+
>
|
|
207
|
+
<Input placeholder="请输入邮箱" />
|
|
208
|
+
</Form.Item>
|
|
209
|
+
|
|
210
|
+
<Form.Item
|
|
211
|
+
name="role"
|
|
212
|
+
label="角色"
|
|
213
|
+
rules={[{ required: true, message: '请选择角色' }]}
|
|
214
|
+
>
|
|
215
|
+
<Select placeholder="请选择角色">
|
|
216
|
+
<Select.Option value="ADMIN">管理员</Select.Option>
|
|
217
|
+
<Select.Option value="EDITOR">编辑</Select.Option>
|
|
218
|
+
<Select.Option value="VIEWER">查看者</Select.Option>
|
|
219
|
+
</Select>
|
|
220
|
+
</Form.Item>
|
|
221
|
+
</Form>
|
|
222
|
+
</Modal>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 响应式布局
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { Row, Col, Card, Statistic } from 'antd'
|
|
231
|
+
import { UserOutlined, FileTextOutlined, AuditOutlined } from '@ant-design/icons'
|
|
232
|
+
|
|
233
|
+
const Dashboard: React.FC = () => {
|
|
234
|
+
return (
|
|
235
|
+
<div>
|
|
236
|
+
{/* 统计卡片 - 响应式栅格 */}
|
|
237
|
+
<Row gutter={[16, 16]}>
|
|
238
|
+
<Col xs={24} sm={12} lg={6}>
|
|
239
|
+
<Card>
|
|
240
|
+
<Statistic title="用户总数" value={1280} prefix={<UserOutlined />} />
|
|
241
|
+
</Card>
|
|
242
|
+
</Col>
|
|
243
|
+
<Col xs={24} sm={12} lg={6}>
|
|
244
|
+
<Card>
|
|
245
|
+
<Statistic title="文档数" value={356} prefix={<FileTextOutlined />} />
|
|
246
|
+
</Card>
|
|
247
|
+
</Col>
|
|
248
|
+
<Col xs={24} sm={12} lg={6}>
|
|
249
|
+
<Card>
|
|
250
|
+
<Statistic title="待审核" value={23} prefix={<AuditOutlined />} />
|
|
251
|
+
</Card>
|
|
252
|
+
</Col>
|
|
253
|
+
</Row>
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Ant Design 5.x 组件使用规范
|
|
2
|
+
|
|
3
|
+
## 通用原则
|
|
4
|
+
|
|
5
|
+
- Ant Design 5.x 使用 CSS-in-JS(`@ant-design/cssinjs`),无需额外引入 CSS 文件
|
|
6
|
+
- 组件使用 Tree Shaking 按需导入,`import { Button, Table } from 'antd'` 即可
|
|
7
|
+
- 主题定制通过 `ConfigProvider` 的 `theme` 属性配置,禁止直接覆盖 Ant Design 的 CSS 类名
|
|
8
|
+
- 图标使用 `@ant-design/icons` 的具名导入:`import { UserOutlined } from '@ant-design/icons'`
|
|
9
|
+
|
|
10
|
+
## 常用组件规范
|
|
11
|
+
|
|
12
|
+
### Button(按钮)
|
|
13
|
+
- 主要操作使用 `type="primary"`,次要操作默认
|
|
14
|
+
- 危险操作使用 `danger` 属性
|
|
15
|
+
- 异步操作必须设置 `loading` 状态防重复提交
|
|
16
|
+
- 纯图标按钮必须有 `aria-label` 提供无障碍说明
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<Button type="primary" loading={submitting} onClick={handleSubmit}>
|
|
20
|
+
提交
|
|
21
|
+
</Button>
|
|
22
|
+
<Button danger onClick={handleDelete}>删除</Button>
|
|
23
|
+
<Button icon={<SearchOutlined />} aria-label="搜索" />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Table(表格)
|
|
27
|
+
- 始终指定 `rowKey` 为唯一标识字段(如 `id`)
|
|
28
|
+
- 列表格使用 `pagination` 属性控制分页
|
|
29
|
+
- 长列表开启 `virtual` 虚拟滚动(>= 1000 行)
|
|
30
|
+
- 空数据使用 `locale.emptyText` 自定义空状态提示
|
|
31
|
+
- 后端分页使用 `onChange` 回调 + `pagination.current/pageSize` 受控
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<Table
|
|
35
|
+
dataSource={users}
|
|
36
|
+
columns={columns}
|
|
37
|
+
rowKey="id"
|
|
38
|
+
loading={loading}
|
|
39
|
+
pagination={{
|
|
40
|
+
current: page,
|
|
41
|
+
pageSize: 20,
|
|
42
|
+
total,
|
|
43
|
+
showSizeChanger: true,
|
|
44
|
+
showTotal: (total) => `共 ${total} 条`,
|
|
45
|
+
}}
|
|
46
|
+
onChange={(pagination) => {
|
|
47
|
+
setPage(pagination.current)
|
|
48
|
+
setPageSize(pagination.pageSize)
|
|
49
|
+
}}
|
|
50
|
+
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Form(表单)
|
|
55
|
+
- 使用 `layout="vertical"` 默认垂直布局
|
|
56
|
+
- 所有校验规则集中在 `rules` 中定义,避免使用自定义 `validator`(除非复杂业务)
|
|
57
|
+
- `Form.Item` 的 `name` 必须与提交数据结构的字段名一致
|
|
58
|
+
- 编辑表单用 `form.setFieldsValue()` 填充初始值,不要用 `initialValues`
|
|
59
|
+
- 全局校验信息通过 `ConfigProvider` 的 `form.validateMessages` 统一配置
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Form
|
|
63
|
+
form={form}
|
|
64
|
+
layout="vertical"
|
|
65
|
+
onFinish={handleSubmit}
|
|
66
|
+
autoComplete="off"
|
|
67
|
+
>
|
|
68
|
+
<Form.Item
|
|
69
|
+
name="email"
|
|
70
|
+
label="邮箱"
|
|
71
|
+
rules={[
|
|
72
|
+
{ required: true, message: '请输入邮箱' },
|
|
73
|
+
{ type: 'email', message: '邮箱格式不正确' },
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
<Input placeholder="请输入邮箱" />
|
|
77
|
+
</Form.Item>
|
|
78
|
+
</Form>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Modal(弹窗)
|
|
82
|
+
- 表单弹窗用 `destroyOnClose` 确保关闭后重置状态
|
|
83
|
+
- `afterClose` 用于关闭后的清理回调
|
|
84
|
+
- 确认弹窗用 `Modal.confirm()` 快捷方法
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<Modal
|
|
88
|
+
title="编辑用户"
|
|
89
|
+
open={isOpen}
|
|
90
|
+
onOk={handleSave}
|
|
91
|
+
onCancel={handleCancel}
|
|
92
|
+
confirmLoading={saving}
|
|
93
|
+
destroyOnClose
|
|
94
|
+
afterClose={() => form.resetFields()}
|
|
95
|
+
>
|
|
96
|
+
<UserForm form={form} user={editingUser} />
|
|
97
|
+
</Modal>
|
|
98
|
+
|
|
99
|
+
// 确认弹窗
|
|
100
|
+
Modal.confirm({
|
|
101
|
+
title: '确认删除',
|
|
102
|
+
content: '确定要删除该用户吗?此操作不可恢复。',
|
|
103
|
+
okText: '确认删除',
|
|
104
|
+
okType: 'danger',
|
|
105
|
+
onOk: () => deleteUser(id),
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Select(选择器)
|
|
110
|
+
- 选项少时直接写 `<Select.Option>`;选项多时使用 `options` 数组属性
|
|
111
|
+
- 远程搜索使用 `showSearch` + `onSearch` + `filterOption` 自定义
|
|
112
|
+
- 多选用 `mode="multiple"`
|
|
113
|
+
|
|
114
|
+
### DatePicker(日期选择器)
|
|
115
|
+
- 使用 `format` 指定显示格式
|
|
116
|
+
- 表单中配合 `Form.Item` 使用,值类型为 `dayjs` 对象
|
|
117
|
+
- 范围选择用 `DatePicker.RangePicker`
|
|
118
|
+
|
|
119
|
+
### Space(间距)
|
|
120
|
+
- 组件间间距优先使用 `<Space>` 包裹,避免手动 `margin`
|
|
121
|
+
- 默认间距为 `size="small"`(8px),可根据上下文调整
|
|
122
|
+
|
|
123
|
+
### message / notification(消息提示)
|
|
124
|
+
- 操作反馈使用 `message.success()` / `message.error()` 轻量提示
|
|
125
|
+
- 重要通知使用 `notification.open()` / `notification.info()`
|
|
126
|
+
- 不要在组件中手动创建 message 实例 — 使用静态方法即可
|
|
127
|
+
|
|
128
|
+
### Spin(加载中)
|
|
129
|
+
- 页面级加载使用 `Spin` 包裹内容区域
|
|
130
|
+
- 组件级加载使用 Spin 组件的 `spinning` 属性
|
|
131
|
+
- 全局加载状态使用 `Spin` + Suspense(React.lazy 场景)
|
|
132
|
+
|
|
133
|
+
## 主题定制
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<ConfigProvider
|
|
137
|
+
theme={{
|
|
138
|
+
token: {
|
|
139
|
+
colorPrimary: '#1677ff',
|
|
140
|
+
borderRadius: 6,
|
|
141
|
+
colorBgContainer: '#ffffff',
|
|
142
|
+
},
|
|
143
|
+
components: {
|
|
144
|
+
Table: {
|
|
145
|
+
headerBg: '#fafafa',
|
|
146
|
+
rowHoverBg: '#f5f5f5',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<App />
|
|
152
|
+
</ConfigProvider>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Ant Design + Tailwind CSS 共存
|
|
156
|
+
|
|
157
|
+
- Ant Design 负责组件内部样式,Tailwind 负责页面/组件外部的布局和间距
|
|
158
|
+
- 避免在 Ant Design 组件上直接用 Tailwind 覆盖内联样式(Ant Design 的 CSS-in-JS 优先级高)
|
|
159
|
+
- 布局使用 Tailwind 的 `flex`/`grid` 类 + Ant Design 的 `Row`/`Col` 搭配
|
|
160
|
+
- 使用 Tailwind 的 `dark:` 变体时,需同时配置 Ant Design ConfigProvider 的 `theme.algorithm`(`darkAlgorithm`)
|
|
161
|
+
|
|
162
|
+
## 性能注意
|
|
163
|
+
|
|
164
|
+
- 大列表(>500 行)使用 Table 的 `virtual` 属性开启虚拟滚动
|
|
165
|
+
- 大数据集的选择器使用 `virtual` 属性(Select + options 数量多时)
|
|
166
|
+
- 避免频繁调用 `message`/`notification` — 使用 `message.destroy()` 或防抖
|