@kne/system-layout 0.2.0-alpha.2 → 0.2.0-alpha.21
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 +911 -206
- package/dist/{index-48821d71.js → index-8a15a9a8.js} +1 -1
- package/dist/{index-48821d71.js.map → index-8a15a9a8.js.map} +1 -1
- package/dist/index.css +838 -140
- package/dist/index.css.map +1 -1
- package/dist/index.js +997 -407
- package/dist/index.js.map +1 -1
- package/dist/index.modern.js +905 -379
- package/dist/index.modern.js.map +1 -1
- package/package.json +5 -2
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 对话框**:桌面端支持小窗口和内嵌面板两种展示模式;移动端提供可拖动的悬浮入口按钮,点击后全屏展示对话框。
|
|
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
|
-
|
|
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
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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,483 @@ 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
|
+
- AI 对话框
|
|
298
|
+
- 桌面端 AI 招聘助手示例:通过左侧菜单底部入口唤起,支持小窗口(small)与内嵌面板(inner)两种形态。对话框内实现了完整的聊天交互——消息列表、快捷提问、模拟应答,贴近简历筛选、面试安排、JD 生成、Offer 评估等真实招聘业务场景
|
|
299
|
+
- _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css),antd(antd)
|
|
154
300
|
|
|
155
301
|
```jsx
|
|
156
302
|
const { default: SystemLayout, Page } = _SystemLayout;
|
|
157
|
-
const {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
303
|
+
const { useState, useRef, useEffect } = React;
|
|
304
|
+
const { Flex, Card, Input, Button, Avatar, Tag, Typography, Alert, Spin } = antd;
|
|
305
|
+
const { Text, Paragraph } = Typography;
|
|
306
|
+
|
|
307
|
+
// 模拟招聘助手的知识库应答:根据关键字返回贴近业务的回复
|
|
308
|
+
const answerByKeyword = question => {
|
|
309
|
+
const text = question.toLowerCase();
|
|
310
|
+
if (text.includes('简历') || text.includes('筛选')) {
|
|
311
|
+
return '已为「高级前端工程师」筛选出 3 位高匹配候选人:\n1. 王伟 · 6 年经验 · React/TypeScript · 匹配度 92%\n2. 李静 · 5 年经验 · Vue/Node.js · 匹配度 87%\n3. 张磊 · 4 年经验 · React/微前端 · 匹配度 85%\n是否需要我自动发送面试邀约?';
|
|
312
|
+
}
|
|
313
|
+
if (text.includes('面试') || text.includes('安排') || text.includes('邀约')) {
|
|
314
|
+
return '我可以协助安排面试。候选人王伟本周可预约的时间为:\n· 周三 14:00-15:00\n· 周四 10:00-11:00\n面试官张总监周三下午有空,建议约在周三 14:00,是否确认发送日程邀请?';
|
|
315
|
+
}
|
|
316
|
+
if (text.includes('jd') || text.includes('职位') || text.includes('描述')) {
|
|
317
|
+
return '已根据「产品部-高级产品经理」生成 JD 草稿:\n【岗位职责】负责 B 端产品全生命周期管理,主导需求调研与方案设计;\n【任职要求】5 年以上 B 端产品经验,熟悉 SaaS 业务,具备良好的数据分析能力。\n需要我一键同步到招聘渠道吗?';
|
|
318
|
+
}
|
|
319
|
+
if (text.includes('offer') || text.includes('薪资')) {
|
|
320
|
+
return '根据候选人当前薪资(月薪 28K)与我司薪酬带宽,建议 Offer 方案:\n· 月薪 32K × 15 薪\n· 签字费 20K\n该方案在同级别薪酬 P60 分位,具备竞争力。是否生成 Offer 审批单?';
|
|
321
|
+
}
|
|
322
|
+
return '我是招聘助手小 K,可以帮你筛选简历、安排面试、生成 JD、评估 Offer。你可以试着问我「帮我筛选前端简历」或「安排王伟的面试」。';
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// 快捷提问,模拟真实招聘场景中的高频操作
|
|
326
|
+
const QUICK_PROMPTS = ['帮我筛选高级前端工程师的简历', '安排候选人王伟的面试', '生成产品经理的 JD', '评估王伟的 Offer 薪资'];
|
|
327
|
+
|
|
328
|
+
const AIAssistant = () => {
|
|
329
|
+
const [messages, setMessages] = useState([
|
|
330
|
+
{
|
|
331
|
+
role: 'ai',
|
|
332
|
+
content: '你好,我是招聘助手小 K 👋\n本周你有 3 个职位在招、8 份简历待筛选。需要我帮你做点什么吗?'
|
|
333
|
+
}
|
|
334
|
+
]);
|
|
335
|
+
const [value, setValue] = useState('');
|
|
336
|
+
const [loading, setLoading] = useState(false);
|
|
337
|
+
const listRef = useRef(null);
|
|
338
|
+
|
|
161
339
|
useEffect(() => {
|
|
162
|
-
|
|
163
|
-
|
|
340
|
+
const el = listRef.current;
|
|
341
|
+
if (el) {
|
|
342
|
+
el.scrollTop = el.scrollHeight;
|
|
343
|
+
}
|
|
344
|
+
}, [messages, loading]);
|
|
345
|
+
|
|
346
|
+
const send = question => {
|
|
347
|
+
const content = (question ?? value).trim();
|
|
348
|
+
if (!content || loading) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
setMessages(prev => [...prev, { role: 'user', content }]);
|
|
352
|
+
setValue('');
|
|
353
|
+
setLoading(true);
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
setMessages(prev => [...prev, { role: 'ai', content: answerByKeyword(content) }]);
|
|
356
|
+
setLoading(false);
|
|
357
|
+
}, 800);
|
|
358
|
+
};
|
|
359
|
+
|
|
164
360
|
return (
|
|
165
|
-
<
|
|
166
|
-
{
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
361
|
+
<Flex vertical style={{ height: '100%' }}>
|
|
362
|
+
<div ref={listRef} style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '12px 16px' }}>
|
|
363
|
+
<Flex vertical gap={16}>
|
|
364
|
+
{messages.map((msg, index) => (
|
|
365
|
+
<Flex key={index} gap={8} align="flex-start" justify={msg.role === 'user' ? 'flex-end' : 'flex-start'}>
|
|
366
|
+
{msg.role === 'ai' && <Avatar style={{ background: '#1677ff', flex: '0 0 auto' }}>K</Avatar>}
|
|
367
|
+
<div
|
|
368
|
+
style={{
|
|
369
|
+
maxWidth: '80%',
|
|
370
|
+
padding: '8px 12px',
|
|
371
|
+
borderRadius: 10,
|
|
372
|
+
whiteSpace: 'pre-wrap',
|
|
373
|
+
wordBreak: 'break-word',
|
|
374
|
+
background: msg.role === 'user' ? '#1677ff' : 'rgba(255,255,255,0.9)',
|
|
375
|
+
color: msg.role === 'user' ? '#fff' : 'inherit'
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
{msg.content}
|
|
379
|
+
</div>
|
|
380
|
+
{msg.role === 'user' && <Avatar style={{ background: '#52c41a', flex: '0 0 auto' }}>我</Avatar>}
|
|
381
|
+
</Flex>
|
|
382
|
+
))}
|
|
383
|
+
{loading && (
|
|
384
|
+
<Flex gap={8} align="center">
|
|
385
|
+
<Avatar style={{ background: '#1677ff', flex: '0 0 auto' }}>K</Avatar>
|
|
386
|
+
<div style={{ padding: '8px 12px', borderRadius: 10, background: 'rgba(255,255,255,0.9)' }}>
|
|
387
|
+
<Spin size="small" /> <Text type="secondary">小 K 正在思考…</Text>
|
|
388
|
+
</div>
|
|
389
|
+
</Flex>
|
|
390
|
+
)}
|
|
391
|
+
</Flex>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<Flex vertical gap={10} style={{ flex: '0 0 auto', padding: '8px 16px 12px' }}>
|
|
395
|
+
<Flex wrap="wrap" gap={8}>
|
|
396
|
+
{QUICK_PROMPTS.map(prompt => (
|
|
397
|
+
<Tag
|
|
398
|
+
key={prompt}
|
|
399
|
+
color="blue"
|
|
400
|
+
style={{ cursor: 'pointer', margin: 0, padding: '2px 10px', borderRadius: 12 }}
|
|
401
|
+
onClick={() => send(prompt)}
|
|
402
|
+
>
|
|
403
|
+
{prompt}
|
|
404
|
+
</Tag>
|
|
405
|
+
))}
|
|
406
|
+
</Flex>
|
|
407
|
+
|
|
408
|
+
<Flex gap={8}>
|
|
409
|
+
<Input.TextArea
|
|
410
|
+
value={value}
|
|
411
|
+
onChange={e => setValue(e.target.value)}
|
|
412
|
+
autoSize={{ minRows: 1, maxRows: 3 }}
|
|
413
|
+
placeholder="输入你的问题,回车发送…"
|
|
414
|
+
onPressEnter={e => {
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
send();
|
|
417
|
+
}}
|
|
418
|
+
/>
|
|
419
|
+
<Button type="primary" loading={loading} onClick={() => send()}>
|
|
420
|
+
发送
|
|
421
|
+
</Button>
|
|
422
|
+
</Flex>
|
|
423
|
+
</Flex>
|
|
424
|
+
</Flex>
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const menu = {
|
|
429
|
+
base: '/SystemLayout',
|
|
430
|
+
items: [
|
|
431
|
+
{ path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
|
|
432
|
+
{ group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
|
|
433
|
+
{ group: 'HIRING', path: '/hiring/application', label: 'Application List', icon: 'icon-assignment' },
|
|
434
|
+
{ group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' }
|
|
435
|
+
]
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const BaseExample = () => {
|
|
439
|
+
return (
|
|
440
|
+
<SystemLayout
|
|
441
|
+
userInfo={{ name: 'Lucy L', email: 'lucy@company.com' }}
|
|
442
|
+
menu={menu}
|
|
443
|
+
aiDialog={{
|
|
444
|
+
title: '招聘助手 · 小 K',
|
|
445
|
+
content: <AIAssistant />
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
448
|
+
<Page title="AI 招聘助手">
|
|
449
|
+
<Flex vertical gap={16}>
|
|
450
|
+
<Alert
|
|
451
|
+
type="info"
|
|
452
|
+
showIcon
|
|
453
|
+
message="如何唤起招聘助手"
|
|
454
|
+
description={
|
|
455
|
+
<span>
|
|
456
|
+
<b>桌面端</b>:点击左侧菜单<b>底部的 AI 入口图标</b>,对话框支持两种形态:
|
|
457
|
+
<br />· <b>小窗口(small)</b>:悬浮在左下角,不遮挡主内容;
|
|
458
|
+
<br />· <b>内嵌面板(inner)</b>:点击小窗口右上角的展开图标,助手会嵌入到页面左侧,与内容并排展示。
|
|
459
|
+
<br />
|
|
460
|
+
<b>移动端</b>:点击右下角<b>可拖动的悬浮按钮</b>(可自由拖拽调整位置),以全屏形式展示对话框。
|
|
461
|
+
</span>
|
|
462
|
+
}
|
|
463
|
+
/>
|
|
464
|
+
<Card title="招聘助手能做什么" styles={{ body: { padding: 20 } }} style={{ background: 'rgba(255,255,255,0.5)' }}>
|
|
465
|
+
<Flex vertical gap={12}>
|
|
466
|
+
<Paragraph style={{ margin: 0 }}>
|
|
467
|
+
招聘助手小 K 内置在布局中,可在任意页面随时调用,帮助 HR 高效处理日常招聘工作:
|
|
468
|
+
</Paragraph>
|
|
469
|
+
<Flex vertical gap={8}>
|
|
470
|
+
<Text>· 智能筛选简历,按岗位匹配度排序推荐候选人</Text>
|
|
471
|
+
<Text>· 协调面试官与候选人的空闲时间,一键发送日程邀约</Text>
|
|
472
|
+
<Text>· 根据部门和职级自动生成岗位 JD 草稿</Text>
|
|
473
|
+
<Text>· 结合薪酬带宽评估 Offer 方案,生成审批单</Text>
|
|
474
|
+
</Flex>
|
|
475
|
+
<Text type="secondary">提示:在对话框中试着输入「帮我筛选前端简历」或点击快捷提问标签。</Text>
|
|
476
|
+
</Flex>
|
|
477
|
+
</Card>
|
|
478
|
+
</Flex>
|
|
479
|
+
</Page>
|
|
480
|
+
</SystemLayout>
|
|
481
|
+
);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
render(<BaseExample />);
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
- 滚动加载
|
|
489
|
+
- 结合 @kne/scroll-loader 在 Page 内实现整页下拉无限加载:候选人列表随页面滚动到底部自动加载下一页,支持关键字搜索与阶段筛选,并根据 useIsMobile 响应式切换布局——移动端整行显示,PC 端使用 antd Masonry 瀑布流多列展示(卡片高度不统一)
|
|
490
|
+
- _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)
|
|
491
|
+
|
|
492
|
+
```jsx
|
|
493
|
+
const { default: SystemLayout, Page, useIsMobile } = _SystemLayout;
|
|
494
|
+
const { FetchScrollLoader } = _ScrollLoader;
|
|
495
|
+
const { Flex, Avatar, Tag, Typography, Input, Select, Progress, Empty, Divider, Masonry } = antd;
|
|
496
|
+
const { useState } = React;
|
|
497
|
+
const { Text } = Typography;
|
|
498
|
+
|
|
499
|
+
const STAGES = [
|
|
500
|
+
{ value: 'screening', label: '简历筛选', color: 'default' },
|
|
501
|
+
{ value: 'interview', label: '面试中', color: 'processing' },
|
|
502
|
+
{ value: 'offer', label: '已发 Offer', color: 'success' },
|
|
503
|
+
{ value: 'rejected', label: '未通过', color: 'error' }
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const POSITIONS = [
|
|
507
|
+
{ title: '高级前端工程师', dept: '技术部' },
|
|
508
|
+
{ title: '产品经理', dept: '产品部' },
|
|
509
|
+
{ title: 'UI/UX 设计师', dept: '设计部' },
|
|
510
|
+
{ title: '数据分析师', dept: '数据部' },
|
|
511
|
+
{ title: '增长运营专员', dept: '运营部' }
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
const FIRST_NAMES = ['伟', '芳', '娜', '秀英', '敏', '静', '强', '磊', '洋', '艳', '勇', '军', '杰', '娟', '涛', '明'];
|
|
515
|
+
const SURNAMES = ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴', '徐', '孙', '马', '朱', '胡', '林'];
|
|
516
|
+
|
|
517
|
+
const AVATAR_COLORS = ['#1677ff', '#52c41a', '#faad14', '#eb2f96', '#722ed1', '#13c2c2'];
|
|
518
|
+
|
|
519
|
+
const SKILLS = ['React', 'Vue', 'TypeScript', 'Node.js', 'Figma', '数据分析', 'Python', '项目管理', 'SQL', '增长策略', 'A/B 测试', 'UI 设计'];
|
|
520
|
+
|
|
521
|
+
// 长度不一的自我介绍,用于制造瀑布流所需的高度差异
|
|
522
|
+
const BIOS = [
|
|
523
|
+
'专注体验设计,擅长从 0 到 1 搭建产品。',
|
|
524
|
+
'有丰富的团队协作经验,能够快速融入并推动项目落地,善于在复杂需求中拆解优先级、平衡各方诉求并持续交付高质量成果。',
|
|
525
|
+
'注重数据驱动决策。',
|
|
526
|
+
'热爱技术钻研,长期关注前沿趋势,乐于分享与沉淀方法论,希望在更大的舞台上创造价值,同时帮助团队成员共同成长。',
|
|
527
|
+
''
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
// 模拟候选人列表接口:支持关键字搜索与阶段筛选,返回分页数据
|
|
531
|
+
const mockCandidateList = ({ data = {} }) => {
|
|
532
|
+
const { currentPage = 1, perPage = 12, keyword, stage } = data;
|
|
533
|
+
return new Promise(resolve => {
|
|
534
|
+
const allCandidates = Array.from({ length: 128 }, (_, i) => {
|
|
535
|
+
const position = POSITIONS[i % POSITIONS.length];
|
|
536
|
+
const stageInfo = STAGES[i % STAGES.length];
|
|
537
|
+
return {
|
|
538
|
+
id: i + 1,
|
|
539
|
+
name: `${SURNAMES[i % SURNAMES.length]}${FIRST_NAMES[(i * 3) % FIRST_NAMES.length]}`,
|
|
540
|
+
position: position.title,
|
|
541
|
+
department: position.dept,
|
|
542
|
+
stage: stageInfo.value,
|
|
543
|
+
matchScore: 60 + ((i * 7) % 40),
|
|
544
|
+
experience: 1 + (i % 12),
|
|
545
|
+
location: ['北京', '上海', '深圳', '杭州', '广州', '成都'][i % 6],
|
|
546
|
+
appliedAt: `2024-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
|
|
547
|
+
skills: SKILLS.slice((i * 2) % SKILLS.length, ((i * 2) % SKILLS.length) + 2 + (i % 4)),
|
|
548
|
+
bio: BIOS[i % BIOS.length]
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
let filtered = allCandidates;
|
|
553
|
+
if (keyword) {
|
|
554
|
+
filtered = filtered.filter(item => item.name.includes(keyword) || item.position.includes(keyword));
|
|
555
|
+
}
|
|
556
|
+
if (stage) {
|
|
557
|
+
filtered = filtered.filter(item => item.stage === stage);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const start = (currentPage - 1) * perPage;
|
|
561
|
+
const pageData = filtered.slice(start, start + perPage);
|
|
562
|
+
|
|
563
|
+
setTimeout(() => {
|
|
564
|
+
resolve({ totalCount: filtered.length, pageData });
|
|
565
|
+
}, 700);
|
|
566
|
+
});
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const getStageInfo = value => STAGES.find(item => item.value === value) || STAGES[0];
|
|
570
|
+
|
|
571
|
+
const getScoreColor = score => (score >= 90 ? '#52c41a' : score >= 75 ? '#1677ff' : '#faad14');
|
|
572
|
+
|
|
573
|
+
// 移动端:整行横向布局(头像 + 信息 + 匹配度)
|
|
574
|
+
const CandidateRow = ({ item }) => {
|
|
575
|
+
const stageInfo = getStageInfo(item.stage);
|
|
576
|
+
return (
|
|
577
|
+
<Flex
|
|
578
|
+
align="center"
|
|
579
|
+
gap={16}
|
|
580
|
+
style={{
|
|
581
|
+
padding: 16,
|
|
582
|
+
background: 'rgba(255, 255, 255, 0.6)',
|
|
583
|
+
border: '1px solid rgba(0, 0, 0, 0.06)',
|
|
584
|
+
borderRadius: 12
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
<Avatar size={48} style={{ backgroundColor: AVATAR_COLORS[item.id % AVATAR_COLORS.length], flex: 'none' }}>
|
|
588
|
+
{item.name.slice(0, 1)}
|
|
589
|
+
</Avatar>
|
|
590
|
+
<Flex vertical gap={6} flex={1} style={{ minWidth: 0 }}>
|
|
591
|
+
<Flex align="center" gap={8} wrap="wrap">
|
|
592
|
+
<Text strong style={{ fontSize: 15 }}>
|
|
593
|
+
{item.name}
|
|
594
|
+
</Text>
|
|
595
|
+
<Tag color={stageInfo.color}>{stageInfo.label}</Tag>
|
|
596
|
+
</Flex>
|
|
597
|
+
<Flex align="center" gap={8} wrap="wrap">
|
|
598
|
+
<Text type="secondary" style={{ fontSize: 13 }}>
|
|
599
|
+
{item.position}
|
|
600
|
+
</Text>
|
|
601
|
+
<Divider type="vertical" style={{ margin: 0 }} />
|
|
602
|
+
<Text type="secondary" style={{ fontSize: 13 }}>
|
|
603
|
+
{item.department}
|
|
604
|
+
</Text>
|
|
605
|
+
<Divider type="vertical" style={{ margin: 0 }} />
|
|
606
|
+
<Text type="secondary" style={{ fontSize: 13 }}>
|
|
607
|
+
{item.location} · {item.experience} 年经验
|
|
608
|
+
</Text>
|
|
609
|
+
</Flex>
|
|
610
|
+
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
611
|
+
投递于 {item.appliedAt}
|
|
612
|
+
</Text>
|
|
613
|
+
</Flex>
|
|
614
|
+
<Flex vertical align="center" gap={4} style={{ flex: 'none', width: 96 }}>
|
|
615
|
+
<Progress
|
|
616
|
+
type="circle"
|
|
617
|
+
size={52}
|
|
618
|
+
percent={item.matchScore}
|
|
619
|
+
strokeColor={getScoreColor(item.matchScore)}
|
|
620
|
+
format={percent => `${percent}`}
|
|
621
|
+
/>
|
|
622
|
+
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
623
|
+
匹配度
|
|
624
|
+
</Text>
|
|
625
|
+
</Flex>
|
|
626
|
+
</Flex>
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// PC 端:卡片纵向布局,用于瀑布流展示(内容长度不一,高度自然不统一)
|
|
631
|
+
const CandidateCard = ({ item }) => {
|
|
632
|
+
const stageInfo = getStageInfo(item.stage);
|
|
633
|
+
return (
|
|
634
|
+
<Flex
|
|
635
|
+
vertical
|
|
636
|
+
gap={12}
|
|
637
|
+
style={{
|
|
638
|
+
padding: 16,
|
|
639
|
+
background: 'rgba(255, 255, 255, 0.6)',
|
|
640
|
+
border: '1px solid rgba(0, 0, 0, 0.06)',
|
|
641
|
+
borderRadius: 12
|
|
642
|
+
}}
|
|
643
|
+
>
|
|
644
|
+
<Flex align="center" gap={12}>
|
|
645
|
+
<Avatar size={44} style={{ backgroundColor: AVATAR_COLORS[item.id % AVATAR_COLORS.length], flex: 'none' }}>
|
|
646
|
+
{item.name.slice(0, 1)}
|
|
647
|
+
</Avatar>
|
|
648
|
+
<Flex vertical gap={4} flex={1} style={{ minWidth: 0 }}>
|
|
649
|
+
<Text strong ellipsis style={{ fontSize: 15 }}>
|
|
650
|
+
{item.name}
|
|
651
|
+
</Text>
|
|
652
|
+
<Tag color={stageInfo.color} style={{ margin: 0, width: 'fit-content' }}>
|
|
653
|
+
{stageInfo.label}
|
|
654
|
+
</Tag>
|
|
655
|
+
</Flex>
|
|
656
|
+
<Progress
|
|
657
|
+
type="circle"
|
|
658
|
+
size={48}
|
|
659
|
+
percent={item.matchScore}
|
|
660
|
+
strokeColor={getScoreColor(item.matchScore)}
|
|
661
|
+
format={percent => `${percent}`}
|
|
662
|
+
/>
|
|
663
|
+
</Flex>
|
|
664
|
+
<Flex vertical gap={4}>
|
|
665
|
+
<Text type="secondary" ellipsis style={{ fontSize: 13 }}>
|
|
666
|
+
{item.position} · {item.department}
|
|
667
|
+
</Text>
|
|
668
|
+
<Text type="secondary" style={{ fontSize: 13 }}>
|
|
669
|
+
{item.location} · {item.experience} 年经验
|
|
670
|
+
</Text>
|
|
671
|
+
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
672
|
+
投递于 {item.appliedAt}
|
|
673
|
+
</Text>
|
|
674
|
+
</Flex>
|
|
675
|
+
{item.bio ? (
|
|
676
|
+
<Text style={{ fontSize: 13 }}>{item.bio}</Text>
|
|
677
|
+
) : null}
|
|
678
|
+
<Flex gap={6} wrap="wrap">
|
|
679
|
+
{item.skills.map(skill => (
|
|
680
|
+
<Tag key={skill} style={{ margin: 0 }}>
|
|
681
|
+
{skill}
|
|
682
|
+
</Tag>
|
|
683
|
+
))}
|
|
684
|
+
</Flex>
|
|
685
|
+
</Flex>
|
|
686
|
+
);
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const menu = {
|
|
690
|
+
base: '/SystemLayout',
|
|
691
|
+
items: [
|
|
692
|
+
{ path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
|
|
693
|
+
{ group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
|
|
694
|
+
{ group: 'HIRING', path: '/hiring/application', label: 'Application List', toolbar: true, icon: 'icon-assignment' },
|
|
695
|
+
{ group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' }
|
|
696
|
+
]
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const BaseExample = () => {
|
|
700
|
+
const isMobile = useIsMobile();
|
|
701
|
+
const [keyword, setKeyword] = useState('');
|
|
702
|
+
const [stage, setStage] = useState(undefined);
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<SystemLayout userInfo={{ name: 'Lucy L', email: 'lucy@company.com' }} menu={menu}>
|
|
706
|
+
<Page title="候选人列表">
|
|
707
|
+
<FetchScrollLoader
|
|
708
|
+
useSimpleBar={false}
|
|
709
|
+
api={{ loader: mockCandidateList }}
|
|
710
|
+
searchProps={{ keyword, stage }}
|
|
711
|
+
getSearchProps={props => {
|
|
712
|
+
const result = {};
|
|
713
|
+
if (props.keyword) result.keyword = props.keyword;
|
|
714
|
+
if (props.stage) result.stage = props.stage;
|
|
715
|
+
return result;
|
|
173
716
|
}}
|
|
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
|
-
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>
|
|
717
|
+
pagination={{ paramsType: 'data', current: 'currentPage', pageSizeName: 'perPage', pageSize: 12 }}
|
|
718
|
+
completeTips="— 已加载全部候选人 —"
|
|
719
|
+
render={({ fetchApi, children }) => (
|
|
720
|
+
<Flex vertical gap={16}>
|
|
721
|
+
<Flex justify="space-between" align="center" wrap="wrap" gap={12}>
|
|
722
|
+
<Flex gap={12} align="center" wrap="wrap">
|
|
723
|
+
<Input.Search
|
|
724
|
+
placeholder="搜索姓名或职位"
|
|
725
|
+
allowClear
|
|
726
|
+
onSearch={setKeyword}
|
|
727
|
+
style={{ width: 220 }}
|
|
728
|
+
/>
|
|
729
|
+
<Select
|
|
730
|
+
placeholder="按阶段筛选"
|
|
731
|
+
allowClear
|
|
732
|
+
value={stage}
|
|
733
|
+
onChange={setStage}
|
|
734
|
+
style={{ width: 140 }}
|
|
735
|
+
options={STAGES.map(({ value, label }) => ({ value, label }))}
|
|
736
|
+
/>
|
|
737
|
+
</Flex>
|
|
738
|
+
<Text type="secondary">共 {fetchApi.data.totalCount} 位候选人</Text>
|
|
739
|
+
</Flex>
|
|
740
|
+
<Divider style={{ margin: 0 }} />
|
|
741
|
+
{children}
|
|
742
|
+
</Flex>
|
|
743
|
+
)}
|
|
744
|
+
>
|
|
745
|
+
{({ list }) => {
|
|
746
|
+
if (!list || list.length === 0) {
|
|
747
|
+
return (
|
|
748
|
+
<Flex justify="center" style={{ padding: 48 }}>
|
|
749
|
+
<Empty description="没有符合条件的候选人" />
|
|
750
|
+
</Flex>
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
if (isMobile) {
|
|
754
|
+
return (
|
|
755
|
+
<Flex vertical gap={12}>
|
|
756
|
+
{list.map(item => (
|
|
757
|
+
<CandidateRow key={item.id} item={item} />
|
|
758
|
+
))}
|
|
759
|
+
</Flex>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
return (
|
|
763
|
+
<Masonry
|
|
764
|
+
columns={{ xs: 1, sm: 2, lg: 3, xxl: 4 }}
|
|
765
|
+
gutter={12}
|
|
766
|
+
items={list.map(item => ({ key: item.id, data: item }))}
|
|
767
|
+
itemRender={({ data }) => <CandidateCard item={data} />}
|
|
768
|
+
/>
|
|
769
|
+
);
|
|
770
|
+
}}
|
|
771
|
+
</FetchScrollLoader>
|
|
772
|
+
</Page>
|
|
773
|
+
</SystemLayout>
|
|
250
774
|
);
|
|
251
775
|
};
|
|
252
776
|
|
|
@@ -254,8 +778,8 @@ render(<BaseExample />);
|
|
|
254
778
|
|
|
255
779
|
```
|
|
256
780
|
|
|
257
|
-
-
|
|
258
|
-
-
|
|
781
|
+
- 图标字体
|
|
782
|
+
- 项目内置的图标字体列表
|
|
259
783
|
- _fontList(./src/icons/fonts),(./src/icons/index),_modulesDev(@kne/modules-dev/dist/create-entry)
|
|
260
784
|
|
|
261
785
|
```jsx
|
|
@@ -270,29 +794,210 @@ render(<BaseExample />);
|
|
|
270
794
|
|
|
271
795
|
```
|
|
272
796
|
|
|
273
|
-
|
|
274
797
|
### API
|
|
275
798
|
|
|
799
|
+
### SystemLayout
|
|
800
|
+
|
|
801
|
+
系统初始化布局组件,提供页面整体结构,包含侧边菜单、用户信息、页面内容区和移动端工具栏。支持桌面端和移动端两种模式。
|
|
802
|
+
|
|
803
|
+
#### 属性
|
|
804
|
+
|
|
276
805
|
| 属性 | 类型 | 默认值 | 说明 |
|
|
277
806
|
|------|------|--------|------|
|
|
278
807
|
| `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` |
|
|
284
|
-
| `menuHeader` | `ReactNode` |
|
|
285
|
-
| `userInfo` | `object` | - |
|
|
286
|
-
| `aiDialog` | `object` | `null` | AI
|
|
287
|
-
| `
|
|
288
|
-
|
|
289
|
-
|
|
808
|
+
| `menu` | `object` | - | 菜单配置对象,详见 Menu 配置 |
|
|
809
|
+
| `background` | `string` | `linear-gradient(0deg, #BBCFE7 0%, #BBCFE7 100%), #FFFFFF` | 布局背景样式 |
|
|
810
|
+
| `menuMaxWidth` | `string` | `254px` | 菜单展开时的最大宽度(桌面端) |
|
|
811
|
+
| `menuMinWidth` | `string` | `84px` | 菜单收起时的最小宽度(桌面端) |
|
|
812
|
+
| `logo` | `object` | 默认 Logo | Logo 配置,props 传递给 `@kne/react-file` 的 Image 组件 |
|
|
813
|
+
| `menuHeader` | `ReactNode` \| `function` | 默认 UserCard | 菜单头部内容,函数形式接收 `{ menuOpen, userCard }` 参数 |
|
|
814
|
+
| `userInfo` | `object` | - | 用户信息,详见 userInfo 配置 |
|
|
815
|
+
| `aiDialog` | `object` | `null` | AI 对话框配置,包含 `title` 和 `content` 字段 |
|
|
816
|
+
| `openScrollbar` | `boolean` | - | 是否开启自定义滚动条(SimpleBar),默认桌面端开启、移动端关闭 |
|
|
817
|
+
| `isMobile` | `boolean` | - | 是否强制移动端模式,不设置时自动检测 |
|
|
818
|
+
| `toolbarTarget` | `HTMLElement` | `document.body` | 移动端工具栏 Portal 的目标容器 |
|
|
819
|
+
| `children` | `ReactNode` | - | 页面内容,通常为 Page 组件 |
|
|
820
|
+
|
|
821
|
+
#### userInfo 配置
|
|
822
|
+
|
|
823
|
+
| 属性 | 类型 | 说明 |
|
|
824
|
+
|------|------|------|
|
|
825
|
+
| `name` | `string` | 用户名 |
|
|
826
|
+
| `email` | `string` | 邮箱 |
|
|
827
|
+
| `avatar` | `string` | 头像 URL |
|
|
828
|
+
| `phone` | `string` | 手机号 |
|
|
829
|
+
| `description` | `string` | 描述信息 |
|
|
830
|
+
| `extra` | `ReactNode` | 额外展示内容 |
|
|
831
|
+
|
|
832
|
+
#### aiDialog 配置
|
|
833
|
+
|
|
834
|
+
| 属性 | 类型 | 说明 |
|
|
835
|
+
|------|------|------|
|
|
836
|
+
| `title` | `string` | 对话框标题 |
|
|
837
|
+
| `content` | `ReactNode` | 对话框内容 |
|
|
838
|
+
|
|
839
|
+
> AI 对话框在桌面端支持三种状态:`closed`(关闭)、`small`(小窗口)、`inner`(内嵌面板),通过菜单底部入口按钮打开;`small` 与 `inner` 之间可通过窗口右上角图标切换。
|
|
840
|
+
>
|
|
841
|
+
> 移动端入口为悬浮在右下角(工具栏上方)的可拖动按钮,点击后以全屏形式展示对话框,仅提供关闭按钮,不支持内嵌面板模式。
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
### Menu 配置
|
|
846
|
+
|
|
847
|
+
Menu 组件由 SystemLayout 的 `menu` 属性配置,不需要单独使用。
|
|
848
|
+
|
|
849
|
+
#### 属性
|
|
290
850
|
|
|
291
851
|
| 属性 | 类型 | 默认值 | 说明 |
|
|
292
852
|
|------|------|--------|------|
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
|
|
298
|
-
|
|
853
|
+
| `base` | `string` | `''` | 菜单基础路径,用于路由匹配时去除路径前缀 |
|
|
854
|
+
| `items` | `array` | - | 菜单项数组,详见 menu items 配置 |
|
|
855
|
+
| `activeKey` | `string` \| `function` | - | 当前激活项的 key,函数形式为 `(item, { menuOpen, base }) => boolean` |
|
|
856
|
+
| `onChange` | `function` | - | 菜单项点击回调 `(item, { menuOpen, base }) => void` |
|
|
857
|
+
|
|
858
|
+
#### menu items 配置
|
|
859
|
+
|
|
860
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
861
|
+
|------|------|--------|------|
|
|
862
|
+
| `key` | `string` | `path` 或 `index` | 唯一标识 |
|
|
863
|
+
| `path` | `string` | - | 路由路径,点击后导航至 `base + path` |
|
|
864
|
+
| `label` | `string` | - | 显示文本 |
|
|
865
|
+
| `icon` | `string` \| `function` \| `ReactElement` \| `object` | - | 图标配置,详见 icon 配置 |
|
|
866
|
+
| `group` | `string` | - | 分组名称,相同 group 的项会归为一组 |
|
|
867
|
+
| `groupLabel` | `string` | - | 分组显示文本,默认使用 `group` |
|
|
868
|
+
| `toolbar` | `boolean` | `false` | 是否在移动端底部工具栏中显示 |
|
|
869
|
+
| `onClick` | `function` | - | 自定义点击回调 `(item, { menuOpen, base, event }) => void`,设置后不会自动导航 |
|
|
870
|
+
|
|
871
|
+
#### icon 配置
|
|
872
|
+
|
|
873
|
+
icon 支持多种格式:
|
|
874
|
+
|
|
875
|
+
- **string**:图标类型名,如 `'icon-assignment_ind'`,使用 `@kne/react-icon` 渲染
|
|
876
|
+
- **function**:`({ active, menuOpen }) => string`,根据激活状态和菜单展开状态返回图标名
|
|
877
|
+
- **ReactElement**:自定义图标元素
|
|
878
|
+
- **object**:`{ type: string, ... }`,props 传递给 Icon 组件
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
### Page
|
|
883
|
+
|
|
884
|
+
页面内容区组件,作为 SystemLayout 的子组件使用,提供标题栏、操作按钮、返回按钮等功能。
|
|
885
|
+
|
|
886
|
+
#### 属性
|
|
887
|
+
|
|
888
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
889
|
+
|------|------|--------|------|
|
|
890
|
+
| `title` | `ReactNode` | - | 页面标题 |
|
|
891
|
+
| `extra` | `ReactNode` | `null` | 标题栏右侧自定义内容,设置后替代 `buttonProps` |
|
|
892
|
+
| `back` | `boolean` | - | 是否显示返回按钮,点击调用 `navigate(-1)` |
|
|
893
|
+
| `buttonProps` | `object` | - | 操作按钮组配置,传递给 `@kne/button-group` 的 ButtonGroup 组件 |
|
|
894
|
+
| `toolbar` | `boolean` | `true` | 是否显示移动端底部工具栏 |
|
|
895
|
+
| `navbar` | `boolean` | `true` | 是否显示顶部导航栏 |
|
|
896
|
+
| `noPadding` | `boolean` | - | 是否移除内容区内边距 |
|
|
897
|
+
| `children` | `ReactNode` \| `function` | - | 页面内容 |
|
|
898
|
+
|
|
899
|
+
#### children 为函数时
|
|
900
|
+
|
|
901
|
+
当 `children` 为函数时,Page 将渲染控制权交给子组件,函数接收以下参数:
|
|
902
|
+
|
|
903
|
+
| 参数 | 类型 | 说明 |
|
|
904
|
+
|------|------|------|
|
|
905
|
+
| `navbar` | `ReactElement` | 导航栏元素 |
|
|
906
|
+
| `className` | `string` | 内容区类名(包含 noPadding 等修饰类) |
|
|
907
|
+
| `render` | `function` | 标准渲染函数,`({ children, className }) => ReactElement`,用于包裹自定义内容以保持标准页面结构 |
|
|
908
|
+
| `pageLoading` | `ReactElement` | 加载状态元素(含骨架屏),可直接返回以显示加载状态 |
|
|
909
|
+
|
|
910
|
+
#### buttonProps 配置
|
|
911
|
+
|
|
912
|
+
| 属性 | 类型 | 说明 |
|
|
913
|
+
|------|------|------|
|
|
914
|
+
| `showLength` | `number` | 直接显示的按钮数量,其余收起到"更多"按钮中 |
|
|
915
|
+
| `list` | `array` | 按钮配置数组,每项为 antd Button 的 props |
|
|
916
|
+
| `moreType` | `string` | "更多"按钮的类型,移动端默认为 `'link'` |
|
|
917
|
+
|
|
918
|
+
#### Page.PageLoading
|
|
919
|
+
|
|
920
|
+
加载状态子组件,展示 Skeleton 骨架屏。
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
### useLayoutContext
|
|
925
|
+
|
|
926
|
+
获取 Layout 上下文的 Hook,在 SystemLayout 内部的子组件中使用。
|
|
927
|
+
|
|
928
|
+
```js
|
|
929
|
+
import { useLayoutContext } from '@kne/system-layout';
|
|
930
|
+
const { setToolbarShow, setNavbarShow, setMenuOpen, deviceIsMobile, logo, userAvatar } = useLayoutContext();
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
#### 返回值
|
|
934
|
+
|
|
935
|
+
| 属性 | 类型 | 说明 |
|
|
936
|
+
|------|------|------|
|
|
937
|
+
| `setToolbarShow` | `(show: boolean) => void` | 控制移动端底部工具栏显示/隐藏 |
|
|
938
|
+
| `setNavbarShow` | `(show: boolean) => void` | 控制移动端顶部导航栏显示/隐藏 |
|
|
939
|
+
| `setMenuOpen` | `(open: boolean \| function) => void` | 控制菜单展开/收起 |
|
|
940
|
+
| `deviceIsMobile` | `boolean` | 当前是否为移动端模式 |
|
|
941
|
+
| `logo` | `object` | Logo 配置对象 |
|
|
942
|
+
| `userAvatar` | `string` | 用户头像 URL |
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
### isMobile
|
|
947
|
+
|
|
948
|
+
同步检测当前设备是否为移动端,基于 `window.matchMedia` 和移动端断点(768px)判断。
|
|
949
|
+
|
|
950
|
+
```js
|
|
951
|
+
import { isMobile } from '@kne/system-layout';
|
|
952
|
+
const mobile = isMobile(); // boolean
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### useIsMobile
|
|
956
|
+
|
|
957
|
+
响应式移动端检测 Hook,窗口尺寸变化时自动更新。
|
|
958
|
+
|
|
959
|
+
```js
|
|
960
|
+
import { useIsMobile } from '@kne/system-layout';
|
|
961
|
+
const mobile = useIsMobile(); // boolean
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
### MOBILE_BREAKPOINT
|
|
965
|
+
|
|
966
|
+
移动端断点常量(`768px`),可与 `@kne/responsive-utils` 配合使用。
|
|
967
|
+
|
|
968
|
+
```js
|
|
969
|
+
import { MOBILE_BREAKPOINT } from '@kne/system-layout';
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
### themeToken
|
|
975
|
+
|
|
976
|
+
Ant Design 主题 Token 配置对象,将 `Input`、`InputNumber`、`Card`、`Tree`、`Select`、`DatePicker` 组件的背景色设为透明,适配 SystemLayout 的半透明背景风格。
|
|
977
|
+
|
|
978
|
+
```js
|
|
979
|
+
import { themeToken } from '@kne/system-layout';
|
|
980
|
+
// 在 ConfigProvider 中使用
|
|
981
|
+
<ConfigProvider theme={themeToken}>
|
|
982
|
+
<SystemLayout>...</SystemLayout>
|
|
983
|
+
</ConfigProvider>
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
---
|
|
987
|
+
|
|
988
|
+
### 响应式工具
|
|
989
|
+
|
|
990
|
+
从 `@kne/responsive-utils` 透传的响应式工具,可通过 SystemLayout 直接引入:
|
|
991
|
+
|
|
992
|
+
| 导出名 | 说明 |
|
|
993
|
+
|--------|------|
|
|
994
|
+
| `ResponsiveProvider` | 响应式 Provider 组件 |
|
|
995
|
+
| `useBreakpoint` | 断点检测 Hook |
|
|
996
|
+
| `useMediaQuery` | 媒体查询 Hook |
|
|
997
|
+
| `usePopupContainer` | 弹层容器 Hook |
|
|
998
|
+
| `useScrollElement` | 滚动元素 Hook |
|
|
999
|
+
| `useResponsiveContext` | 响应式 Context Hook |
|
|
1000
|
+
| `mobileBreakpoint` | 移动端断点常量(同 `MOBILE_BREAKPOINT`) |
|
|
1001
|
+
| `RESPONSIVE_CONTAINER_CLASS` | 响应式容器 CSS 类名 |
|
|
1002
|
+
| `RESPONSIVE_BOUNDARY_CLASS` | 响应式边界 CSS 类名 |
|
|
1003
|
+
| `RESPONSIVE_SCROLL_CLASS` | 响应式滚动 CSS 类名 |
|