@kne/system-layout 0.2.0-alpha.2 → 0.2.0-alpha.20

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/README.md CHANGED
@@ -1,29 +1,28 @@
1
-
2
1
  # system-layout
3
2
 
4
-
5
3
  ### 描述
6
4
 
7
5
  用于一个系统初始化布局
8
6
 
9
-
10
7
  ### 安装
11
8
 
12
9
  ```shell
13
10
  npm i --save @kne/system-layout
14
11
  ```
15
12
 
16
-
17
13
  ### 概述
18
14
 
19
15
  用于一个系统初始化布局,提供基础的页面结构和功能模块。
20
16
 
21
17
  #### 功能模块
22
18
 
23
- - **布局组件**:提供灵活的页面布局,支持菜单展开/折叠。
24
- - **用户信息展示**:支持用户卡片展示。
25
- - **菜单管理**:支持动态菜单配置和路由跳转。
26
-
19
+ - **布局组件(SystemLayout)**:提供灵活的页面布局,支持桌面端和移动端两种模式。桌面端包含可展开/收起的侧边菜单,移动端为侧滑抽屉菜单 + 底部工具栏。
20
+ - **页面组件(Page)**:提供标题栏、返回按钮、操作按钮组、加载状态等功能,支持函数式子组件自定义渲染。移动端标题栏滚动时自动收缩为胶囊样式。
21
+ - **菜单管理(Menu)**:支持分组、图标、自定义激活项、自定义点击回调,菜单项可配置是否在移动端工具栏中显示。
22
+ - **用户信息(UserCard)**:在菜单头部展示用户信息,支持头像、姓名、邮箱、手机号、描述等字段。
23
+ - **AI 对话框**:桌面端支持 AI 对话框小窗口和内嵌面板两种展示模式。
24
+ - **响应式工具**:透传 `@kne/responsive-utils` 的响应式工具,支持断点检测、媒体查询、弹层容器等功能。
25
+ - **主题适配**:提供 `themeToken` 配置,将常用 Ant Design 组件背景色设为透明,适配半透明背景风格。
27
26
 
28
27
  #### 注意
29
28
 
@@ -37,110 +36,257 @@ import '@kne/system-layout/dist/index.css';
37
36
  #### TODO
38
37
 
39
38
  - [x] 完成基础布局
40
- - [ ] 适配到移动端
39
+ - [x] 适配到移动端
40
+ - [ ] 完善桌面端 AI 对话框功能
41
41
 
42
- ### 示例(全屏)
43
42
 
43
+ ### 示例(全屏)
44
44
 
45
45
  #### 示例样式
46
46
 
47
47
  ```scss
48
- .mobile-layout {
49
- width: 430px;
50
- margin: 0 auto;
51
- position: relative;
52
-
53
- .toolbar-list {
54
- position: absolute !important;
55
- }
56
- }
48
+ /* 示例样式 - 移动端布局示例无需额外样式 */
57
49
  ```
58
50
 
59
51
  #### 示例代码
60
52
 
61
- - 这里填写示例标题
62
- - 这里填写示例说明
63
- - _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css)
53
+ - 基础布局
54
+ - 移动端基础布局示例,展示 SystemLayout 在 isMobile 模式下的菜单、工具栏、用户信息和页面内容
55
+ - _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css),antd(antd)
64
56
 
65
57
  ```jsx
66
58
  const { default: SystemLayout, Page } = _SystemLayout;
59
+ const { Flex, Card, Row, Col, Statistic, Progress, Tag, Typography } = antd;
60
+ const { Text, Title } = Typography;
61
+
62
+ const STATS = [
63
+ { title: '待办任务', value: 8, suffix: '项', color: '#1677ff' },
64
+ { title: '本月入职', value: 24, suffix: '人', color: '#52c41a' },
65
+ { title: '待面试', value: 15, suffix: '场', color: '#faad14' },
66
+ { title: '离职申请', value: 3, suffix: '份', color: '#ff4d4f' }
67
+ ];
68
+
69
+ const ONBOARDING_STEPS = [
70
+ { title: '完善个人资料', done: true },
71
+ { title: '签署入职文件', done: true },
72
+ { title: '领取办公设备', done: false },
73
+ { title: '参加新人培训', done: false }
74
+ ];
67
75
 
68
76
  const BaseExample = () => {
77
+ const doneCount = ONBOARDING_STEPS.filter(step => step.done).length;
78
+ const percent = Math.round((doneCount / ONBOARDING_STEPS.length) * 100);
79
+
69
80
  return (
70
- <div>
71
- <SystemLayout
72
- userInfo={{
73
- name: 'Lucy L',
74
- email: 'lucy@company.com'
81
+ <SystemLayout
82
+ userInfo={{ name: 'Lucy L', email: 'lucy@company.com' }}
83
+ menu={{
84
+ base: '/SystemLayout',
85
+ items: [
86
+ { path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
87
+ { group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
88
+ { group: 'HIRING', path: '/hiring/application', label: 'Application List', icon: 'icon-assignment' },
89
+ { group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' },
90
+ { group: 'TALENT REVIEW', path: '/talent-review', label: 'Projects', icon: 'icon-manage_accounts' },
91
+ { group: 'TALENT REVIEW', path: '/talent-review/employee', label: 'Employee', toolbar: true, icon: 'icon-groups_2' },
92
+ { group: 'TALENT REVIEW', path: '/talent-review/ai-models', label: 'AI Models', icon: 'icon-network_intelligence' }
93
+ ]
94
+ }}
95
+ >
96
+ <Page
97
+ title="Onboarding"
98
+ buttonProps={{
99
+ showLength: 1,
100
+ list: [
101
+ { type: 'primary', children: 'New' },
102
+ { children: 'Options' }
103
+ ]
75
104
  }}
76
- aiDialog={{}}
77
- menu={{
78
- base: '/SystemLayout',
79
- items: [
80
- {
81
- path: '/',
82
- label: 'Onboarding',
83
- icon: ({ active }) => (active ? 'home' : 'home_line')
84
- },
85
- {
86
- group: 'HIRING',
87
- path: '/hiring',
88
- label: 'Hiring Hub',
89
- icon: 'icon-assignment_ind'
90
- },
91
- {
92
- group: 'HIRING',
93
- path: '/hiring/application',
94
- label: 'Application List',
95
- icon: 'icon-assignment'
96
- },
97
- {
98
- group: 'PEOPLE',
99
- path: '/people',
100
- label: 'Management',
101
- icon: 'icon-automation'
102
- },
103
- {
104
- group: 'TALENT REVIEW',
105
- path: '/talent-review',
106
- label: 'Projects',
107
- icon: 'icon-manage_accounts'
108
- },
109
- {
110
- group: 'TALENT REVIEW',
111
- path: '/talent-review/employee',
112
- label: 'Employee',
113
- icon: 'icon-groups_2'
114
- },
115
- {
116
- group: 'TALENT REVIEW',
117
- path: '/talent-review/ai-models',
118
- label: 'AI Models',
119
- icon: 'icon-network_intelligence'
120
- }
105
+ >
106
+ <Flex vertical gap={16}>
107
+ <Card styles={{ body: { padding: 20 } }} style={{ background: 'rgba(255,255,255,0.5)' }}>
108
+ <Flex justify="space-between" align="center" wrap="wrap" gap={16}>
109
+ <Flex vertical gap={4}>
110
+ <Title level={4} style={{ margin: 0 }}>
111
+ 下午好,Lucy 👋
112
+ </Title>
113
+ <Text type="secondary">欢迎回到工作台,今天有 8 项待办事项等待处理。</Text>
114
+ </Flex>
115
+ <Tag color="processing" style={{ fontSize: 13, padding: '4px 12px' }}>
116
+ 入职进度 {percent}%
117
+ </Tag>
118
+ </Flex>
119
+ </Card>
120
+
121
+ <Row gutter={[12, 12]}>
122
+ {STATS.map(stat => (
123
+ <Col xs={12} md={6} key={stat.title}>
124
+ <Card styles={{ body: { padding: 16 } }} style={{ background: 'rgba(255,255,255,0.5)' }}>
125
+ <Statistic
126
+ title={stat.title}
127
+ value={stat.value}
128
+ suffix={stat.suffix}
129
+ valueStyle={{ color: stat.color, fontSize: 24 }}
130
+ />
131
+ </Card>
132
+ </Col>
133
+ ))}
134
+ </Row>
135
+
136
+ <Card title="入职清单" styles={{ body: { padding: 20 } }} style={{ background: 'rgba(255,255,255,0.5)' }}>
137
+ <Flex vertical gap={16}>
138
+ <Progress percent={percent} strokeColor={{ from: '#1677ff', to: '#52c41a' }} />
139
+ <Flex vertical gap={10}>
140
+ {ONBOARDING_STEPS.map(step => (
141
+ <Flex key={step.title} align="center" gap={10}>
142
+ <Tag color={step.done ? 'success' : 'default'} style={{ margin: 0 }}>
143
+ {step.done ? '已完成' : '待办'}
144
+ </Tag>
145
+ <Text delete={step.done} type={step.done ? 'secondary' : undefined}>
146
+ {step.title}
147
+ </Text>
148
+ </Flex>
149
+ ))}
150
+ </Flex>
151
+ </Flex>
152
+ </Card>
153
+ </Flex>
154
+ </Page>
155
+ </SystemLayout>
156
+ );
157
+ };
158
+
159
+ render(<BaseExample />);
160
+
161
+ ```
162
+
163
+ - Page 组件
164
+ - Page 组件的各种功能:返回按钮、自定义内容、函数式子组件、加载状态、navbar/toolbar 控制等
165
+ - _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css),antd(antd)
166
+
167
+ ```jsx
168
+ const { default: SystemLayout, Page } = _SystemLayout;
169
+ const { useState } = React;
170
+ const { Segmented, Button, Flex, Card, Alert, Descriptions, Empty } = antd;
171
+
172
+ const MODE_OPTIONS = [
173
+ { label: '基础', value: 'basic' },
174
+ { label: '返回', value: 'back' },
175
+ { label: 'Extra', value: 'extra' },
176
+ { label: '无填充', value: 'noPadding' },
177
+ { label: '函数式', value: 'function' },
178
+ { label: '无导航', value: 'noNavbar' },
179
+ { label: '无工具栏', value: 'noToolbar' }
180
+ ];
181
+
182
+ const MODE_TIPS = {
183
+ basic: '标准页面:标题栏 + 操作按钮组 + 内容区。',
184
+ back: '已启用返回按钮(back),点击会调用 navigate(-1)。',
185
+ extra: '已自定义 extra 内容,替代默认的按钮组。',
186
+ noPadding: '已移除内容区内边距(noPadding),适合放置全屏地图、表格等。',
187
+ function: '函数式子组件模式:Page 将渲染控制权交给 children 函数。',
188
+ noNavbar: '已隐藏顶部导航栏(navbar=false)。',
189
+ noToolbar: '已隐藏底部工具栏(toolbar=false)。'
190
+ };
191
+
192
+ const ModeSwitcher = ({ mode, setMode }) => (
193
+ <Segmented block value={mode} onChange={setMode} options={MODE_OPTIONS} />
194
+ );
195
+
196
+ const menu = {
197
+ base: '/SystemLayout',
198
+ items: [
199
+ { path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
200
+ { group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
201
+ { group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' }
202
+ ]
203
+ };
204
+
205
+ const FunctionParams = () => (
206
+ <Descriptions column={1} size="small" bordered items={[
207
+ { key: 'navbar', label: 'navbar', children: '导航栏元素' },
208
+ { key: 'className', label: 'className', children: '内容区类名(含 noPadding 等)' },
209
+ { key: 'render', label: 'render', children: '标准渲染函数,用于包裹自定义内容' },
210
+ { key: 'pageLoading', label: 'pageLoading', children: '加载状态元素(含骨架屏)' }
211
+ ]} />
212
+ );
213
+
214
+ const BaseExample = () => {
215
+ const [mode, setMode] = useState('basic');
216
+ const [loading, setLoading] = useState(false);
217
+
218
+ return (
219
+ <SystemLayout userInfo={{ name: 'Lucy L', email: 'lucy@company.com' }} menu={menu}>
220
+ <Page
221
+ title="Page 组件演示"
222
+ back={mode === 'back'}
223
+ extra={mode === 'extra' ? <Button size="small" type="primary">自定义操作</Button> : null}
224
+ noPadding={mode === 'noPadding'}
225
+ navbar={mode !== 'noNavbar'}
226
+ toolbar={mode !== 'noToolbar'}
227
+ buttonProps={{
228
+ showLength: 1,
229
+ list: [
230
+ { type: 'primary', children: 'New' },
231
+ { children: 'Options' },
232
+ { children: 'Export' }
121
233
  ]
122
- }}>
123
- <Page
124
- title="Home"
125
- buttonProps={{
126
- list: [
127
- {
128
- type: 'primary',
129
- children: 'New'
130
- },
131
- {
132
- children: 'Options'
133
- },
134
- {
135
- loading: true,
136
- children: 'Options2'
234
+ }}
235
+ >
236
+ {mode === 'function'
237
+ ? ({ render, pageLoading }) => {
238
+ if (loading) {
239
+ return pageLoading;
137
240
  }
138
- ]
139
- }}>
140
- Content
141
- </Page>
142
- </SystemLayout>
143
- </div>
241
+ return render({
242
+ children: (
243
+ <Flex vertical gap={16}>
244
+ <ModeSwitcher mode={mode} setMode={setMode} />
245
+ <Alert type="info" showIcon message={MODE_TIPS.function} />
246
+ <Card size="small" title="children 函数参数">
247
+ <FunctionParams />
248
+ </Card>
249
+ <Button
250
+ type="primary"
251
+ loading={loading}
252
+ onClick={() => {
253
+ setLoading(true);
254
+ setTimeout(() => setLoading(false), 2000);
255
+ }}
256
+ >
257
+ 模拟加载(展示骨架屏)
258
+ </Button>
259
+ </Flex>
260
+ )
261
+ });
262
+ }
263
+ : (
264
+ <Flex vertical gap={16}>
265
+ <ModeSwitcher mode={mode} setMode={setMode} />
266
+ {mode === 'noPadding' ? (
267
+ <Flex
268
+ align="center"
269
+ justify="center"
270
+ style={{ background: 'rgba(0,0,0,0.04)', height: 300, borderRadius: 8 }}
271
+ >
272
+ <Empty description="无内边距内容区(noPadding)" />
273
+ </Flex>
274
+ ) : (
275
+ <>
276
+ <Alert type="info" showIcon message={MODE_TIPS[mode]} />
277
+ <Card size="small" style={{ background: 'rgba(255,255,255,0.5)' }}>
278
+ <Descriptions column={1} size="small" items={[
279
+ { key: 'mode', label: '当前模式', children: mode },
280
+ { key: 'desc', label: '说明', children: 'Page 提供标题栏、返回按钮、操作按钮组与自定义内容。' },
281
+ { key: 'mobile', label: '移动端', children: '标题栏固定在顶部,向下滚动时收缩为胶囊样式。' }
282
+ ]} />
283
+ </Card>
284
+ </>
285
+ )}
286
+ </Flex>
287
+ )}
288
+ </Page>
289
+ </SystemLayout>
144
290
  );
145
291
  };
146
292
 
@@ -148,105 +294,292 @@ render(<BaseExample />);
148
294
 
149
295
  ```
150
296
 
151
- - 这里填写示例标题
152
- - 这里填写示例说明
153
- - _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css)
297
+ - 滚动加载
298
+ - 结合 @kne/scroll-loader 在 Page 内实现整页下拉无限加载:候选人列表随页面滚动到底部自动加载下一页,支持关键字搜索与阶段筛选,并根据 useIsMobile 响应式切换布局——移动端整行显示,PC 端使用 antd Masonry 瀑布流多列展示(卡片高度不统一)
299
+ - _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css),_ScrollLoader(@kne/scroll-loader)[import * as _ScrollLoader from "@kne/scroll-loader"],(@kne/scroll-loader/dist/index.css),antd(antd)
154
300
 
155
301
  ```jsx
156
- const { default: SystemLayout, Page } = _SystemLayout;
157
- const { useRef, useState, useEffect } = React;
302
+ const { default: SystemLayout, Page, useIsMobile } = _SystemLayout;
303
+ const { FetchScrollLoader } = _ScrollLoader;
304
+ const { Flex, Avatar, Tag, Typography, Input, Select, Progress, Empty, Divider, Masonry } = antd;
305
+ const { useState } = React;
306
+ const { Text } = Typography;
307
+
308
+ const STAGES = [
309
+ { value: 'screening', label: '简历筛选', color: 'default' },
310
+ { value: 'interview', label: '面试中', color: 'processing' },
311
+ { value: 'offer', label: '已发 Offer', color: 'success' },
312
+ { value: 'rejected', label: '未通过', color: 'error' }
313
+ ];
314
+
315
+ const POSITIONS = [
316
+ { title: '高级前端工程师', dept: '技术部' },
317
+ { title: '产品经理', dept: '产品部' },
318
+ { title: 'UI/UX 设计师', dept: '设计部' },
319
+ { title: '数据分析师', dept: '数据部' },
320
+ { title: '增长运营专员', dept: '运营部' }
321
+ ];
322
+
323
+ const FIRST_NAMES = ['伟', '芳', '娜', '秀英', '敏', '静', '强', '磊', '洋', '艳', '勇', '军', '杰', '娟', '涛', '明'];
324
+ const SURNAMES = ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴', '徐', '孙', '马', '朱', '胡', '林'];
325
+
326
+ const AVATAR_COLORS = ['#1677ff', '#52c41a', '#faad14', '#eb2f96', '#722ed1', '#13c2c2'];
327
+
328
+ const SKILLS = ['React', 'Vue', 'TypeScript', 'Node.js', 'Figma', '数据分析', 'Python', '项目管理', 'SQL', '增长策略', 'A/B 测试', 'UI 设计'];
329
+
330
+ // 长度不一的自我介绍,用于制造瀑布流所需的高度差异
331
+ const BIOS = [
332
+ '专注体验设计,擅长从 0 到 1 搭建产品。',
333
+ '有丰富的团队协作经验,能够快速融入并推动项目落地,善于在复杂需求中拆解优先级、平衡各方诉求并持续交付高质量成果。',
334
+ '注重数据驱动决策。',
335
+ '热爱技术钻研,长期关注前沿趋势,乐于分享与沉淀方法论,希望在更大的舞台上创造价值,同时帮助团队成员共同成长。',
336
+ ''
337
+ ];
338
+
339
+ // 模拟候选人列表接口:支持关键字搜索与阶段筛选,返回分页数据
340
+ const mockCandidateList = ({ data = {} }) => {
341
+ const { currentPage = 1, perPage = 12, keyword, stage } = data;
342
+ return new Promise(resolve => {
343
+ const allCandidates = Array.from({ length: 128 }, (_, i) => {
344
+ const position = POSITIONS[i % POSITIONS.length];
345
+ const stageInfo = STAGES[i % STAGES.length];
346
+ return {
347
+ id: i + 1,
348
+ name: &#96;${SURNAMES[i % SURNAMES.length]}${FIRST_NAMES[(i * 3) % FIRST_NAMES.length]}&#96;,
349
+ position: position.title,
350
+ department: position.dept,
351
+ stage: stageInfo.value,
352
+ matchScore: 60 + ((i * 7) % 40),
353
+ experience: 1 + (i % 12),
354
+ location: ['北京', '上海', '深圳', '杭州', '广州', '成都'][i % 6],
355
+ appliedAt: &#96;2024-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}&#96;,
356
+ skills: SKILLS.slice((i * 2) % SKILLS.length, ((i * 2) % SKILLS.length) + 2 + (i % 4)),
357
+ bio: BIOS[i % BIOS.length]
358
+ };
359
+ });
360
+
361
+ let filtered = allCandidates;
362
+ if (keyword) {
363
+ filtered = filtered.filter(item => item.name.includes(keyword) || item.position.includes(keyword));
364
+ }
365
+ if (stage) {
366
+ filtered = filtered.filter(item => item.stage === stage);
367
+ }
368
+
369
+ const start = (currentPage - 1) * perPage;
370
+ const pageData = filtered.slice(start, start + perPage);
371
+
372
+ setTimeout(() => {
373
+ resolve({ totalCount: filtered.length, pageData });
374
+ }, 700);
375
+ });
376
+ };
377
+
378
+ const getStageInfo = value => STAGES.find(item => item.value === value) || STAGES[0];
379
+
380
+ const getScoreColor = score => (score >= 90 ? '#52c41a' : score >= 75 ? '#1677ff' : '#faad14');
381
+
382
+ // 移动端:整行横向布局(头像 + 信息 + 匹配度)
383
+ const CandidateRow = ({ item }) => {
384
+ const stageInfo = getStageInfo(item.stage);
385
+ return (
386
+ <Flex
387
+ align="center"
388
+ gap={16}
389
+ style={{
390
+ padding: 16,
391
+ background: 'rgba(255, 255, 255, 0.6)',
392
+ border: '1px solid rgba(0, 0, 0, 0.06)',
393
+ borderRadius: 12
394
+ }}
395
+ >
396
+ <Avatar size={48} style={{ backgroundColor: AVATAR_COLORS[item.id % AVATAR_COLORS.length], flex: 'none' }}>
397
+ {item.name.slice(0, 1)}
398
+ </Avatar>
399
+ <Flex vertical gap={6} flex={1} style={{ minWidth: 0 }}>
400
+ <Flex align="center" gap={8} wrap="wrap">
401
+ <Text strong style={{ fontSize: 15 }}>
402
+ {item.name}
403
+ </Text>
404
+ <Tag color={stageInfo.color}>{stageInfo.label}</Tag>
405
+ </Flex>
406
+ <Flex align="center" gap={8} wrap="wrap">
407
+ <Text type="secondary" style={{ fontSize: 13 }}>
408
+ {item.position}
409
+ </Text>
410
+ <Divider type="vertical" style={{ margin: 0 }} />
411
+ <Text type="secondary" style={{ fontSize: 13 }}>
412
+ {item.department}
413
+ </Text>
414
+ <Divider type="vertical" style={{ margin: 0 }} />
415
+ <Text type="secondary" style={{ fontSize: 13 }}>
416
+ {item.location} · {item.experience} 年经验
417
+ </Text>
418
+ </Flex>
419
+ <Text type="secondary" style={{ fontSize: 12 }}>
420
+ 投递于 {item.appliedAt}
421
+ </Text>
422
+ </Flex>
423
+ <Flex vertical align="center" gap={4} style={{ flex: 'none', width: 96 }}>
424
+ <Progress
425
+ type="circle"
426
+ size={52}
427
+ percent={item.matchScore}
428
+ strokeColor={getScoreColor(item.matchScore)}
429
+ format={percent => &#96;${percent}&#96;}
430
+ />
431
+ <Text type="secondary" style={{ fontSize: 12 }}>
432
+ 匹配度
433
+ </Text>
434
+ </Flex>
435
+ </Flex>
436
+ );
437
+ };
438
+
439
+ // PC 端:卡片纵向布局,用于瀑布流展示(内容长度不一,高度自然不统一)
440
+ const CandidateCard = ({ item }) => {
441
+ const stageInfo = getStageInfo(item.stage);
442
+ return (
443
+ <Flex
444
+ vertical
445
+ gap={12}
446
+ style={{
447
+ padding: 16,
448
+ background: 'rgba(255, 255, 255, 0.6)',
449
+ border: '1px solid rgba(0, 0, 0, 0.06)',
450
+ borderRadius: 12
451
+ }}
452
+ >
453
+ <Flex align="center" gap={12}>
454
+ <Avatar size={44} style={{ backgroundColor: AVATAR_COLORS[item.id % AVATAR_COLORS.length], flex: 'none' }}>
455
+ {item.name.slice(0, 1)}
456
+ </Avatar>
457
+ <Flex vertical gap={4} flex={1} style={{ minWidth: 0 }}>
458
+ <Text strong ellipsis style={{ fontSize: 15 }}>
459
+ {item.name}
460
+ </Text>
461
+ <Tag color={stageInfo.color} style={{ margin: 0, width: 'fit-content' }}>
462
+ {stageInfo.label}
463
+ </Tag>
464
+ </Flex>
465
+ <Progress
466
+ type="circle"
467
+ size={48}
468
+ percent={item.matchScore}
469
+ strokeColor={getScoreColor(item.matchScore)}
470
+ format={percent => &#96;${percent}&#96;}
471
+ />
472
+ </Flex>
473
+ <Flex vertical gap={4}>
474
+ <Text type="secondary" ellipsis style={{ fontSize: 13 }}>
475
+ {item.position} · {item.department}
476
+ </Text>
477
+ <Text type="secondary" style={{ fontSize: 13 }}>
478
+ {item.location} · {item.experience} 年经验
479
+ </Text>
480
+ <Text type="secondary" style={{ fontSize: 12 }}>
481
+ 投递于 {item.appliedAt}
482
+ </Text>
483
+ </Flex>
484
+ {item.bio ? (
485
+ <Text style={{ fontSize: 13 }}>{item.bio}</Text>
486
+ ) : null}
487
+ <Flex gap={6} wrap="wrap">
488
+ {item.skills.map(skill => (
489
+ <Tag key={skill} style={{ margin: 0 }}>
490
+ {skill}
491
+ </Tag>
492
+ ))}
493
+ </Flex>
494
+ </Flex>
495
+ );
496
+ };
497
+
498
+ const menu = {
499
+ base: '/SystemLayout',
500
+ items: [
501
+ { path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
502
+ { group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
503
+ { group: 'HIRING', path: '/hiring/application', label: 'Application List', toolbar: true, icon: 'icon-assignment' },
504
+ { group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' }
505
+ ]
506
+ };
507
+
158
508
  const BaseExample = () => {
159
- const ref = useRef(null);
160
- const [toolbarTarget, setToolbarTarget] = useState(null);
161
- useEffect(() => {
162
- setToolbarTarget(ref.current);
163
- }, []);
509
+ const isMobile = useIsMobile();
510
+ const [keyword, setKeyword] = useState('');
511
+ const [stage, setStage] = useState(undefined);
512
+
164
513
  return (
165
- <div className="mobile-layout">
166
- {toolbarTarget && (
167
- <SystemLayout
168
- isMobile
169
- toolbarTarget={toolbarTarget}
170
- userInfo={{
171
- name: 'Lucy L',
172
- email: 'lucy@company.com'
514
+ <SystemLayout userInfo={{ name: 'Lucy L', email: 'lucy@company.com' }} menu={menu}>
515
+ <Page title="候选人列表">
516
+ <FetchScrollLoader
517
+ useSimpleBar={false}
518
+ api={{ loader: mockCandidateList }}
519
+ searchProps={{ keyword, stage }}
520
+ getSearchProps={props => {
521
+ const result = {};
522
+ if (props.keyword) result.keyword = props.keyword;
523
+ if (props.stage) result.stage = props.stage;
524
+ return result;
173
525
  }}
174
- aiDialog={{}}
175
- menu={{
176
- base: '/SystemLayout',
177
- items: [
178
- {
179
- path: '/',
180
- label: 'Onboarding',
181
- toolbar: true,
182
- icon: ({ active }) => (active ? 'home' : 'home_line')
183
- },
184
- {
185
- group: 'HIRING',
186
- path: '/hiring',
187
- label: 'Hiring Hub',
188
- toolbar: true,
189
- icon: 'icon-assignment_ind'
190
- },
191
- {
192
- group: 'HIRING',
193
- path: '/hiring/application',
194
- label: 'Application List',
195
- toolbar: true,
196
- icon: 'icon-assignment'
197
- },
198
- {
199
- group: 'PEOPLE',
200
- path: '/people',
201
- label: 'Management',
202
- icon: 'icon-automation'
203
- },
204
- {
205
- group: 'TALENT REVIEW',
206
- path: '/talent-review',
207
- label: 'Projects',
208
- icon: 'icon-manage_accounts'
209
- },
210
- {
211
- group: 'TALENT REVIEW',
212
- path: '/talent-review/employee',
213
- label: 'Employee',
214
- toolbar: true,
215
- icon: 'icon-groups_2'
216
- },
217
- {
218
- group: 'TALENT REVIEW',
219
- path: '/talent-review/ai-models',
220
- label: 'AI Models',
221
- icon: 'icon-network_intelligence'
222
- }
223
- ]
224
- }}>
225
- <Page
226
- toolbar
227
- title="Home"
228
- buttonProps={{
229
- showLength: 1,
230
- list: [
231
- {
232
- type: 'primary',
233
- children: 'New'
234
- },
235
- {
236
- children: 'Options'
237
- },
238
- {
239
- loading: true,
240
- children: 'Options2'
241
- }
242
- ]
243
- }}>
244
- Content
245
- </Page>
246
- </SystemLayout>
247
- )}
248
- <div ref={ref} />
249
- </div>
526
+ pagination={{ paramsType: 'data', current: 'currentPage', pageSizeName: 'perPage', pageSize: 12 }}
527
+ completeTips="— 已加载全部候选人 —"
528
+ render={({ fetchApi, children }) => (
529
+ <Flex vertical gap={16}>
530
+ <Flex justify="space-between" align="center" wrap="wrap" gap={12}>
531
+ <Flex gap={12} align="center" wrap="wrap">
532
+ <Input.Search
533
+ placeholder="搜索姓名或职位"
534
+ allowClear
535
+ onSearch={setKeyword}
536
+ style={{ width: 220 }}
537
+ />
538
+ <Select
539
+ placeholder="按阶段筛选"
540
+ allowClear
541
+ value={stage}
542
+ onChange={setStage}
543
+ style={{ width: 140 }}
544
+ options={STAGES.map(({ value, label }) => ({ value, label }))}
545
+ />
546
+ </Flex>
547
+ <Text type="secondary">共 {fetchApi.data.totalCount} 位候选人</Text>
548
+ </Flex>
549
+ <Divider style={{ margin: 0 }} />
550
+ {children}
551
+ </Flex>
552
+ )}
553
+ >
554
+ {({ list }) => {
555
+ if (!list || list.length === 0) {
556
+ return (
557
+ <Flex justify="center" style={{ padding: 48 }}>
558
+ <Empty description="没有符合条件的候选人" />
559
+ </Flex>
560
+ );
561
+ }
562
+ if (isMobile) {
563
+ return (
564
+ <Flex vertical gap={12}>
565
+ {list.map(item => (
566
+ <CandidateRow key={item.id} item={item} />
567
+ ))}
568
+ </Flex>
569
+ );
570
+ }
571
+ return (
572
+ <Masonry
573
+ columns={{ xs: 1, sm: 2, lg: 3, xxl: 4 }}
574
+ gutter={12}
575
+ items={list.map(item => ({ key: item.id, data: item }))}
576
+ itemRender={({ data }) => <CandidateCard item={data} />}
577
+ />
578
+ );
579
+ }}
580
+ </FetchScrollLoader>
581
+ </Page>
582
+ </SystemLayout>
250
583
  );
251
584
  };
252
585
 
@@ -254,8 +587,8 @@ render(<BaseExample />);
254
587
 
255
588
  ```
256
589
 
257
- - 这里填写示例标题
258
- - 这里填写示例说明
590
+ - 图标字体
591
+ - 项目内置的图标字体列表
259
592
  - _fontList(./src/icons/fonts),(./src/icons/index),_modulesDev(@kne/modules-dev/dist/create-entry)
260
593
 
261
594
  ```jsx
@@ -270,29 +603,208 @@ render(<BaseExample />);
270
603
 
271
604
  ```
272
605
 
273
-
274
606
  ### API
275
607
 
608
+ ### SystemLayout
609
+
610
+ 系统初始化布局组件,提供页面整体结构,包含侧边菜单、用户信息、页面内容区和移动端工具栏。支持桌面端和移动端两种模式。
611
+
612
+ #### 属性
613
+
276
614
  | 属性 | 类型 | 默认值 | 说明 |
277
615
  |------|------|--------|------|
278
616
  | `className` | `string` | - | 自定义类名 |
279
- | `menu` | `object` | - | 菜单配置 |
280
- | `background` | `string` | `linear-gradient(0deg, #BBCFE7 0%, #BBCFE7 100%), #FFFFFF` | 背景样式 |
281
- | `menuMaxWidth` | `string` | `254px` | 菜单最大宽度 |
282
- | `menuMinWidth` | `string` | `84px` | 菜单最小宽度 |
283
- | `logo` | `object` | - | Logo 配置 |
284
- | `menuHeader` | `ReactNode` | - | 菜单头部内容 |
285
- | `userInfo` | `object` | - | 用户信息 |
286
- | `aiDialog` | `object` | `null` | AI 对话框配置 |
287
- | `children` | `ReactNode` | - | 子组件 |
288
-
289
- #### Menu 组件
617
+ | `menu` | `object` | - | 菜单配置对象,详见 Menu 配置 |
618
+ | `background` | `string` | `linear-gradient(0deg, #BBCFE7 0%, #BBCFE7 100%), #FFFFFF` | 布局背景样式 |
619
+ | `menuMaxWidth` | `string` | `254px` | 菜单展开时的最大宽度(桌面端) |
620
+ | `menuMinWidth` | `string` | `84px` | 菜单收起时的最小宽度(桌面端) |
621
+ | `logo` | `object` | 默认 Logo | Logo 配置,props 传递给 `@kne/react-file` 的 Image 组件 |
622
+ | `menuHeader` | `ReactNode` \| `function` | 默认 UserCard | 菜单头部内容,函数形式接收 `{ menuOpen, userCard }` 参数 |
623
+ | `userInfo` | `object` | - | 用户信息,详见 userInfo 配置 |
624
+ | `aiDialog` | `object` | `null` | AI 对话框配置,包含 `title` 和 `content` 字段(仅桌面端) |
625
+ | `openScrollbar` | `boolean` | - | 是否开启自定义滚动条(SimpleBar),默认桌面端开启、移动端关闭 |
626
+ | `isMobile` | `boolean` | - | 是否强制移动端模式,不设置时自动检测 |
627
+ | `toolbarTarget` | `HTMLElement` | `document.body` | 移动端工具栏 Portal 的目标容器 |
628
+ | `children` | `ReactNode` | - | 页面内容,通常为 Page 组件 |
629
+
630
+ #### userInfo 配置
631
+
632
+ | 属性 | 类型 | 说明 |
633
+ |------|------|------|
634
+ | `name` | `string` | 用户名 |
635
+ | `email` | `string` | 邮箱 |
636
+ | `avatar` | `string` | 头像 URL |
637
+ | `phone` | `string` | 手机号 |
638
+ | `description` | `string` | 描述信息 |
639
+ | `extra` | `ReactNode` | 额外展示内容 |
640
+
641
+ #### aiDialog 配置
642
+
643
+ | 属性 | 类型 | 说明 |
644
+ |------|------|------|
645
+ | `title` | `string` | 对话框标题 |
646
+ | `content` | `ReactNode` | 对话框内容 |
647
+
648
+ > AI 对话框支持三种状态:`closed`(关闭)、`small`(小窗口)、`inner`(内嵌面板)。仅桌面端可通过菜单底部入口按钮打开。
649
+
650
+ ---
651
+
652
+ ### Menu 配置
653
+
654
+ Menu 组件由 SystemLayout 的 `menu` 属性配置,不需要单独使用。
655
+
656
+ #### 属性
290
657
 
291
658
  | 属性 | 类型 | 默认值 | 说明 |
292
659
  |------|------|--------|------|
293
- | `className` | `string` | - | 自定义类名 |
294
- | `menuOpen` | `boolean` | - | 菜单是否展开 |
295
- | `items` | `array` | - | 菜单项配置 |
296
- | `activeKey` | `string` 或 `function` | - | 当前激活的菜单项 |
297
- | `base` | `string` | `''` | 基础路径 |
298
- | `onChange` | `function` | - | 菜单项点击回调 |
660
+ | `base` | `string` | `''` | 菜单基础路径,用于路由匹配时去除路径前缀 |
661
+ | `items` | `array` | - | 菜单项数组,详见 menu items 配置 |
662
+ | `activeKey` | `string` \| `function` | - | 当前激活项的 key,函数形式为 `(item, { menuOpen, base }) => boolean` |
663
+ | `onChange` | `function` | - | 菜单项点击回调 `(item, { menuOpen, base }) => void` |
664
+
665
+ #### menu items 配置
666
+
667
+ | 属性 | 类型 | 默认值 | 说明 |
668
+ |------|------|--------|------|
669
+ | `key` | `string` | `path` 或 `index` | 唯一标识 |
670
+ | `path` | `string` | - | 路由路径,点击后导航至 `base + path` |
671
+ | `label` | `string` | - | 显示文本 |
672
+ | `icon` | `string` \| `function` \| `ReactElement` \| `object` | - | 图标配置,详见 icon 配置 |
673
+ | `group` | `string` | - | 分组名称,相同 group 的项会归为一组 |
674
+ | `groupLabel` | `string` | - | 分组显示文本,默认使用 `group` |
675
+ | `toolbar` | `boolean` | `false` | 是否在移动端底部工具栏中显示 |
676
+ | `onClick` | `function` | - | 自定义点击回调 `(item, { menuOpen, base, event }) => void`,设置后不会自动导航 |
677
+
678
+ #### icon 配置
679
+
680
+ icon 支持多种格式:
681
+
682
+ - **string**:图标类型名,如 `'icon-assignment_ind'`,使用 `@kne/react-icon` 渲染
683
+ - **function**:`({ active, menuOpen }) => string`,根据激活状态和菜单展开状态返回图标名
684
+ - **ReactElement**:自定义图标元素
685
+ - **object**:`{ type: string, ... }`,props 传递给 Icon 组件
686
+
687
+ ---
688
+
689
+ ### Page
690
+
691
+ 页面内容区组件,作为 SystemLayout 的子组件使用,提供标题栏、操作按钮、返回按钮等功能。
692
+
693
+ #### 属性
694
+
695
+ | 属性 | 类型 | 默认值 | 说明 |
696
+ |------|------|--------|------|
697
+ | `title` | `ReactNode` | - | 页面标题 |
698
+ | `extra` | `ReactNode` | `null` | 标题栏右侧自定义内容,设置后替代 `buttonProps` |
699
+ | `back` | `boolean` | - | 是否显示返回按钮,点击调用 `navigate(-1)` |
700
+ | `buttonProps` | `object` | - | 操作按钮组配置,传递给 `@kne/button-group` 的 ButtonGroup 组件 |
701
+ | `toolbar` | `boolean` | `true` | 是否显示移动端底部工具栏 |
702
+ | `navbar` | `boolean` | `true` | 是否显示顶部导航栏 |
703
+ | `noPadding` | `boolean` | - | 是否移除内容区内边距 |
704
+ | `children` | `ReactNode` \| `function` | - | 页面内容 |
705
+
706
+ #### children 为函数时
707
+
708
+ 当 `children` 为函数时,Page 将渲染控制权交给子组件,函数接收以下参数:
709
+
710
+ | 参数 | 类型 | 说明 |
711
+ |------|------|------|
712
+ | `navbar` | `ReactElement` | 导航栏元素 |
713
+ | `className` | `string` | 内容区类名(包含 noPadding 等修饰类) |
714
+ | `render` | `function` | 标准渲染函数,`({ children, className }) => ReactElement`,用于包裹自定义内容以保持标准页面结构 |
715
+ | `pageLoading` | `ReactElement` | 加载状态元素(含骨架屏),可直接返回以显示加载状态 |
716
+
717
+ #### buttonProps 配置
718
+
719
+ | 属性 | 类型 | 说明 |
720
+ |------|------|------|
721
+ | `showLength` | `number` | 直接显示的按钮数量,其余收起到"更多"按钮中 |
722
+ | `list` | `array` | 按钮配置数组,每项为 antd Button 的 props |
723
+ | `moreType` | `string` | "更多"按钮的类型,移动端默认为 `'link'` |
724
+
725
+ #### Page.PageLoading
726
+
727
+ 加载状态子组件,展示 Skeleton 骨架屏。
728
+
729
+ ---
730
+
731
+ ### useLayoutContext
732
+
733
+ 获取 Layout 上下文的 Hook,在 SystemLayout 内部的子组件中使用。
734
+
735
+ ```js
736
+ import { useLayoutContext } from '@kne/system-layout';
737
+ const { setToolbarShow, setNavbarShow, setMenuOpen, deviceIsMobile, logo, userAvatar } = useLayoutContext();
738
+ ```
739
+
740
+ #### 返回值
741
+
742
+ | 属性 | 类型 | 说明 |
743
+ |------|------|------|
744
+ | `setToolbarShow` | `(show: boolean) => void` | 控制移动端底部工具栏显示/隐藏 |
745
+ | `setNavbarShow` | `(show: boolean) => void` | 控制移动端顶部导航栏显示/隐藏 |
746
+ | `setMenuOpen` | `(open: boolean \| function) => void` | 控制菜单展开/收起 |
747
+ | `deviceIsMobile` | `boolean` | 当前是否为移动端模式 |
748
+ | `logo` | `object` | Logo 配置对象 |
749
+ | `userAvatar` | `string` | 用户头像 URL |
750
+
751
+ ---
752
+
753
+ ### isMobile
754
+
755
+ 同步检测当前设备是否为移动端,基于 `window.matchMedia` 和移动端断点(768px)判断。
756
+
757
+ ```js
758
+ import { isMobile } from '@kne/system-layout';
759
+ const mobile = isMobile(); // boolean
760
+ ```
761
+
762
+ ### useIsMobile
763
+
764
+ 响应式移动端检测 Hook,窗口尺寸变化时自动更新。
765
+
766
+ ```js
767
+ import { useIsMobile } from '@kne/system-layout';
768
+ const mobile = useIsMobile(); // boolean
769
+ ```
770
+
771
+ ### MOBILE_BREAKPOINT
772
+
773
+ 移动端断点常量(`768px`),可与 `@kne/responsive-utils` 配合使用。
774
+
775
+ ```js
776
+ import { MOBILE_BREAKPOINT } from '@kne/system-layout';
777
+ ```
778
+
779
+ ---
780
+
781
+ ### themeToken
782
+
783
+ Ant Design 主题 Token 配置对象,将 `Input`、`InputNumber`、`Card`、`Tree`、`Select`、`DatePicker` 组件的背景色设为透明,适配 SystemLayout 的半透明背景风格。
784
+
785
+ ```js
786
+ import { themeToken } from '@kne/system-layout';
787
+ // 在 ConfigProvider 中使用
788
+ <ConfigProvider theme={themeToken}>
789
+ <SystemLayout>...</SystemLayout>
790
+ </ConfigProvider>
791
+ ```
792
+
793
+ ---
794
+
795
+ ### 响应式工具
796
+
797
+ 从 `@kne/responsive-utils` 透传的响应式工具,可通过 SystemLayout 直接引入:
798
+
799
+ | 导出名 | 说明 |
800
+ |--------|------|
801
+ | `ResponsiveProvider` | 响应式 Provider 组件 |
802
+ | `useBreakpoint` | 断点检测 Hook |
803
+ | `useMediaQuery` | 媒体查询 Hook |
804
+ | `usePopupContainer` | 弹层容器 Hook |
805
+ | `useScrollElement` | 滚动元素 Hook |
806
+ | `useResponsiveContext` | 响应式 Context Hook |
807
+ | `mobileBreakpoint` | 移动端断点常量(同 `MOBILE_BREAKPOINT`) |
808
+ | `RESPONSIVE_CONTAINER_CLASS` | 响应式容器 CSS 类名 |
809
+ | `RESPONSIVE_BOUNDARY_CLASS` | 响应式边界 CSS 类名 |
810
+ | `RESPONSIVE_SCROLL_CLASS` | 响应式滚动 CSS 类名 |