Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 196 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm i --save @kne/system-layout
- **页面组件(Page)**:提供标题栏、返回按钮、操作按钮组、加载状态等功能,支持函数式子组件自定义渲染。移动端标题栏滚动时自动收缩为胶囊样式。
- **菜单管理(Menu)**:支持分组、图标、自定义激活项、自定义点击回调,菜单项可配置是否在移动端工具栏中显示。
- **用户信息(UserCard)**:在菜单头部展示用户信息,支持头像、姓名、邮箱、手机号、描述等字段。
- **AI 对话框**:桌面端支持 AI 对话框小窗口和内嵌面板两种展示模式。
- **AI 对话框**:桌面端支持小窗口和内嵌面板两种展示模式;移动端提供可拖动的悬浮入口按钮,点击后全屏展示对话框。
- **响应式工具**:透传 `@kne/responsive-utils` 的响应式工具,支持断点检测、媒体查询、弹层容器等功能。
- **主题适配**:提供 `themeToken` 配置,将常用 Ant Design 组件背景色设为透明,适配半透明背景风格。

Expand Down Expand Up @@ -294,6 +294,197 @@ render(<BaseExample />);

```

- AI 对话框
- 桌面端 AI 招聘助手示例:通过左侧菜单底部入口唤起,支持小窗口(small)与内嵌面板(inner)两种形态。对话框内实现了完整的聊天交互——消息列表、快捷提问、模拟应答,贴近简历筛选、面试安排、JD 生成、Offer 评估等真实招聘业务场景
- _SystemLayout(@kne/current-lib_system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/current-lib_system-layout/dist/index.css),antd(antd)

```jsx
const { default: SystemLayout, Page } = _SystemLayout;
const { useState, useRef, useEffect } = React;
const { Flex, Card, Input, Button, Avatar, Tag, Typography, Alert, Spin } = antd;
const { Text, Paragraph } = Typography;

// 模拟招聘助手的知识库应答:根据关键字返回贴近业务的回复
const answerByKeyword = question => {
const text = question.toLowerCase();
if (text.includes('简历') || text.includes('筛选')) {
return '已为「高级前端工程师」筛选出 3 位高匹配候选人:\n1. 王伟 · 6 年经验 · React/TypeScript · 匹配度 92%\n2. 李静 · 5 年经验 · Vue/Node.js · 匹配度 87%\n3. 张磊 · 4 年经验 · React/微前端 · 匹配度 85%\n是否需要我自动发送面试邀约?';
}
if (text.includes('面试') || text.includes('安排') || text.includes('邀约')) {
return '我可以协助安排面试。候选人王伟本周可预约的时间为:\n· 周三 14:00-15:00\n· 周四 10:00-11:00\n面试官张总监周三下午有空,建议约在周三 14:00,是否确认发送日程邀请?';
}
if (text.includes('jd') || text.includes('职位') || text.includes('描述')) {
return '已根据「产品部-高级产品经理」生成 JD 草稿:\n【岗位职责】负责 B 端产品全生命周期管理,主导需求调研与方案设计;\n【任职要求】5 年以上 B 端产品经验,熟悉 SaaS 业务,具备良好的数据分析能力。\n需要我一键同步到招聘渠道吗?';
}
if (text.includes('offer') || text.includes('薪资')) {
return '根据候选人当前薪资(月薪 28K)与我司薪酬带宽,建议 Offer 方案:\n· 月薪 32K × 15 薪\n· 签字费 20K\n该方案在同级别薪酬 P60 分位,具备竞争力。是否生成 Offer 审批单?';
}
return '我是招聘助手小 K,可以帮你筛选简历、安排面试、生成 JD、评估 Offer。你可以试着问我「帮我筛选前端简历」或「安排王伟的面试」。';
};

// 快捷提问,模拟真实招聘场景中的高频操作
const QUICK_PROMPTS = ['帮我筛选高级前端工程师的简历', '安排候选人王伟的面试', '生成产品经理的 JD', '评估王伟的 Offer 薪资'];

const AIAssistant = () => {
const [messages, setMessages] = useState([
{
role: 'ai',
content: '你好,我是招聘助手小 K 👋\n本周你有 3 个职位在招、8 份简历待筛选。需要我帮你做点什么吗?'
}
]);
const [value, setValue] = useState('');
const [loading, setLoading] = useState(false);
const listRef = useRef(null);

useEffect(() => {
const el = listRef.current;
if (el) {
el.scrollTop = el.scrollHeight;
}
}, [messages, loading]);

const send = question => {
const content = (question ?? value).trim();
if (!content || loading) {
return;
}
setMessages(prev => [...prev, { role: 'user', content }]);
setValue('');
setLoading(true);
setTimeout(() => {
setMessages(prev => [...prev, { role: 'ai', content: answerByKeyword(content) }]);
setLoading(false);
}, 800);
};

return (
<Flex vertical style={{ height: '100%' }}>
<div ref={listRef} style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '12px 16px' }}>
<Flex vertical gap={16}>
{messages.map((msg, index) => (
<Flex key={index} gap={8} align="flex-start" justify={msg.role === 'user' ? 'flex-end' : 'flex-start'}>
{msg.role === 'ai' && <Avatar style={{ background: '#1677ff', flex: '0 0 auto' }}>K</Avatar>}
<div
style={{
maxWidth: '80%',
padding: '8px 12px',
borderRadius: 10,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
background: msg.role === 'user' ? '#1677ff' : 'rgba(255,255,255,0.9)',
color: msg.role === 'user' ? '#fff' : 'inherit'
}}
>
{msg.content}
</div>
{msg.role === 'user' && <Avatar style={{ background: '#52c41a', flex: '0 0 auto' }}>我</Avatar>}
</Flex>
))}
{loading && (
<Flex gap={8} align="center">
<Avatar style={{ background: '#1677ff', flex: '0 0 auto' }}>K</Avatar>
<div style={{ padding: '8px 12px', borderRadius: 10, background: 'rgba(255,255,255,0.9)' }}>
<Spin size="small" /> <Text type="secondary">小 K 正在思考…</Text>
</div>
</Flex>
)}
</Flex>
</div>

<Flex vertical gap={10} style={{ flex: '0 0 auto', padding: '8px 16px 12px' }}>
<Flex wrap="wrap" gap={8}>
{QUICK_PROMPTS.map(prompt => (
<Tag
key={prompt}
color="blue"
style={{ cursor: 'pointer', margin: 0, padding: '2px 10px', borderRadius: 12 }}
onClick={() => send(prompt)}
>
{prompt}
</Tag>
))}
</Flex>

<Flex gap={8}>
<Input.TextArea
value={value}
onChange={e => setValue(e.target.value)}
autoSize={{ minRows: 1, maxRows: 3 }}
placeholder="输入你的问题,回车发送…"
onPressEnter={e => {
e.preventDefault();
send();
}}
/>
<Button type="primary" loading={loading} onClick={() => send()}>
发送
</Button>
</Flex>
</Flex>
</Flex>
);
};

const menu = {
base: '/SystemLayout',
items: [
{ path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
{ group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
{ group: 'HIRING', path: '/hiring/application', label: 'Application List', icon: 'icon-assignment' },
{ group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' }
]
};

const BaseExample = () => {
return (
<SystemLayout
userInfo={{ name: 'Lucy L', email: 'lucy@company.com' }}
menu={menu}
aiDialog={{
title: '招聘助手 · 小 K',
content: <AIAssistant />
}}
>
<Page title="AI 招聘助手">
<Flex vertical gap={16}>
<Alert
type="info"
showIcon
message="如何唤起招聘助手"
description={
<span>
<b>桌面端</b>:点击左侧菜单<b>底部的 AI 入口图标</b>,对话框支持两种形态:
<br />· <b>小窗口(small)</b>:悬浮在左下角,不遮挡主内容;
<br />· <b>内嵌面板(inner)</b>:点击小窗口右上角的展开图标,助手会嵌入到页面左侧,与内容并排展示。
<br />
<b>移动端</b>:点击右下角<b>可拖动的悬浮按钮</b>(可自由拖拽调整位置),以全屏形式展示对话框。
</span>
}
/>
<Card title="招聘助手能做什么" styles={{ body: { padding: 20 } }} style={{ background: 'rgba(255,255,255,0.5)' }}>
<Flex vertical gap={12}>
<Paragraph style={{ margin: 0 }}>
招聘助手小 K 内置在布局中,可在任意页面随时调用,帮助 HR 高效处理日常招聘工作:
</Paragraph>
<Flex vertical gap={8}>
<Text>· 智能筛选简历,按岗位匹配度排序推荐候选人</Text>
<Text>· 协调面试官与候选人的空闲时间,一键发送日程邀约</Text>
<Text>· 根据部门和职级自动生成岗位 JD 草稿</Text>
<Text>· 结合薪酬带宽评估 Offer 方案,生成审批单</Text>
</Flex>
<Text type="secondary">提示:在对话框中试着输入「帮我筛选前端简历」或点击快捷提问标签。</Text>
</Flex>
</Card>
</Flex>
</Page>
</SystemLayout>
);
};

render(<BaseExample />);

```

- 滚动加载
- 结合 @kne/scroll-loader 在 Page 内实现整页下拉无限加载:候选人列表随页面滚动到底部自动加载下一页,支持关键字搜索与阶段筛选,并根据 useIsMobile 响应式切换布局——移动端整行显示,PC 端使用 antd Masonry 瀑布流多列展示(卡片高度不统一)
- _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)
Expand Down Expand Up @@ -621,7 +812,7 @@ render(<BaseExample />);
| `logo` | `object` | 默认 Logo | Logo 配置,props 传递给 `@kne/react-file` 的 Image 组件 |
| `menuHeader` | `ReactNode` \| `function` | 默认 UserCard | 菜单头部内容,函数形式接收 `{ menuOpen, userCard }` 参数 |
| `userInfo` | `object` | - | 用户信息,详见 userInfo 配置 |
| `aiDialog` | `object` | `null` | AI 对话框配置,包含 `title` 和 `content` 字段(仅桌面端) |
| `aiDialog` | `object` | `null` | AI 对话框配置,包含 `title` 和 `content` 字段 |
| `openScrollbar` | `boolean` | - | 是否开启自定义滚动条(SimpleBar),默认桌面端开启、移动端关闭 |
| `isMobile` | `boolean` | - | 是否强制移动端模式,不设置时自动检测 |
| `toolbarTarget` | `HTMLElement` | `document.body` | 移动端工具栏 Portal 的目标容器 |
Expand All @@ -645,7 +836,9 @@ render(<BaseExample />);
| `title` | `string` | 对话框标题 |
| `content` | `ReactNode` | 对话框内容 |

> AI 对话框支持三种状态:`closed`(关闭)、`small`(小窗口)、`inner`(内嵌面板)。仅桌面端可通过菜单底部入口按钮打开。
> AI 对话框在桌面端支持三种状态:`closed`(关闭)、`small`(小窗口)、`inner`(内嵌面板),通过菜单底部入口按钮打开;`small` 与 `inner` 之间可通过窗口右上角图标切换。
>
> 移动端入口为悬浮在右下角(工具栏上方)的可拖动按钮,点击后以全屏形式展示对话框,仅提供关闭按钮,不支持内嵌面板模式。

---

Expand Down
Loading