人员管理页面开发

master
jiangxucong 2 months ago
parent 95a20db11c
commit 88aeb60d11

@ -31,6 +31,12 @@ export default [
name: 'staffstatistics',
component: './multi-d-statistics/MultiDStatistics',
},
{
path: '/topnavbar00/multidstatistics/performance',
// icon: 'bank',
name: 'performance',
component: './performance/Performance',
},
],
},
// 人员管理
@ -45,6 +51,42 @@ export default [
name: 'staffinfo',
component: './staffmanage_staffinfo/StaffInfo',
},
{
path: '/topnavbar00/staffmanage/onboarding',
// icon: 'bank',
name: 'onboarding',
component: './staffmanage_onboarding/OnBoarding',
},
{
path: '/topnavbar00/staffmanage/onboardingregister',
// icon: 'bank',
name: 'onboardingregister',
component: './staffmanage_onboardingregister/OnBoardingRegister',
},
{
path: '/topnavbar00/staffmanage/onboardingapproval',
// icon: 'bank',
name: 'onboardingapproval',
component: './staffmanage_onboardingapproval/OnBoardingApproval',
},
{
path: '/topnavbar00/staffmanage/resignation',
// icon: 'bank',
name: 'resignation',
component: './staffmanage_resignation/Resignation',
},
{
path: '/topnavbar00/staffmanage/training',
// icon: 'bank',
name: 'training',
component: './staffmanage_training/Training',
},
{
path: '/topnavbar00/staffmanage/trainingplan',
// icon: 'bank',
name: 'trainingplan',
component: './staffmanage_trainingplan/TrainingPlan',
},
{
path: '/topnavbar00/staffmanage/staffresume',
// icon: 'bank',
@ -63,7 +105,7 @@ export default [
name: 'staffresume',
component: './staffmanage_staffresume/StaffResume',
},
{
{
path: '/topnavbar00/staffmanage/particulars',
// icon: 'bank',
name: 'particulars',
@ -83,6 +125,12 @@ export default [
name: 'organchart',
component: './organmanage_organchart/OrganChart',
},
{
path: '/topnavbar00/organmanage/reportrelation',
// icon: 'bank',
name: 'reportrelation',
component: './organmanage_reportrelation/ReportRelation',
},
{
path: '/topnavbar00/organmanage/deptmaintain',
// icon: 'bank',
@ -101,7 +149,13 @@ export default [
path: '/topnavbar00/attendancemanage/attendancedata',
// icon: 'bank',
name: 'attendancedata',
component: './attendancemanage_attendancedata/attendancemanageAttendancedata',
component: './attendancemanage_attendancedata/AttendancemanageAttendancedata',
},
{
path: '/topnavbar00/attendancemanage/attendancedetails',
// icon: 'bank',
name: 'attendancedetails',
component: './attendancemanage_attendancedetails/Attendancedetails',
},
],
},
@ -149,6 +203,12 @@ export default [
name: 'salaryruleset',
component: './salarymanage_salaryruleset/SalaryRuleset',
},
{
path: '/topnavbar00/salarymanage/salarydetail',
// icon: 'bank',
name: 'salarydetail',
component: './salarymanage_salarydetail/SalaryDetail',
},
],
},
//效率管理

@ -31,7 +31,7 @@
}
.ant-btn-primary {
background: rgba(36, 114, 214, 1);
// background: rgba(36, 114, 214, 1);
}
// top导航样式开始

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,838 @@
import React, { PureComponent } from 'react';
import {
Card,
Tree,
Input,
Button,
DatePicker,
Select,
Row,
Col,
Space,
Radio,
Checkbox,
InputNumber,
Avatar,
message,
Typography
} from 'antd';
import {
SaveOutlined,
CloseOutlined,
PlusCircleOutlined,
DeleteOutlined,
ExpandOutlined,
UserOutlined,
TeamOutlined,
ApartmentOutlined,
SyncOutlined,
DollarOutlined,
SettingOutlined,
CalendarOutlined,
UsergroupAddOutlined,
UserSwitchOutlined,
StopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ContactsOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import styles from './OnBoarding.less';
const { Option } = Select;
const { TextArea } = Input;
const { Title } = Typography;
class OnBoarding extends PureComponent {
constructor(props) {
super(props);
this.state = {
selectedOrgKeys: ['0-0-4'], // 默认选中人事部
expandedKeys: ['0-0', '0-0-0', '0-0-1', '0-0-2'],
// 入职计划表单数据
onboardingForm: {
// 入职部门信息
departmentName: '人力资源部',
departmentManager: '张明远',
currentStaff: '24/30',
// 计划时段设置
planStartDate: dayjs('2023-09-01'),
planEndDate: dayjs('2023-09-30'),
// 可入职岗位设置
availablePositions: [
{ id: 1, name: '人力资源专员', count: 3, checked: true },
{ id: 2, name: '薪酬福利专员', count: 2, checked: true },
{ id: 3, name: '培训发展专员', count: 1, checked: false },
{ id: 4, name: '招聘专员', count: 2, checked: false }
],
// 计划入职人数
totalPlan: 5,
confirmedCount: 2,
remainingCount: 3,
// 人员要求
ageRange: '22-30岁',
gender: '不限',
education: '本科及以上',
workYears: '1-3年',
otherRequirements: '',
// 黑名单
blacklist: [
{ id: 1, name: '林晓梅', idCard: '310***19880512****', avatar: null },
{ id: 2, name: '王建国', idCard: '420***19900215****', avatar: null }
],
// 白名单
whitelist: [
{ id: 1, name: '陈思思', recommender: '张明远', avatar: null }
]
},
organizationData: [
{
title: '飞利信科技有限公司',
key: '0-0',
count: 356,
children: [
{
title: '技术部',
key: '0-0-0',
count: 120,
children: [
{ title: '前端组', key: '0-0-0-0', count: 45 },
{ title: '后端组', key: '0-0-0-1', count: 52 },
{ title: '测试组', key: '0-0-0-2', count: 23 }
],
},
{
title: '产品部',
key: '0-0-1',
count: 68,
children: [
{ title: '产品设计组', key: '0-0-1-0', count: 28 },
{ title: '用户体验组', key: '0-0-1-1', count: 25 },
{ title: '产品运营组', key: '0-0-1-2', count: 15 }
],
},
{
title: '运营部',
key: '0-0-2',
count: 52,
children: [
{ title: '市场营销组', key: '0-0-2-0', count: 22 },
{ title: '客户服务组', key: '0-0-2-1', count: 18 },
{ title: '商务合作组', key: '0-0-2-2', count: 12 }
],
},
{
title: '财务部',
key: '0-0-3',
count: 32,
children: [
{ title: '会计组', key: '0-0-3-0', count: 18 },
{ title: '审计组', key: '0-0-3-1', count: 14 }
],
},
{
title: '人事部',
key: '0-0-4',
count: 84,
children: [
{ title: 'HR专员组', key: '0-0-4-0', count: 25 },
{ title: '招聘组', key: '0-0-4-1', count: 22 },
{ title: '培训组', key: '0-0-4-2', count: 18 },
{ title: '薪酬组', key: '0-0-4-3', count: 19 }
],
},
],
},
]
};
}
// 获取处理后的树形数据
getTreeData = () => {
const { organizationData } = this.state;
const processNode = (node) => ({
key: node.key,
title: (
<span className={styles['tree-node-title']}>
{this.getNodeIcon(node.title)}
<span className={styles['node-title']}>{node.title}</span>
<span className={styles['node-count']}>({node.count})</span>
</span>
),
children: node.children ? node.children.map(processNode) : undefined
});
return organizationData.map(processNode);
};
// 获取节点图标
getNodeIcon = (title) => {
if (title.includes('公司') || title.includes('集团')) return <ApartmentOutlined style={{ color: '#1890ff', fontSize: '14px' }} />;
if (title.includes('技术') || title.includes('开发') || title.includes('测试')) return <SettingOutlined style={{ color: '#52c41a', fontSize: '14px' }} />;
if (title.includes('产品') || title.includes('设计') || title.includes('体验')) return <TeamOutlined style={{ color: '#fa8c16', fontSize: '14px' }} />;
if (title.includes('运营') || title.includes('市场') || title.includes('客户') || title.includes('商务')) return <TeamOutlined style={{ color: '#eb2f96', fontSize: '14px' }} />;
if (title.includes('财务') || title.includes('会计') || title.includes('审计')) return <DollarOutlined style={{ color: '#722ed1', fontSize: '14px' }} />;
if (title.includes('人事') || title.includes('HR') || title.includes('招聘') || title.includes('培训') || title.includes('薪酬')) return <UserOutlined style={{ color: '#13c2c2', fontSize: '14px' }} />;
return <TeamOutlined style={{ color: '#666', fontSize: '14px' }} />;
};
// 刷新树数据
handleTreeRefresh = () => {
message.info('刷新组织架构数据');
};
// 展开/收缩所有节点
handleTreeToggle = () => {
const { expandedKeys, organizationData } = this.state;
if (expandedKeys.length > 0) {
this.setState({ expandedKeys: [] });
} else {
const getAllKeys = (nodes) => {
let keys = [];
nodes.forEach(node => {
keys.push(node.key);
if (node.children) {
keys = keys.concat(getAllKeys(node.children));
}
});
return keys;
};
this.setState({ expandedKeys: getAllKeys(organizationData) });
}
};
// 组织树选择
onTreeSelect = (selectedKeys, info) => {
console.log('选中节点:', selectedKeys, info);
this.setState({ selectedOrgKeys: selectedKeys });
if (selectedKeys.length > 0) {
const selectedKey = selectedKeys[0];
const findNodeByKey = (nodes, key) => {
for (let node of nodes) {
if (node.key === key) {
return node;
}
if (node.children) {
const found = findNodeByKey(node.children, key);
if (found) return found;
}
}
return null;
};
const selectedNode = findNodeByKey(this.state.organizationData, selectedKey);
if (selectedNode) {
this.handleOnboardingFormChange('departmentName', selectedNode.title);
const departmentInfo = this.getDepartmentInfo(selectedNode.title);
this.setState(prevState => ({
onboardingForm: {
...prevState.onboardingForm,
...departmentInfo
}
}));
}
}
};
// 获取部门信息
getDepartmentInfo = (departmentName) => {
const departmentMap = {
'技术部': {
departmentManager: '李技术',
currentStaff: '115/120',
availablePositions: [
{ id: 1, name: '前端工程师', count: 2, checked: true },
{ id: 2, name: '后端工程师', count: 3, checked: true },
{ id: 3, name: '测试工程师', count: 1, checked: false }
]
},
'产品部': {
departmentManager: '王产品',
currentStaff: '65/68',
availablePositions: [
{ id: 1, name: '产品经理', count: 1, checked: true },
{ id: 2, name: '产品设计师', count: 2, checked: true }
]
},
'人事部': {
departmentManager: '张明远',
currentStaff: '24/30',
availablePositions: [
{ id: 1, name: '人力资源专员', count: 3, checked: true },
{ id: 2, name: '薪酬福利专员', count: 2, checked: true },
{ id: 3, name: '培训发展专员', count: 1, checked: false },
{ id: 4, name: '招聘专员', count: 2, checked: false }
]
},
'财务部': {
departmentManager: '赵财务',
currentStaff: '30/32',
availablePositions: [
{ id: 1, name: '会计', count: 1, checked: true },
{ id: 2, name: '出纳', count: 1, checked: false }
]
},
'运营部': {
departmentManager: '钱运营',
currentStaff: '48/52',
availablePositions: [
{ id: 1, name: '市场专员', count: 2, checked: true },
{ id: 2, name: '客服专员', count: 1, checked: true }
]
}
};
return departmentMap[departmentName] || {
departmentManager: '未知',
currentStaff: '0/0',
availablePositions: []
};
};
// 树展开/收缩
onTreeExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
// 入职表单字段变化
handleOnboardingFormChange = (field, value) => {
this.setState(prevState => ({
onboardingForm: {
...prevState.onboardingForm,
[field]: value
}
}));
};
// 岗位选择变化
handlePositionChange = (id, checked) => {
const { onboardingForm } = this.state;
const newPositions = onboardingForm.availablePositions.map(pos =>
pos.id === id ? { ...pos, checked } : pos
);
this.setState({
onboardingForm: {
...onboardingForm,
availablePositions: newPositions
}
});
};
// 添加新岗位
addNewPosition = () => {
const { onboardingForm } = this.state;
const newPosition = {
id: Date.now(),
name: '新岗位',
count: 1,
checked: false
};
this.setState({
onboardingForm: {
...onboardingForm,
availablePositions: [...onboardingForm.availablePositions, newPosition]
}
});
message.success('新岗位添加成功');
};
// 移除黑名单人员
removeFromBlacklist = (id) => {
const { onboardingForm } = this.state;
const newBlacklist = onboardingForm.blacklist.filter(person => person.id !== id);
this.setState({
onboardingForm: {
...onboardingForm,
blacklist: newBlacklist
}
});
message.success('已移除黑名单人员');
};
// 移除白名单人员
removeFromWhitelist = (id) => {
const { onboardingForm } = this.state;
const newWhitelist = onboardingForm.whitelist.filter(person => person.id !== id);
this.setState({
onboardingForm: {
...onboardingForm,
whitelist: newWhitelist
}
});
message.success('已移除白名单人员');
};
// 添加黑名单人员
addToBlacklist = () => {
message.info('功能开发中,敬请期待');
};
// 添加白名单人员
addToWhitelist = () => {
message.info('功能开发中,敬请期待');
};
// 保存设置
handleSave = () => {
const { onboardingForm } = this.state;
if (!onboardingForm.departmentName) {
message.error('请选择部门');
return;
}
if (!onboardingForm.planStartDate || !onboardingForm.planEndDate) {
message.error('请选择计划时段');
return;
}
if (onboardingForm.planStartDate.isAfter(onboardingForm.planEndDate)) {
message.error('开始日期不能晚于结束日期');
return;
}
if (onboardingForm.totalPlan <= 0) {
message.error('计划总人数必须大于0');
return;
}
console.log('保存入职计划设置:', onboardingForm);
message.success('入职计划设置保存成功');
};
// 取消操作
handleCancel = () => {
message.info('已取消操作');
};
render() {
const { selectedOrgKeys, expandedKeys, onboardingForm } = this.state;
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<Card className={styles.contentCard}>
<Row gutter={24}>
{/* 左侧组织架构树 - 保持不变 */}
<Col span={5}>
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TeamOutlined style={{ marginRight: 8, color: '#1890ff' }} />
<span>组织架构</span>
</div>
<Space>
<Button
type="text"
icon={<SyncOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeRefresh}
title="刷新"
/>
<Button
type="text"
icon={<ExpandOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeToggle}
title={expandedKeys.length > 0 ? "收缩全部" : "展开全部"}
/>
</Space>
</div>
}
className={styles.treeCard}
>
<div className={styles.treeContainer}>
<Tree
showIcon
selectedKeys={selectedOrgKeys}
expandedKeys={expandedKeys}
treeData={this.getTreeData()}
onSelect={this.onTreeSelect}
onExpand={this.onTreeExpand}
className={styles.orgTree}
/>
</div>
</Card>
</Col>
{/* 右侧入职计划设置 - 替换为HTML内容 */}
<Col span={19}>
<Card className={styles.mainCard}>
{/* 头部标题 */}
<div className={styles.header}>
<Title level={3} style={{ margin: 0, color: '#333', fontSize: '18px' }}>
入职计划设置
</Title>
<Space>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={this.handleSave}
>
保存设置
</Button>
<Button
icon={<CloseOutlined />}
onClick={this.handleCancel}
>
取消
</Button>
</Space>
</div>
{/* 表单内容区 */}
<div className={styles.content}>
{/* 入职部门信息 */}
<Card
title={
<span>
<ApartmentOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
入职部门信息
</span>
}
size="small"
className={styles.formCard}
>
<Row gutter={16}>
<Col span={8}>
<div className={styles.formItem}>
<label>部门名称</label>
<div className={styles.readOnlyField}>
{onboardingForm.departmentName}
</div>
</div>
</Col>
<Col span={8}>
<div className={styles.formItem}>
<label>部门负责人</label>
<div className={styles.readOnlyField}>
{onboardingForm.departmentManager}
</div>
</div>
</Col>
<Col span={8}>
<div className={styles.formItem}>
<label>当前在岗人数</label>
<div className={styles.readOnlyField}>
{onboardingForm.currentStaff}
</div>
</div>
</Col>
</Row>
</Card>
{/* 计划时段设置 */}
<Card
title={
<span>
<CalendarOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
计划时段设置
</span>
}
size="small"
className={styles.formCard}
>
<Row gutter={16}>
<Col span={12}>
<div className={styles.formItem}>
<label>计划开始日期</label>
<DatePicker
value={onboardingForm.planStartDate}
style={{ width: '100%' }}
onChange={(date) => this.handleOnboardingFormChange('planStartDate', date)}
placeholder="选择开始日期"
/>
</div>
</Col>
<Col span={12}>
<div className={styles.formItem}>
<label>计划结束日期</label>
<DatePicker
value={onboardingForm.planEndDate}
style={{ width: '100%' }}
onChange={(date) => this.handleOnboardingFormChange('planEndDate', date)}
placeholder="选择结束日期"
/>
</div>
</Col>
</Row>
</Card>
{/* 可入职岗位设置 */}
<Card
title={
<span>
<ContactsOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
可入职岗位设置
</span>
}
size="small"
className={styles.formCard}
>
<div className={styles.positionList}>
{onboardingForm.availablePositions.map(position => (
<div key={position.id} className={styles.positionItem}>
<Checkbox
checked={position.checked}
onChange={(e) => this.handlePositionChange(position.id, e.target.checked)}
>
{position.name} ({position.count})
</Checkbox>
</div>
))}
<Button
type="link"
icon={<PlusCircleOutlined />}
onClick={this.addNewPosition}
className={styles.addButton}
>
添加其他岗位
</Button>
</div>
</Card>
{/* 计划入职人数 */}
<Card
title={
<span>
<UsergroupAddOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
计划入职人数
</span>
}
size="small"
className={styles.formCard}
>
<Row gutter={16}>
<Col span={8}>
<div className={styles.formItem}>
<label>计划总人数</label>
<InputNumber
value={onboardingForm.totalPlan}
min={0}
style={{ width: '100%' }}
onChange={(value) => this.handleOnboardingFormChange('totalPlan', value)}
placeholder="请输入计划总人数"
/>
</div>
</Col>
<Col span={8}>
<div className={styles.formItem}>
<label>已确认人数</label>
<InputNumber
value={onboardingForm.confirmedCount}
disabled
style={{ width: '100%' }}
/>
</div>
</Col>
<Col span={8}>
<div className={styles.formItem}>
<label>剩余名额</label>
<InputNumber
value={onboardingForm.remainingCount}
disabled
style={{ width: '100%' }}
/>
</div>
</Col>
</Row>
</Card>
{/* 人员要求 */}
<Card
title={
<span>
<UserSwitchOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
人员要求
</span>
}
size="small"
className={styles.formCard}
>
<div className={styles.requirementSection}>
<div className={styles.formItem}>
<label style={{display: 'block',marginRight: '10px'}}>年龄段</label>
<Radio.Group
value={onboardingForm.ageRange}
onChange={(e) => this.handleOnboardingFormChange('ageRange', e.target.value)}
>
<Radio value="22-30岁">22-30</Radio>
<Radio value="30-35岁">30-35</Radio>
<Radio value="不限">不限</Radio>
</Radio.Group>
</div>
<div className={styles.formItem}>
<label style={{display: 'block', marginRight: '10px'}}>性别</label>
<Radio.Group
value={onboardingForm.gender}
onChange={(e) => this.handleOnboardingFormChange('gender', e.target.value)}
>
<Radio value="男"></Radio>
<Radio value="女"></Radio>
<Radio value="不限">不限</Radio>
</Radio.Group>
</div>
<Row gutter={16}>
<Col span={12}>
<div className={styles.formItem}>
<label>学历要求</label>
<Select
value={onboardingForm.education}
style={{ width: '100%' }}
onChange={(value) => this.handleOnboardingFormChange('education', value)}
placeholder="请选择学历要求"
>
<Option value="本科及以上">本科及以上</Option>
<Option value="硕士及以上">硕士及以上</Option>
<Option value="大专及以上">大专及以上</Option>
<Option value="不限">不限</Option>
</Select>
</div>
</Col>
<Col span={12}>
<div className={styles.formItem}>
<label>工作年限</label>
<Select
value={onboardingForm.workYears}
style={{ width: '100%' }}
onChange={(value) => this.handleOnboardingFormChange('workYears', value)}
placeholder="请选择工作年限"
>
<Option value="1-3年">1-3</Option>
<Option value="3-5年">3-5</Option>
<Option value="5年以上">5年以上</Option>
<Option value="不限">不限</Option>
</Select>
</div>
</Col>
</Row>
<div className={styles.formItem}>
<label>其他要求</label>
<TextArea
value={onboardingForm.otherRequirements}
rows={3}
placeholder="请输入其他特殊要求..."
onChange={(e) => this.handleOnboardingFormChange('otherRequirements', e.target.value)}
/>
</div>
</div>
</Card>
{/* 黑名单设置 */}
<Card
title={
<span>
<StopOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
黑名单设置
</span>
}
size="small"
className={styles.formCard}
>
<div className={styles.personList}>
{onboardingForm.blacklist.map(person => (
<div key={person.id} className={styles.personItem}>
<div className={styles.personInfo}>
<Avatar
src={person.avatar}
size={32}
icon={!person.avatar ? <UserOutlined /> : undefined}
/>
<div className={styles.personDetails}>
<div className={styles.personName}>{person.name}</div>
<div className={styles.personMeta}>身份证: {person.idCard}</div>
</div>
</div>
<Button
type="link"
danger
size="small"
icon={<CloseCircleOutlined />}
onClick={() => this.removeFromBlacklist(person.id)}
>
移除
</Button>
</div>
))}
<Button
type="link"
icon={<PlusCircleOutlined />}
onClick={this.addToBlacklist}
className={styles.addButton}
>
添加黑名单人员
</Button>
</div>
</Card>
{/* 白名单设置 */}
<Card
title={
<span>
<CheckCircleOutlined style={{ marginRight: 8, color: '#2D5CF6' }} />
白名单设置
</span>
}
size="small"
className={styles.formCard}
>
<div className={styles.personList}>
{onboardingForm.whitelist.map(person => (
<div key={person.id} className={styles.personItem}>
<div className={styles.personInfo}>
<Avatar
src={person.avatar}
size={32}
icon={!person.avatar ? <UserOutlined /> : undefined}
/>
<div className={styles.personDetails}>
<div className={styles.personName}>{person.name}</div>
<div className={styles.personMeta}>推荐人: {person.recommender}</div>
</div>
</div>
<Button
type="link"
danger
size="small"
icon={<CloseCircleOutlined />}
onClick={() => this.removeFromWhitelist(person.id)}
>
移除
</Button>
</div>
))}
<Button
type="link"
icon={<PlusCircleOutlined />}
onClick={this.addToWhitelist}
className={styles.addButton}
>
添加白名单人员
</Button>
</div>
</Card>
</div>
</Card>
</Col>
</Row>
</Card>
</div>
</div>
);
}
}
export default OnBoarding;

@ -0,0 +1,366 @@
@import '~@/utils/utils.less';
// 容器样式
.container {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background-color: #f5f6fa;
}
// 主内容区域
.mainContent {
height: 100vh;
overflow: hidden;
.contentCard {
height: 100%;
border: none !important;
:global(.ant-card-body) {
padding: 12px 24px;
// height: calc(100vh - 24px);
// overflow: hidden;
}
}
}
// 组织架构树卡片样式
.treeCard {
height: 600px;
border: 1px solid #e8e8e8;
border-radius: 8px;
.treeContainer {
height: 520px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
.orgTree {
.ant-tree-node-content-wrapper {
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px 8px;
&:hover {
background-color: #f0f9ff;
transform: translateX(2px);
}
&.ant-tree-node-selected {
background-color: #e6f7ff !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
}
}
}
/* Tree节点标题样式 */
.tree-node-title {
display: flex;
align-items: center;
width: 100%;
.node-title {
flex: 1;
font-size: 14px;
margin-left: 8px;
color: #333;
}
.node-count {
color: #999;
font-size: 12px;
margin-left: auto;
background: #f0f0f0;
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
}
// 主要内容卡片样式
.mainCard {
height: calc(100vh - 60px);
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
.ant-card-body {
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
// 头部区域
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
height: 64px;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// 内容区域
.content {
flex: 1;
padding: 20px 24px 28px 24px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 关键允许flex子元素收缩
height: calc(100vh - 180px); // 减去头部高度和其他空间,强制设置高度
max-height: calc(100vh - 180px); // 明确最大高度,确保滚动条出现
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.28);
}
}
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.18) rgba(0, 0, 0, 0.02);
}
// 表单卡片
.formCard {
margin-bottom: 20px;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&:last-child {
margin-bottom: 0;
}
.ant-card-head {
border-bottom: 1px solid #f0f0f0;
.ant-card-head-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
}
.ant-card-body {
padding: 20px;
}
}
// 表单项
.formItem {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
label {
// display: block;
font-weight: 500;
color: #333;
font-size: 14px;
margin-bottom: 6px;
}
}
.readOnlyField {
background-color: #f9fafb;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
color: #374151;
font-size: 14px;
}
/* 岗位列表 */
.positionList {
.positionItem {
margin-bottom: 12px;
}
.addButton {
color: #2d5cf6;
padding: 0;
height: auto;
font-size: 14px;
&:hover {
color: #1d4ed8;
}
}
}
/* 人员要求区域 */
.requirementSection {
.formItem {
margin-bottom: 20px;
.ant-radio-group {
display: flex;
gap: 16px;
}
}
}
/* 人员列表 */
.personList {
.personItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-bottom: 12px;
transition: all 0.2s;
&:hover {
border-color: #d1d5db;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
}
}
.personInfo {
display: flex;
align-items: center;
gap: 12px;
}
.personDetails {
.personName {
font-size: 14px;
font-weight: 500;
color: #262626;
margin-bottom: 2px;
}
.personMeta {
font-size: 12px;
color: #6b7280;
}
}
.addButton {
color: #2d5cf6;
padding: 0;
height: auto;
font-size: 14px;
&:hover {
color: #1d4ed8;
}
}
}
// 响应式设计
@media (max-width: 1200px) {
.treeCard {
height: 300px;
}
.mainCard {
height: auto;
max-height: 80vh;
min-height: 500px;
.content {
max-height: 65vh;
&::-webkit-scrollbar {
width: 4px;
}
}
}
}
// 页面特定样式
.container {
.ant-btn-primary {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
border-radius: 6px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(45, 92, 246, 0.3);
transition: all 0.3s ease;
&:hover,
&:focus {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.4);
}
}
.ant-btn:not(.ant-btn-primary) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover,
&:focus {
border-color: #4c7bff;
color: #2d5cf6;
}
}
.ant-input,
.ant-select-selector,
.ant-picker {
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
border-color: #4c7bff;
}
&:focus,
&.ant-select-focused .ant-select-selector,
&.ant-picker-focused {
border-color: #2d5cf6;
box-shadow: 0 0 0 2px rgba(45, 92, 246, 0.2);
}
}
}

@ -0,0 +1,585 @@
import React, { PureComponent } from 'react';
import {
Card,
Tree,
Input,
Button,
DatePicker,
Select,
Row,
Col,
Space,
Radio,
Checkbox,
InputNumber,
Avatar,
message,
Timeline,
Tag,
Typography,
Divider
} from 'antd';
import {
SaveOutlined,
CloseOutlined,
PlusCircleOutlined,
DeleteOutlined,
ExpandOutlined,
UserOutlined,
TeamOutlined,
ApartmentOutlined,
SyncOutlined,
DollarOutlined,
SettingOutlined,
CalendarOutlined,
UsergroupAddOutlined,
UserSwitchOutlined,
StopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ContactsOutlined,
PrinterOutlined,
CheckOutlined,
ClockCircleOutlined,
LoadingOutlined,
EyeOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import styles from './OnBoardingApproval.less';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text, Paragraph } = Typography;
class OnBoardingApproval extends PureComponent {
constructor(props) {
super(props);
this.state = {
selectedOrgKeys: ['0-0-4'], // 默认选中人事部
expandedKeys: ['0-0', '0-0-0', '0-0-1', '0-0-2'],
// 审批详情数据
approvalData: {
applicant: {
name: '张三',
gender: '男',
age: 28,
phone: '138****5678',
department: '产品中心 - 用户体验部',
position: '高级产品经理',
joinDate: '2023-11-15',
salary: '¥18,000/月',
status: '审批中',
avatar: '/img/avatar.jpg',
idCard: '/img/card.jpg'
},
education: [
{
period: '2014.09 - 2018.06',
school: '北京大学',
major: '信息管理与信息系统',
degree: '本科'
},
{
period: '2018.09 - 2021.06',
school: '清华大学',
major: '工商管理',
degree: '硕士'
}
],
workExperience: [
{
period: '2021.07 - 2023.04',
company: '字节跳动',
position: '产品经理'
},
{
period: '2023.05 - 2023.10',
company: '腾讯科技',
position: '高级产品经理'
}
],
approvalFlow: [
{
step: '提交申请',
status: 'completed',
operator: '张三',
time: '2023-10-20 09:30',
comment: '申请人张三提交了入职申请'
},
{
step: '部门初审',
status: 'completed',
operator: '张伟',
time: '2023-10-21 14:15',
comment: '部门经理张伟审核通过',
opinion: '候选人专业背景和工作经验符合岗位要求,建议录用'
},
{
step: '部门复审',
status: 'completed',
operator: '李强',
time: '2023-10-23 10:45',
comment: '产品总监李强审核通过',
opinion: '面试表现优秀,专业能力突出,同意录用'
},
{
step: 'HR审核',
status: 'completed',
operator: '王芳',
time: '2023-10-25 16:20',
comment: 'HRBP王芳审核通过',
opinion: '背景调查无异常,薪资符合公司标准,同意录用'
},
{
step: '总经理审批',
status: 'processing',
operator: '赵明',
time: '待审批',
comment: '等待总经理赵明审批'
},
{
step: '入职生效',
status: 'waiting',
time: '待处理',
comment: '审批通过后自动生效'
},
{
step: '审批完结',
status: 'waiting',
time: '待处理',
comment: '流程自动归档'
}
]
},
organizationData: [
{
title: '飞利信科技有限公司',
key: '0-0',
count: 356,
children: [
{
title: '技术部',
key: '0-0-0',
count: 120,
children: [
{ title: '前端组', key: '0-0-0-0', count: 45 },
{ title: '后端组', key: '0-0-0-1', count: 52 },
{ title: '测试组', key: '0-0-0-2', count: 23 }
],
},
{
title: '产品部',
key: '0-0-1',
count: 68,
children: [
{ title: '产品设计组', key: '0-0-1-0', count: 28 },
{ title: '用户体验组', key: '0-0-1-1', count: 25 },
{ title: '产品运营组', key: '0-0-1-2', count: 15 }
],
},
{
title: '运营部',
key: '0-0-2',
count: 52,
children: [
{ title: '市场营销组', key: '0-0-2-0', count: 22 },
{ title: '客户服务组', key: '0-0-2-1', count: 18 },
{ title: '商务合作组', key: '0-0-2-2', count: 12 }
],
},
{
title: '财务部',
key: '0-0-3',
count: 32,
children: [
{ title: '会计组', key: '0-0-3-0', count: 18 },
{ title: '审计组', key: '0-0-3-1', count: 14 }
],
},
{
title: '人事部',
key: '0-0-4',
count: 84,
children: [
{ title: 'HR专员组', key: '0-0-4-0', count: 25 },
{ title: '招聘组', key: '0-0-4-1', count: 22 },
{ title: '培训组', key: '0-0-4-2', count: 18 },
{ title: '薪酬组', key: '0-0-4-3', count: 19 }
],
},
],
},
]
};
}
// 获取处理后的树形数据
getTreeData = () => {
const { organizationData } = this.state;
const processNode = (node) => ({
key: node.key,
title: (
<span className={styles['tree-node-title']}>
{this.getNodeIcon(node.title)}
<span className={styles['node-title']}>{node.title}</span>
<span className={styles['node-count']}>({node.count})</span>
</span>
),
children: node.children ? node.children.map(processNode) : undefined
});
return organizationData.map(processNode);
};
// 获取节点图标
getNodeIcon = (title) => {
if (title.includes('公司') || title.includes('集团')) return <ApartmentOutlined style={{ color: '#1890ff', fontSize: '14px' }} />;
if (title.includes('技术') || title.includes('开发') || title.includes('测试')) return <SettingOutlined style={{ color: '#52c41a', fontSize: '14px' }} />;
if (title.includes('产品') || title.includes('设计') || title.includes('体验')) return <TeamOutlined style={{ color: '#fa8c16', fontSize: '14px' }} />;
if (title.includes('运营') || title.includes('市场') || title.includes('客户') || title.includes('商务')) return <TeamOutlined style={{ color: '#eb2f96', fontSize: '14px' }} />;
if (title.includes('财务') || title.includes('会计') || title.includes('审计')) return <DollarOutlined style={{ color: '#722ed1', fontSize: '14px' }} />;
if (title.includes('人事') || title.includes('HR') || title.includes('招聘') || title.includes('培训') || title.includes('薪酬')) return <UserOutlined style={{ color: '#13c2c2', fontSize: '14px' }} />;
return <TeamOutlined style={{ color: '#666', fontSize: '14px' }} />;
};
// 刷新树数据
handleTreeRefresh = () => {
message.info('刷新组织架构数据');
};
// 展开/收缩所有节点
handleTreeToggle = () => {
const { expandedKeys, organizationData } = this.state;
if (expandedKeys.length > 0) {
this.setState({ expandedKeys: [] });
} else {
const getAllKeys = (nodes) => {
let keys = [];
nodes.forEach(node => {
keys.push(node.key);
if (node.children) {
keys = keys.concat(getAllKeys(node.children));
}
});
return keys;
};
this.setState({ expandedKeys: getAllKeys(organizationData) });
}
};
// 组织树选择
onTreeSelect = (selectedKeys, info) => {
console.log('选中节点:', selectedKeys, info);
this.setState({ selectedOrgKeys: selectedKeys });
};
// 树展开/收缩
onTreeExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
// 审批通过
handleApprove = () => {
message.success('审批通过,已提交到下一审批环节');
};
// 审批拒绝
handleReject = () => {
message.error('审批已拒绝');
};
// 打印
handlePrint = () => {
window.print();
};
// 获取审批状态图标
getStatusIcon = (status) => {
switch (status) {
case 'completed':
return <CheckCircleOutlined className={styles.completedIcon} />;
case 'processing':
return <LoadingOutlined className={styles.processingIcon} />;
case 'waiting':
return <ClockCircleOutlined className={styles.waitingIcon} />;
default:
return <ClockCircleOutlined className={styles.waitingIcon} />;
}
};
// 获取状态标签
getStatusTag = (status) => {
switch (status) {
case '审批中':
return <Tag color="processing">审批中</Tag>;
case '已通过':
return <Tag color="success">已通过</Tag>;
case '已拒绝':
return <Tag color="error">已拒绝</Tag>;
default:
return <Tag color="default">{status}</Tag>;
}
};
render() {
const { selectedOrgKeys, expandedKeys, approvalData } = this.state;
const { applicant, education, workExperience, approvalFlow } = approvalData;
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<Card className={styles.contentCard}>
<Row gutter={24}>
{/* 左侧组织架构树 - 保持不变 */}
<Col span={5}>
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TeamOutlined style={{ marginRight: 8, color: '#1890ff' }} />
<span>组织架构</span>
</div>
<Space>
<Button
type="text"
icon={<SyncOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeRefresh}
title="刷新"
/>
<Button
type="text"
icon={<ExpandOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeToggle}
title={expandedKeys.length > 0 ? "收缩全部" : "展开全部"}
/>
</Space>
</div>
}
className={styles.treeCard}
>
<div className={styles.treeContainer}>
<Tree
showIcon
selectedKeys={selectedOrgKeys}
expandedKeys={expandedKeys}
treeData={this.getTreeData()}
onSelect={this.onTreeSelect}
onExpand={this.onTreeExpand}
className={styles.orgTree}
/>
</div>
</Card>
</Col>
{/* 右侧入职审批详情 */}
<Col span={19}>
<Card className={styles.mainCard}>
{/* 头部标题和操作按钮 */}
<div className={styles.header}>
<Title level={3} style={{ margin: 0, color: '#333', fontSize: '18px' }}>
入职审批详情
</Title>
<Space>
<Button
type="primary"
icon={<CheckOutlined />}
onClick={this.handleApprove}
// className={styles.approveBtn}
>
通过
</Button>
<Button
danger
icon={<CloseOutlined />}
onClick={this.handleReject}
// className={styles.rejectBtn}
>
拒绝
</Button>
<Button
icon={<PrinterOutlined />}
onClick={this.handlePrint}
>
打印
</Button>
</Space>
</div>
{/* 滚动内容区 */}
<div className={styles.content}>
{/* 基本信息卡片 */}
<Card className={styles.infoCard} title="申请人信息">
<Row gutter={24}>
{/* 申请人照片 */}
<Col span={6}>
<div className={styles.photoContainer}>
<img
src={applicant.avatar}
alt="申请人照片"
className={styles.applicantPhoto}
/>
<Text className={styles.photoLabel}>申请人照片</Text>
</div>
</Col>
{/* 身份证照片 */}
<Col span={6}>
<div className={styles.photoContainer}>
<img
src={applicant.idCard}
alt="身份证照片"
className={styles.idCardPhoto}
/>
<Text className={styles.photoLabel}>身份证正反面</Text>
</div>
</Col>
{/* 基本信息 */}
<Col span={12}>
<div className={styles.basicInfo}>
<Row gutter={[16, 12]}>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>申请人姓名</Text>
<Text className={styles.infoValue}>{applicant.name}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>性别</Text>
<Text className={styles.infoValue}>{applicant.gender}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>年龄</Text>
<Text className={styles.infoValue}>{applicant.age}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>联系电话</Text>
<Text className={styles.infoValue}>{applicant.phone}</Text>
</div>
</Col>
<Col span={24}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>申请部门</Text>
<Text className={styles.infoValue}>{applicant.department}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>职位</Text>
<Text className={styles.infoValue}>{applicant.position}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>入职日期</Text>
<Text className={styles.infoValue}>{applicant.joinDate}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>薪资待遇</Text>
<Text className={styles.infoValue}>{applicant.salary}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>审批状态</Text>
{this.getStatusTag(applicant.status)}
</div>
</Col>
</Row>
</div>
</Col>
</Row>
</Card>
{/* 教育背景 */}
<Card className={styles.infoCard} title="教育背景">
<div className={styles.backgroundSection}>
{education.map((edu, index) => (
<Row key={index} gutter={16} className={styles.backgroundRow}>
<Col span={6}>
<Text strong>{edu.period}</Text>
</Col>
<Col span={6}>
<Text strong>{edu.school}</Text>
</Col>
<Col span={6}>
<Text>{edu.major}</Text>
</Col>
<Col span={6}>
<Text>{edu.degree}</Text>
</Col>
</Row>
))}
</div>
</Card>
{/* 工作经历 */}
<Card className={styles.infoCard} title="工作经历">
<div className={styles.backgroundSection}>
{workExperience.map((work, index) => (
<Row key={index} gutter={16} className={styles.backgroundRow}>
<Col span={8}>
<Text strong>{work.period}</Text>
</Col>
<Col span={8}>
<Text strong>{work.company}</Text>
</Col>
<Col span={8}>
<Text>{work.position}</Text>
</Col>
</Row>
))}
</div>
</Card>
{/* 审批流程 */}
<Card className={styles.infoCard} title="审批流程">
<Timeline className={styles.approvalTimeline}>
{approvalFlow.map((flow, index) => (
<Timeline.Item
key={index}
dot={this.getStatusIcon(flow.status)}
className={styles.timelineItem}
>
<div className={styles.timelineContent}>
<div className={styles.timelineHeader}>
<Text strong className={styles.stepTitle}>{flow.step}</Text>
<Text className={styles.stepTime}>{flow.time}</Text>
</div>
<Paragraph className={styles.stepComment}>
{flow.comment}
</Paragraph>
{flow.opinion && (
<div className={styles.stepOpinion}>
<Text>意见{flow.opinion}</Text>
</div>
)}
</div>
</Timeline.Item>
))}
</Timeline>
</Card>
</div>
</Card>
</Col>
</Row>
</Card>
</div>
</div>
);
}
}
export default OnBoardingApproval;

@ -0,0 +1,375 @@
@import '~@/utils/utils.less';
// 使用与OnBoarding相同的基础样式
.container {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background-color: #f5f6fa;
}
.mainContent {
height: 100vh;
overflow: hidden;
.contentCard {
height: 100%;
border: none !important;
:global(.ant-card-body) {
padding: 12px 24px;
// height: calc(100vh - 24px);
// overflow: hidden;
}
}
}
// 组织架构树样式 - 与OnBoarding保持一致
.treeCard {
height: 600px;
border: 1px solid #e8e8e8;
border-radius: 8px;
.treeContainer {
height: 520px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
.orgTree {
.ant-tree-node-content-wrapper {
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px 8px;
&:hover {
background-color: #f0f9ff;
transform: translateX(2px);
}
&.ant-tree-node-selected {
background-color: #e6f7ff !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
}
}
}
.tree-node-title {
display: flex;
align-items: center;
width: 100%;
.node-title {
flex: 1;
font-size: 14px;
margin-left: 8px;
color: #333;
}
.node-count {
color: #999;
font-size: 12px;
margin-left: auto;
background: #f0f0f0;
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
}
// 主卡片样式
.mainCard {
height: calc(100vh - 60px);
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
.ant-card-body {
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
// 头部区域
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
height: 72px;
// background: #fafafa;
}
// 内容区域
.content {
flex: 1;
padding: 20px 24px 28px 24px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 关键允许flex子元素收缩
height: calc(100vh - 180px); // 减去头部高度和其他空间,强制设置高度
max-height: calc(100vh - 180px); // 明确最大高度,确保滚动条出现
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.28);
}
}
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.18) rgba(0, 0, 0, 0.02);
}
// 按钮样式
.approveBtn {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border: none;
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
&:hover {
background: linear-gradient(135deg, #73d13d 0%, #95de64 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
}
}
.rejectBtn {
box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.4);
}
}
// 信息卡片
.infoCard {
margin-bottom: 20px;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&:last-child {
margin-bottom: 0;
}
.ant-card-head {
border-bottom: 1px solid #f0f0f0;
.ant-card-head-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.ant-card-body {
padding: 20px;
}
}
// 照片容器
.photoContainer {
text-align: center;
.applicantPhoto,
.idCardPhoto {
width: 100%;
max-width: 200px;
height: auto;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 8px;
}
.photoLabel {
display: block;
font-size: 12px;
color: #666;
}
}
// 基本信息
.basicInfo {
.infoItem {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.infoLabel {
color: #666;
font-size: 14px;
}
.infoValue {
font-weight: 500;
color: #333;
font-size: 14px;
}
}
}
// 背景信息区域
.backgroundSection {
background: #fafafa;
border-radius: 8px;
padding: 16px;
.backgroundRow {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
}
// 审批流程时间线
.approvalTimeline {
.timelineItem {
padding-bottom: 24px;
.timelineContent {
background: #fafafa;
border-radius: 8px;
padding: 16px;
margin-left: 16px;
border: 1px solid #f0f0f0;
.timelineHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.stepTitle {
font-size: 16px;
color: #333;
}
.stepTime {
font-size: 12px;
color: #999;
}
}
.stepComment {
margin: 0;
color: #666;
font-size: 14px;
}
.stepOpinion {
margin-top: 8px;
padding: 8px 12px;
background: #e6f7ff;
border-radius: 4px;
border-left: 3px solid #1890ff;
span {
color: #1890ff;
font-size: 13px;
}
}
}
}
}
// 状态图标
.completedIcon {
color: #52c41a;
font-size: 16px;
}
.processingIcon {
color: #faad14;
font-size: 16px;
}
.waitingIcon {
color: #d9d9d9;
font-size: 16px;
}
// 响应式设计
@media (max-width: 1200px) {
.treeCard {
height: 300px;
}
.mainCard {
height: auto;
max-height: 80vh;
min-height: 500px;
.content {
max-height: 65vh;
&::-webkit-scrollbar {
width: 4px;
}
}
}
.photoContainer {
margin-bottom: 16px;
}
}
// 页面特定样式
.container {
.ant-btn-primary {
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.ant-btn:not(.ant-btn-primary) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover,
&:focus {
border-color: #4c7bff;
color: #2d5cf6;
}
}
}

@ -0,0 +1,345 @@
import React, { PureComponent } from 'react';
import {
Card,
Form,
Input,
Button,
Upload,
message,
Avatar,
Row,
Col,
Space,
Typography
} from 'antd';
import {
CameraOutlined,
UserOutlined,
IdcardOutlined,
PlusOutlined,
CheckCircleOutlined
} from '@ant-design/icons';
import styles from './OnBoardingRegister.less';
const { TextArea } = Input;
const { Title, Text } = Typography;
class OnBoardingRegister extends PureComponent {
constructor(props) {
super(props);
this.state = {
faceVerified: false,
idCardFront: null,
idCardBack: null,
formData: {
department: '技术研发部',
position: '前端开发工程师',
joinDate: '2025-07-02',
name: '',
age: '',
phone: '',
idNumber: '',
remark: ''
}
};
}
// 刷脸认证
handleFaceVerification = () => {
// 模拟刷脸认证
setTimeout(() => {
this.setState({ faceVerified: true });
message.success('刷脸认证成功');
}, 2000);
message.loading('正在进行刷脸认证...', 2);
};
// 处理表单字段变化
handleFormChange = (field, value) => {
this.setState(prevState => ({
formData: {
...prevState.formData,
[field]: value
}
}));
};
// 处理身份证上传
handleIdCardUpload = (type, info) => {
if (info.file.status === 'uploading') {
return;
}
if (info.file.status === 'done') {
// 获取上传结果
const imageUrl = URL.createObjectURL(info.file.originFileObj);
this.setState({
[type]: imageUrl
});
message.success(`身份证${type === 'idCardFront' ? '正面' : '反面'}上传成功`);
}
};
// 自定义上传
customUpload = ({ file, onSuccess }) => {
setTimeout(() => {
onSuccess("ok");
}, 0);
};
// 提交表单
handleSubmit = () => {
const { formData, faceVerified, idCardFront, idCardBack } = this.state;
if (!faceVerified) {
message.error('请先完成刷脸认证');
return;
}
if (!formData.name) {
message.error('请输入姓名');
return;
}
if (!formData.age) {
message.error('请输入年龄');
return;
}
if (!formData.phone) {
message.error('请输入手机号');
return;
}
if (!formData.idNumber) {
message.error('请输入身份证号');
return;
}
if (!idCardFront || !idCardBack) {
message.error('请上传身份证正反面照片');
return;
}
console.log('提交入职申请数据:', {
...formData,
faceVerified,
idCardFront,
idCardBack
});
message.success('入职申请提交成功,请等待审核');
};
render() {
const { faceVerified, idCardFront, idCardBack, formData } = this.state;
return (
<div className={styles.container}>
<div className={styles.content}>
<Title level={2} className={styles.title}>
人员入职登记
</Title>
<Card className={styles.formCard}>
{/* 刷脸认证区域 */}
<div className={styles.faceSection}>
<div className={styles.faceContainer}>
{faceVerified ? (
<Avatar size={120} icon={<UserOutlined />} className={styles.faceAvatar} />
) : (
<div className={styles.faceUpload}>
<CameraOutlined className={styles.cameraIcon} />
</div>
)}
{faceVerified && (
<CheckCircleOutlined className={styles.verifiedIcon} />
)}
</div>
<Button
type="primary"
onClick={this.handleFaceVerification}
disabled={faceVerified}
className={styles.faceButton}
>
{faceVerified ? '刷脸认证成功' : '点击刷脸认证'}
</Button>
</div>
{/* 表单区域 */}
<Form layout="vertical" className={styles.form}>
{/* 入职信息 */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label="入职部门">
<Input
value={formData.department}
disabled
className={styles.readOnlyInput}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="岗位">
<Input
value={formData.position}
disabled
className={styles.readOnlyInput}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="入职时间">
<Input
value={formData.joinDate}
disabled
className={styles.readOnlyInput}
/>
</Form.Item>
</Col>
</Row>
{/* 个人信息 */}
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="姓名"
required
className={styles.requiredField}
>
<Input
value={formData.name}
onChange={(e) => this.handleFormChange('name', e.target.value)}
placeholder="请输入真实姓名"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="年龄"
required
className={styles.requiredField}
>
<Input
type="number"
value={formData.age}
onChange={(e) => this.handleFormChange('age', e.target.value)}
placeholder="请输入年龄"
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="手机号"
required
className={styles.requiredField}
>
<Input
value={formData.phone}
onChange={(e) => this.handleFormChange('phone', e.target.value)}
placeholder="请输入手机号码"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="身份证号"
required
className={styles.requiredField}
>
<Input
value={formData.idNumber}
onChange={(e) => this.handleFormChange('idNumber', e.target.value)}
placeholder="请输入身份证号码"
/>
</Form.Item>
</Col>
</Row>
{/* 身份证上传 */}
<Form.Item
label="身份证正反面"
required
className={styles.requiredField}
>
<Row gutter={16}>
<Col span={12}>
<Upload
name="idCardFront"
listType="picture-card"
className={styles.idCardUpload}
showUploadList={false}
customRequest={this.customUpload}
onChange={(info) => this.handleIdCardUpload('idCardFront', info)}
>
{idCardFront ? (
<img src={idCardFront} alt="身份证正面" className={styles.idCardImage} />
) : (
<div className={styles.uploadContent}>
<IdcardOutlined className={styles.uploadIcon} />
<div className={styles.uploadText}>身份证正面</div>
<Button size="small" className={styles.uploadButton}>
上传照片
</Button>
</div>
)}
</Upload>
</Col>
<Col span={12}>
<Upload
name="idCardBack"
listType="picture-card"
className={styles.idCardUpload}
showUploadList={false}
customRequest={this.customUpload}
onChange={(info) => this.handleIdCardUpload('idCardBack', info)}
>
{idCardBack ? (
<img src={idCardBack} alt="身份证反面" className={styles.idCardImage} />
) : (
<div className={styles.uploadContent}>
<IdcardOutlined className={styles.uploadIcon} />
<div className={styles.uploadText}>身份证反面</div>
<Button size="small" className={styles.uploadButton}>
上传照片
</Button>
</div>
)}
</Upload>
</Col>
</Row>
</Form.Item>
{/* 备注 */}
<Form.Item label="备注">
<TextArea
value={formData.remark}
onChange={(e) => this.handleFormChange('remark', e.target.value)}
rows={3}
placeholder="可填写特殊情况说明"
/>
</Form.Item>
{/* 提交按钮 */}
<Form.Item className={styles.submitSection}>
<Button
type="primary"
size="large"
block
onClick={this.handleSubmit}
className={styles.submitButton}
>
提交入职申请
</Button>
</Form.Item>
</Form>
</Card>
</div>
</div>
);
}
}
export default OnBoardingRegister;

@ -0,0 +1,411 @@
@import '~@/utils/utils.less';
.container {
min-height: 90vh;
height: 90vh; // 固定高度
background-color: #ffffff;
padding: 24px;
overflow: hidden; // 容器不滚动
}
.content {
max-width: 960px;
margin: 0 auto;
height: 100%; // 占满容器高度
overflow-y: auto; // 添加垂直滚动
overflow-x: hidden;
padding-right: 8px; // 为滚动条留出空间
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.28);
}
&:active {
background: rgba(0, 0, 0, 0.35);
}
}
/* Firefox滚动条 */
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.18) rgba(0, 0, 0, 0.02);
}
.title {
text-align: center;
margin-bottom: 32px !important;
color: #333;
font-weight: 600;
flex-shrink: 0; // 标题不收缩
}
.formCard {
background: white;
border-radius: 12px;
// 增强阴影效果,添加多层阴影
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.06),
0 4px 16px rgba(0, 0, 0, 0.08),
0 8px 32px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.06); // 添加边框增强立体感
margin-bottom: 24px; // 底部留出空间
transition: all 0.3s ease; // 添加过渡效果
// 悬停时增强阴影
// &:hover {
// box-shadow:
// 0 4px 12px rgba(0, 0, 0, 0.08),
// 0 8px 24px rgba(0, 0, 0, 0.12),
// 0 16px 48px rgba(0, 0, 0, 0.06);
// transform: translateY(-2px); // 轻微上浮效果
// }
.ant-card-body {
padding: 40px;
}
}
/* 刷脸认证区域 */
.faceSection {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
}
.faceContainer {
position: relative;
margin-bottom: 16px;
}
.faceUpload {
width: 120px;
height: 120px;
border: 2px dashed #d9d9d9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
transition: all 0.3s ease;
// 为刷脸上传区域添加轻微阴影
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
&:hover {
border-color: #2d5cf6;
background-color: #f0f9ff;
box-shadow: 0 4px 8px rgba(45, 92, 246, 0.1); // 悬停时显示主题色阴影
}
}
.cameraIcon {
font-size: 36px;
color: #bfbfbf;
}
.faceAvatar {
background-color: #2d5cf6 !important;
border: 3px solid #52c41a;
// 为头像添加阴影
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.2);
}
.verifiedIcon {
position: absolute;
bottom: 8px;
right: 8px;
font-size: 24px;
color: #52c41a;
background: white;
border-radius: 50%;
padding: 2px;
// 为认证图标添加阴影
box-shadow: 0 2px 6px rgba(82, 196, 26, 0.3);
}
.faceButton {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
border-radius: 6px;
font-weight: 500;
height: 36px;
padding: 0 20px;
// 增强按钮阴影
box-shadow: 0 3px 8px rgba(45, 92, 246, 0.3);
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(45, 92, 246, 0.4);
}
&:disabled {
background: #52c41a;
transform: none;
box-shadow: 0 2px 6px rgba(82, 196, 26, 0.2);
cursor: not-allowed;
}
}
/* 表单样式 */
.form {
.ant-form-item-label > label {
font-weight: 500;
color: #333;
}
}
.readOnlyInput {
background-color: #f5f5f5 !important;
color: #666 !important;
cursor: not-allowed;
}
.requiredField {
.ant-form-item-label > label::after {
content: "*";
color: #ff4d4f;
margin-left: 4px;
}
}
/* 身份证上传 */
.idCardUpload {
.ant-upload {
width: 100% !important;
height: 120px !important;
border: 2px dashed #d9d9d9;
border-radius: 8px;
background-color: #fafafa;
transition: all 0.3s ease;
// 为上传区域添加阴影
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
&:hover {
border-color: #2d5cf6;
background-color: #f0f9ff;
box-shadow: 0 4px 8px rgba(45, 92, 246, 0.1);
}
}
.ant-upload-select {
width: 100% !important;
height: 100% !important;
}
}
.uploadContent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 16px;
}
.uploadIcon {
font-size: 32px;
color: #bfbfbf;
margin-bottom: 8px;
}
.uploadText {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.uploadButton {
background-color: #f5f6fa;
border: 1px solid #d9d9d9;
color: #666;
border-radius: 4px;
font-size: 12px;
// 为小按钮添加轻微阴影
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
&:hover {
border-color: #2d5cf6;
color: #2d5cf6;
box-shadow: 0 2px 6px rgba(45, 92, 246, 0.15);
}
}
.idCardImage {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
// 为上传的图片添加阴影
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 提交区域 */
.submitSection {
margin-top: 32px;
margin-bottom: 0 !important;
}
.submitButton {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
height: 48px;
// 增强提交按钮阴影
box-shadow:
0 4px 12px rgba(45, 92, 246, 0.3),
0 2px 6px rgba(45, 92, 246, 0.2);
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-2px);
box-shadow:
0 8px 24px rgba(45, 92, 246, 0.4),
0 4px 12px rgba(45, 92, 246, 0.3);
}
&:active {
transform: translateY(0);
box-shadow:
0 2px 8px rgba(45, 92, 246, 0.3),
0 1px 4px rgba(45, 92, 246, 0.2);
}
}
/* 输入框样式 */
.container {
.ant-input,
.ant-input-number,
.ant-select-selector {
border-radius: 6px;
border: 1px solid #d9d9d9;
transition: all 0.3s ease;
// 为输入框添加轻微阴影
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
&:hover {
border-color: #4c7bff;
box-shadow: 0 2px 6px rgba(76, 123, 255, 0.1);
}
&:focus,
&.ant-input-focused,
&.ant-select-focused .ant-select-selector {
border-color: #2d5cf6;
box-shadow:
0 0 0 2px rgba(45, 92, 246, 0.2),
0 2px 8px rgba(45, 92, 246, 0.15);
}
}
.ant-input::placeholder {
color: #bfbfbf;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 16px;
height: 90vh; // 保持固定高度
}
.content {
/* 移动端使用更细的滚动条 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.12);
border-radius: 2px;
&:hover {
background: rgba(0, 0, 0, 0.20);
}
}
}
.formCard {
// 移动端减少阴影强度
box-shadow:
0 1px 4px rgba(0, 0, 0, 0.06),
0 2px 8px rgba(0, 0, 0, 0.08);
&:hover {
transform: none; // 移动端取消悬停上浮效果
box-shadow:
0 1px 4px rgba(0, 0, 0, 0.06),
0 2px 8px rgba(0, 0, 0, 0.08);
}
.ant-card-body {
padding: 24px 20px;
}
}
.faceUpload {
width: 100px;
height: 100px;
}
.cameraIcon {
font-size: 28px;
}
.uploadContent {
padding: 12px;
}
.uploadIcon {
font-size: 24px;
}
.idCardUpload .ant-upload {
height: 100px !important;
}
}
@media (max-width: 576px) {
.container {
padding: 12px; // 进一步减少内边距
}
.content {
padding-right: 4px; // 滚动条空间更小
}
.title {
font-size: 20px !important;
margin-bottom: 24px !important; // 减少标题下边距
}
.formCard {
.ant-card-body {
padding: 20px 16px;
}
}
.submitButton {
font-size: 14px;
height: 44px;
}
}

@ -0,0 +1,540 @@
import React, { PureComponent } from 'react';
import {
Card,
Tree,
Input,
Button,
DatePicker,
Select,
Row,
Col,
Space,
Radio,
Checkbox,
InputNumber,
Avatar,
message,
Timeline,
Tag,
Typography,
Divider
} from 'antd';
import {
SaveOutlined,
CloseOutlined,
PlusCircleOutlined,
DeleteOutlined,
ExpandOutlined,
UserOutlined,
TeamOutlined,
ApartmentOutlined,
SyncOutlined,
DollarOutlined,
SettingOutlined,
CalendarOutlined,
UsergroupAddOutlined,
UserSwitchOutlined,
StopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ContactsOutlined,
PrinterOutlined,
CheckOutlined,
ClockCircleOutlined,
LoadingOutlined,
EyeOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import styles from './Resignation.less';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text, Paragraph } = Typography;
class Resignation extends PureComponent {
constructor(props) {
super(props);
this.state = {
selectedOrgKeys: ['0-0-4'], // 默认选中人事部
expandedKeys: ['0-0', '0-0-0', '0-0-1', '0-0-2'],
// 员工异动审批数据
transferData: {
employee: {
name: '林志强',
employeeId: 'EMP20230085',
avatar: '/img/avatar1.jpg',
idCardFront: '/img/card.jpg',
idCardBack: '/img/card.jpg',
originalDepartment: '前端开发部',
newDepartment: '产品设计部',
originalPosition: '高级前端工程师',
newPosition: '产品设计师',
transferType: '部门调动',
transferDate: '2023-06-15',
reason: '根据公司人才发展规划,结合员工个人职业发展意愿,经双方协商一致,同意调动。',
status: '审批中'
},
approvalFlow: [
{
step: '提交申请',
status: 'completed',
operator: '林志强',
time: '2023-06-05 09:30',
comment: '申请人:林志强',
opinion: ''
},
{
step: '部门经理审批',
status: 'completed',
operator: '王建国(前端开发部经理)',
time: '2023-06-05 14:15',
comment: '审批人:王建国(前端开发部经理)',
opinion: '同意调动,该员工在产品设计方面有潜力。'
},
{
step: '技术总监审批',
status: 'completed',
operator: '张伟(技术中心总监)',
time: '2023-06-06 10:45',
comment: '审批人:张伟(技术中心总监)',
opinion: '同意,符合技术人才发展规划。'
},
{
step: 'HR审批',
status: 'completed',
operator: '李芳(人力资源部经理)',
time: '2023-06-07 11:20',
comment: '审批人:李芳(人力资源部经理)',
opinion: '已核实相关情况,同意调动。'
},
{
step: '总经理审批',
status: 'completed',
operator: '陈明(总经理)',
time: '2023-06-08 16:30',
comment: '审批人:陈明(总经理)',
opinion: '同意执行。'
},
{
step: '入职生效',
status: 'processing',
operator: '',
time: '待处理',
comment: '计划生效日期2023-06-15',
opinion: ''
},
{
step: '审批完结',
status: 'waiting',
operator: '',
time: '未开始',
comment: '',
opinion: ''
}
]
},
organizationData: [
{
title: '飞利信科技有限公司',
key: '0-0',
count: 356,
children: [
{
title: '技术部',
key: '0-0-0',
count: 120,
children: [
{ title: '前端组', key: '0-0-0-0', count: 45 },
{ title: '后端组', key: '0-0-0-1', count: 52 },
{ title: '测试组', key: '0-0-0-2', count: 23 }
],
},
{
title: '产品部',
key: '0-0-1',
count: 68,
children: [
{ title: '产品设计组', key: '0-0-1-0', count: 28 },
{ title: '用户体验组', key: '0-0-1-1', count: 25 },
{ title: '产品运营组', key: '0-0-1-2', count: 15 }
],
},
{
title: '运营部',
key: '0-0-2',
count: 52,
children: [
{ title: '市场营销组', key: '0-0-2-0', count: 22 },
{ title: '客户服务组', key: '0-0-2-1', count: 18 },
{ title: '商务合作组', key: '0-0-2-2', count: 12 }
],
},
{
title: '财务部',
key: '0-0-3',
count: 32,
children: [
{ title: '会计组', key: '0-0-3-0', count: 18 },
{ title: '审计组', key: '0-0-3-1', count: 14 }
],
},
{
title: '人事部',
key: '0-0-4',
count: 84,
children: [
{ title: 'HR专员组', key: '0-0-4-0', count: 25 },
{ title: '招聘组', key: '0-0-4-1', count: 22 },
{ title: '培训组', key: '0-0-4-2', count: 18 },
{ title: '薪酬组', key: '0-0-4-3', count: 19 }
],
},
],
},
]
};
}
// 获取处理后的树形数据
getTreeData = () => {
const { organizationData } = this.state;
const processNode = (node) => ({
key: node.key,
title: (
<span className={styles['tree-node-title']}>
{this.getNodeIcon(node.title)}
<span className={styles['node-title']}>{node.title}</span>
<span className={styles['node-count']}>({node.count})</span>
</span>
),
children: node.children ? node.children.map(processNode) : undefined
});
return organizationData.map(processNode);
};
// 获取节点图标
getNodeIcon = (title) => {
if (title.includes('公司') || title.includes('集团')) return <ApartmentOutlined style={{ color: '#1890ff', fontSize: '14px' }} />;
if (title.includes('技术') || title.includes('开发') || title.includes('测试')) return <SettingOutlined style={{ color: '#52c41a', fontSize: '14px' }} />;
if (title.includes('产品') || title.includes('设计') || title.includes('体验')) return <TeamOutlined style={{ color: '#fa8c16', fontSize: '14px' }} />;
if (title.includes('运营') || title.includes('市场') || title.includes('客户') || title.includes('商务')) return <TeamOutlined style={{ color: '#eb2f96', fontSize: '14px' }} />;
if (title.includes('财务') || title.includes('会计') || title.includes('审计')) return <DollarOutlined style={{ color: '#722ed1', fontSize: '14px' }} />;
if (title.includes('人事') || title.includes('HR') || title.includes('招聘') || title.includes('培训') || title.includes('薪酬')) return <UserOutlined style={{ color: '#13c2c2', fontSize: '14px' }} />;
return <TeamOutlined style={{ color: '#666', fontSize: '14px' }} />;
};
// 刷新树数据
handleTreeRefresh = () => {
message.info('刷新组织架构数据');
};
// 展开/收缩所有节点
handleTreeToggle = () => {
const { expandedKeys, organizationData } = this.state;
if (expandedKeys.length > 0) {
this.setState({ expandedKeys: [] });
} else {
const getAllKeys = (nodes) => {
let keys = [];
nodes.forEach(node => {
keys.push(node.key);
if (node.children) {
keys = keys.concat(getAllKeys(node.children));
}
});
return keys;
};
this.setState({ expandedKeys: getAllKeys(organizationData) });
}
};
// 组织树选择
onTreeSelect = (selectedKeys, info) => {
console.log('选中节点:', selectedKeys, info);
this.setState({ selectedOrgKeys: selectedKeys });
};
// 树展开/收缩
onTreeExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
// 同意审批
handleApprove = () => {
message.success('同意审批,已提交到下一环节');
};
// 拒绝审批
handleReject = () => {
message.error('拒绝审批');
};
// 打印
handlePrint = () => {
window.print();
};
// 获取审批状态图标
getStatusIcon = (status) => {
switch (status) {
case 'completed':
return <CheckCircleOutlined className={styles.completedIcon} />;
case 'processing':
return <LoadingOutlined className={styles.processingIcon} />;
case 'waiting':
return <ClockCircleOutlined className={styles.waitingIcon} />;
default:
return <ClockCircleOutlined className={styles.waitingIcon} />;
}
};
// 获取状态标签
getStatusTag = (status) => {
switch (status) {
case '审批中':
return <Tag color="processing">审批中</Tag>;
case '已通过':
return <Tag color="success">已通过</Tag>;
case '已拒绝':
return <Tag color="error">已拒绝</Tag>;
default:
return <Tag color="default">{status}</Tag>;
}
};
render() {
const { selectedOrgKeys, expandedKeys, transferData } = this.state;
const { employee, approvalFlow } = transferData;
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<Card className={styles.contentCard}>
<Row gutter={24}>
{/* 左侧组织架构树 - 保持不变 */}
<Col span={5}>
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TeamOutlined style={{ marginRight: 8, color: '#1890ff' }} />
<span>组织架构</span>
</div>
<Space>
<Button
type="text"
icon={<SyncOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeRefresh}
title="刷新"
/>
<Button
type="text"
icon={<ExpandOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeToggle}
title={expandedKeys.length > 0 ? "收缩全部" : "展开全部"}
/>
</Space>
</div>
}
className={styles.treeCard}
>
<div className={styles.treeContainer}>
<Tree
showIcon
selectedKeys={selectedOrgKeys}
expandedKeys={expandedKeys}
treeData={this.getTreeData()}
onSelect={this.onTreeSelect}
onExpand={this.onTreeExpand}
className={styles.orgTree}
/>
</div>
</Card>
</Col>
{/* 右侧员工异动审批详情 */}
<Col span={19}>
<Card className={styles.mainCard}>
{/* 头部标题和操作按钮 */}
<div className={styles.header}>
<Title level={3} style={{ margin: 0, color: '#333', fontSize: '20px' }}>
员工异动审批详情
</Title>
<Space>
<Button
type="primary"
icon={<CheckOutlined />}
onClick={this.handleApprove}
// className={styles.approveBtn}
>
同意
</Button>
<Button
danger
icon={<CloseOutlined />}
onClick={this.handleReject}
// className={styles.rejectBtn}
>
拒绝
</Button>
<Button
icon={<PrinterOutlined />}
onClick={this.handlePrint}
>
打印
</Button>
</Space>
</div>
{/* 滚动内容区 */}
<div className={styles.content}>
{/* 基本信息卡片 */}
<Card className={styles.infoCard} title="基本信息">
<Row gutter={24}>
{/* 员工照片 */}
<Col span={6}>
<div className={styles.photoContainer}>
<img
src={employee.avatar}
alt="员工照片"
className={styles.employeePhoto}
/>
<Text className={styles.photoLabel}>员工照片</Text>
</div>
</Col>
{/* 基本信息 */}
<Col span={18}>
<div className={styles.basicInfo}>
<Row gutter={[16, 12]}>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>员工姓名</Text>
<Text className={styles.infoValue}>{employee.name}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>员工编号</Text>
<Text className={styles.infoValue}>{employee.employeeId}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>原部门</Text>
<Text className={styles.infoValue}>{employee.originalDepartment}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>新部门</Text>
<Text className={styles.infoValue}>{employee.newDepartment}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>原岗位</Text>
<Text className={styles.infoValue}>{employee.originalPosition}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>新岗位</Text>
<Text className={styles.infoValue}>{employee.newPosition}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>异动类型</Text>
<Text className={styles.infoValue}>{employee.transferType}</Text>
</div>
</Col>
<Col span={12}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>异动时间</Text>
<Text className={styles.infoValue}>{employee.transferDate}</Text>
</div>
</Col>
<Col span={24}>
<div className={styles.infoItem}>
<Text className={styles.infoLabel}>异动原因</Text>
<Text className={styles.infoValue}>{employee.reason}</Text>
</div>
</Col>
</Row>
</div>
</Col>
</Row>
</Card>
{/* 证件信息 */}
<Card className={styles.infoCard} title="证件信息">
<Row gutter={24}>
<Col span={12}>
<div className={styles.idCardContainer}>
<Text className={styles.photoLabel}>身份证正面</Text>
<img
src={employee.idCardFront}
alt="身份证正面"
className={styles.idCardPhoto}
/>
</div>
</Col>
<Col span={12}>
<div className={styles.idCardContainer}>
<Text className={styles.photoLabel}>身份证反面</Text>
<img
src={employee.idCardBack}
alt="身份证反面"
className={styles.idCardPhoto}
/>
</div>
</Col>
</Row>
</Card>
{/* 审批流程 */}
<Card className={styles.infoCard} title="审批流程">
<Timeline className={styles.approvalTimeline}>
{approvalFlow.map((flow, index) => (
<Timeline.Item
key={index}
dot={this.getStatusIcon(flow.status)}
className={styles.timelineItem}
>
<div className={styles.timelineContent}>
<div className={styles.timelineHeader}>
<Text strong className={styles.stepTitle}>{flow.step}</Text>
<Text className={styles.stepTime}>{flow.time}</Text>
</div>
<Paragraph className={styles.stepComment}>
{flow.comment}
</Paragraph>
{flow.opinion && (
<div className={styles.stepOpinion}>
<Text>审批意见{flow.opinion}</Text>
</div>
)}
</div>
</Timeline.Item>
))}
</Timeline>
</Card>
</div>
</Card>
</Col>
</Row>
</Card>
</div>
</div>
);
}
}
export default Resignation;

@ -0,0 +1,408 @@
@import '~@/utils/utils.less';
// 使用与OnBoarding相同的基础样式
.container {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background-color: #f5f6fa;
}
.mainContent {
height: 100vh;
overflow: hidden;
.contentCard {
height: 100%;
border: none !important;
:global(.ant-card-body) {
padding: 12px 24px;
// height: calc(100vh - 24px);
// overflow: hidden;
}
}
}
// 组织架构树样式 - 与OnBoarding保持一致
.treeCard {
height: 600px;
border: 1px solid #e8e8e8;
border-radius: 8px;
.treeContainer {
height: 520px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
.orgTree {
.ant-tree-node-content-wrapper {
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px 8px;
&:hover {
background-color: #f0f9ff;
transform: translateX(2px);
}
&.ant-tree-node-selected {
background-color: #e6f7ff !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
}
}
}
.tree-node-title {
display: flex;
align-items: center;
width: 100%;
.node-title {
flex: 1;
font-size: 14px;
margin-left: 8px;
color: #333;
}
.node-count {
color: #999;
font-size: 12px;
margin-left: auto;
background: #f0f0f0;
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
}
// 主卡片样式
.mainCard {
height: calc(100vh - 60px);
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
.ant-card-body {
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
// 头部区域
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
height: 72px;
// background: #fafafa;
}
// 内容区域
.content {
flex: 1;
padding: 20px 24px 28px 24px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 关键允许flex子元素收缩
height: calc(100vh - 180px); // 减去头部高度和其他空间,强制设置高度
max-height: calc(100vh - 180px); // 明确最大高度,确保滚动条出现
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.28);
}
}
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.18) rgba(0, 0, 0, 0.02);
}
// 按钮样式
.approveBtn {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border: none;
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
&:hover {
background: linear-gradient(135deg, #73d13d 0%, #95de64 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
}
}
.rejectBtn {
box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.4);
}
}
// 信息卡片
.infoCard {
margin-bottom: 20px;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&:last-child {
margin-bottom: 0;
}
.ant-card-head {
border-bottom: 1px solid #f0f0f0;
.ant-card-head-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.ant-card-body {
padding: 20px;
}
}
// 照片容器
.photoContainer {
text-align: center;
.employeePhoto {
width: 96px;
height: 96px;
border-radius: 50%;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 8px;
object-fit: cover;
}
.photoLabel {
display: block;
font-size: 12px;
color: #666;
}
}
// 身份证容器
.idCardContainer {
text-align: center;
.photoLabel {
display: block;
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
.idCardPhoto {
width: 100%;
max-width: 280px; // 限制最大宽度
height: 140px; // 从 192px 改为 140px
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
object-fit: cover;
}
}
// 基本信息
.basicInfo {
.infoItem {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
.infoLabel {
color: #666;
font-size: 14px;
flex-shrink: 0;
margin-right: 8px;
}
.infoValue {
font-weight: 500;
color: #333;
font-size: 14px;
text-align: right;
flex: 1;
word-break: break-all;
}
}
}
// 审批流程时间线
.approvalTimeline {
.timelineItem {
padding-bottom: 24px;
.timelineContent {
background: #fafafa;
border-radius: 8px;
padding: 16px;
margin-left: 16px;
border: 1px solid #f0f0f0;
.timelineHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.stepTitle {
font-size: 16px;
color: #333;
}
.stepTime {
font-size: 12px;
color: #999;
}
}
.stepComment {
margin: 0;
color: #666;
font-size: 14px;
}
.stepOpinion {
margin-top: 8px;
padding: 8px 12px;
background: #e6f7ff;
border-radius: 4px;
border-left: 3px solid #1890ff;
span {
color: #1890ff;
font-size: 13px;
}
}
}
}
}
// 状态图标
.completedIcon {
color: #52c41a;
font-size: 16px;
}
.processingIcon {
color: #faad14;
font-size: 16px;
}
.waitingIcon {
color: #d9d9d9;
font-size: 16px;
}
// 响应式设计
@media (max-width: 1200px) {
.treeCard {
height: 300px;
}
.mainCard {
height: auto;
max-height: 80vh;
min-height: 500px;
.content {
max-height: 65vh;
&::-webkit-scrollbar {
width: 4px;
}
}
}
.photoContainer {
margin-bottom: 16px;
.employeePhoto {
width: 80px;
height: 80px;
}
}
.idCardContainer {
margin-bottom: 16px;
.idCardPhoto {
max-width: 240px; // 移动端进一步限制宽度
height: 120px; // 移动端从 150px 改为 120px
}
}
}
@media (max-width: 768px) {
.idCardContainer {
.idCardPhoto {
max-width: 200px; // 小屏幕设备进一步缩小
height: 100px; // 小屏幕设备高度调整
}
}
}
// 页面特定样式
.container {
.ant-btn-primary {
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.ant-btn:not(.ant-btn-primary) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover,
&:focus {
border-color: #4c7bff;
color: #2d5cf6;
}
}
}

@ -25,7 +25,7 @@ import {
} from '@ant-design/icons';
import styles from './StaffInfo.less';
import StandardTable from '@/components/StandardTable';
import StaffInfoAdd from './form/StaffInfoadd';
import StaffInfoAdd from './form/StaffInfoAdd';
import StaffInfoRenderSimpleForm from "./form/StaffInfoRenderSimpleForm" //表单
const { Option } = Select;
@ -220,6 +220,7 @@ class StaffInfo extends PureComponent {
title: '操作',
key: 'action',
width: 80,
align: 'center',
render: (_, record) => (
<Space size="middle">
<Button
@ -493,16 +494,16 @@ class StaffInfo extends PureComponent {
<Space size="middle">
<Button
icon={<DownloadOutlined />}
size="large"
className={styles.exportButton}
size="middle"
// className={styles.exportButton}
>
导出
</Button>
<Button
type="primary"
icon={<UserAddOutlined />}
size="large"
className={styles.addButton}
size="middle"
// className={styles.addButton}
onClick={this.showAddModal}
>
新增
@ -516,7 +517,7 @@ class StaffInfo extends PureComponent {
columns={this.defaultColumns}
dataSource={tableData}
pagination={false}
scroll={{ x: 1000,y: 600 }}
// scroll={{ x: 1000,y: 600 }}
size="small"
selectedRows={[]}
/>

@ -2,7 +2,8 @@
.staffInfoContainer {
min-height: 100vh;
background-color: #f5f6fa;
height: 100vh;
// background-color: #f5f6fa;
.announcementBar {
background: #e6f7ff;
@ -48,7 +49,7 @@
}
.mainContent {
padding: 24px;
// padding: 12px;
.contentCard {
.ant-card-head {
@ -154,68 +155,77 @@
.searchCard {
margin-bottom: 16px;
background-color: #f9fbfc;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.ant-card-body {
padding: 12px;
background-color: #f9fbfc;
border-radius: 6px;
padding: 16px;
}
.ant-form-item-label {
font-weight: 500;
label {
color: #ecf0f1 !important;
}
}
/* 搜索表单容器样式 */
.searchFormContainer {
.searchForm {
.ant-form-item {
margin-bottom: 16px;
// 深色背景下的表单项样式
.ant-input,
.ant-select-selector {
background-color: #34495e !important;
border-color: #5a6c7d !important;
color: #ecf0f1 !important;
.ant-form-item-label {
font-weight: 500;
color: #333;
label {
color: #333 !important;
font-size: 14px;
}
}
&:hover {
border-color: #7fb3d3 !important;
}
.ant-input,
.ant-select-selector,
.ant-picker {
border-radius: 6px;
border: 1px solid #d9d9d9;
transition: all 0.3s ease;
&:focus {
border-color: #2d5cf6 !important;
box-shadow: 0 0 0 2px rgba(45, 92, 246, 0.2) !important;
}
}
&:hover {
border-color: #4c7bff;
}
.ant-select-selection-placeholder,
.ant-input::placeholder {
color: #95a5a6 !important;
}
&:focus,
&.ant-select-focused .ant-select-selector,
&.ant-picker-focused {
border-color: #2d5cf6;
box-shadow: 0 0 0 2px rgba(45, 92, 246, 0.2);
}
}
.anticon {
color: #95a5a6 !important;
.ant-select-selection-placeholder,
.ant-input::placeholder,
.ant-picker-input input::placeholder {
color: #bfbfbf;
}
}
}
}
/* 搜索按钮样式 */
.searchButton {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
border-radius: 8px;
font-weight: 600;
border-radius: 6px;
color: white;
font-weight: 500;
font-size: 14px;
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.3);
box-shadow: 0 2px 8px rgba(45, 92, 246, 0.3);
transition: all 0.3s ease;
margin-top: -8px;
height: 35px;
height: 32px;
padding: 0 16px;
&:hover,
&:focus {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(45, 92, 246, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.4);
color: white;
}
&:active {
@ -225,31 +235,39 @@
/* 重置按钮样式 */
.resetButton {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
background: #fff;
border: 1px solid #d9d9d9;
color: #666;
border-radius: 8px;
border-radius: 6px;
font-weight: 500;
font-size: 14px;
transition: all 0.3s ease;
margin-top: -8px;
height: 35px;
height: 32px;
padding: 0 16px;
&:hover,
&:focus {
background: linear-gradient(135deg, #e8e8e8 0%, #dcdcdc 100%);
border-color: #bfbfbf;
color: #333;
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
border-color: #4c7bff;
color: #2d5cf6;
}
.anticon {
color: #ff7875;
}
}
/* 展开按钮样式 */
.expandButton {
color: #2d5cf6;
font-size: 14px;
padding: 0 8px;
height: 32px;
&:hover,
&:focus {
color: #4c7bff;
}
}
}
.actionBar {
@ -325,81 +343,6 @@
}
}
.tableCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.ant-table-wrapper {
max-height: 600px;
overflow-y: auto;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 4px;
&:hover {
background: #bfbfbf;
}
}
.ant-table-thead>tr>th {
background-color: #fafafa;
font-weight: 600;
color: #333;
border-bottom: 1px solid #e8e8e8;
position: sticky;
top: 0;
z-index: 1;
}
.ant-table-tbody>tr {
&:hover>td {
background-color: #f5f5f5 !important;
}
>td {
border-bottom: 1px solid #f0f0f0;
}
}
}
.paginationWrapper {
margin-top: 20px;
text-align: right;
.ant-pagination {
.ant-pagination-total-text {
color: #666;
}
.ant-pagination-item {
border-radius: 4px;
&.ant-pagination-item-active {
background-color: #2d5cf6;
border-color: #2d5cf6;
}
}
.ant-pagination-prev,
.ant-pagination-next {
border-radius: 4px;
}
}
}
}
.tableCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
@ -462,7 +405,7 @@
border-radius: 4px;
&.ant-pagination-item-active {
background-color: #fff;
background-color: #2d5cf6;
border-color: #2d5cf6;
}
}
@ -477,14 +420,11 @@
}
}
.mainContent {
padding: 12px;
// padding: 12px;
.contentCard {
border: none !important;
.ant-card-head {
.ant-card-head-title {
font-size: 18px;
@ -494,144 +434,6 @@
}
}
.treeCard {
height: 600px;
border: 1px solid #e8e8e8;
border-radius: 8px;
.treeHeader {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: 500;
}
.treeContainer {
height: 520px;
overflow-y: auto;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
&:hover {
background: #bfbfbf;
}
}
.orgTree {
.ant-tree-treenode {
padding: 2px 0;
.ant-tree-node-content-wrapper {
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px 8px;
&:hover {
background-color: #f0f9ff;
transform: translateX(2px);
}
&.ant-tree-node-selected {
background-color: #e6f7ff !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
}
.ant-tree-iconEle {
margin-right: 8px;
}
.ant-tree-title {
font-size: 14px;
}
}
}
}
}
/* Tree节点标题样式 */
.tree-node-title {
display: flex;
align-items: center;
width: 100%;
.node-title {
flex: 1;
font-size: 14px;
margin-left: 8px;
color: #333;
}
.node-count {
color: #999;
font-size: 12px;
margin-left: auto;
background: #f0f0f0;
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
}
.searchCard {
margin-bottom: 16px;
background-color: #f9fbfc;
border-radius: 8px;
.ant-card-body {
padding: 10px;
background-color: #f9fbfc;
border-radius: 6px;
}
.ant-form-item-label {
font-weight: 500;
label {
color: #ecf0f1 !important;
}
}
// 深色背景下的表单项样式
.ant-input,
.ant-select-selector {
background-color: #34495e !important;
border-color: #5a6c7d !important;
color: #ecf0f1 !important;
&:hover {
border-color: #7fb3d3 !important;
}
&:focus {
border-color: #2d5cf6 !important;
box-shadow: 0 0 0 2px rgba(45, 92, 246, 0.2) !important;
}
}
.ant-select-selection-placeholder,
.ant-input::placeholder {
color: #95a5a6 !important;
}
.anticon {
color: #95a5a6 !important;
}
}
.actionBar {
display: flex;
justify-content: space-between;
@ -644,14 +446,11 @@
}
}
:global {
.ant-card-body {
padding: 12px 24px 0px 24px;
}
}
}

@ -1,91 +1,89 @@
import React, { useState } from 'react';
import { Button, Col, Form, Input, Row, message,Select, Space } from 'antd';
import { ClearOutlined,SearchOutlined } from '@ant-design/icons';
import { Button, Col, Form, Input, Row, message, Select, Space, DatePicker } from 'antd';
import { ClearOutlined, SearchOutlined, ExpandOutlined, CompressOutlined } from '@ant-design/icons';
import styles from "../StaffInfo.less";
const FormItem = Form.Item;
const FormItem = Form.Item;
const { Option } = Select;
const StaffInfoRenderSimpleForm = (props) => {
const [form] = Form.useForm();
const [startDateOpen, setStartDateOpen] = useState(false);
const [endDateOpen, setEndDateOpen] = useState(false);
const [expandForm, setExpandForm] = useState(false);
const { submitButtons, handleSearch, handleFormReset, toggleForm, params } = props;
React.useEffect(() => {
form.setFieldsValue({
opername: params?.opername,
opertype: params?.opertype,
name: params?.name,
phone: params?.phone,
idCard: params?.idCard,
department: params?.department,
position: params?.position,
status: params?.status,
});
}, [params]);
const onFinish = values => {
const params = {
const searchParams = {
...values,
startDate: values.startDate ? values.startDate.format('YYYY-MM-DD') : undefined,
endDate: values.endDate ? values.endDate.format('YYYY-MM-DD') : undefined,
entryDateStart: values.entryDateStart ? values.entryDateStart.format('YYYY-MM-DD') : undefined,
entryDateEnd: values.entryDateEnd ? values.entryDateEnd.format('YYYY-MM-DD') : undefined,
};
handleSearch(params);
handleSearch && handleSearch(searchParams);
};
const myhandleFormReset = () => {
form.resetFields();
handleFormReset();
handleFormReset && handleFormReset();
};
const toggleExpandForm = () => {
setExpandForm(!expandForm);
};
const onEndDateChange = (value) => {
const startDate = form.getFieldValue("startDate");
const startDate = form.getFieldValue("entryDateStart");
if (startDate && value && value.isBefore(startDate, "day")) {
message.error("结束日期不能小于开始日期!");
// 清空
form.setFieldsValue({ endDate: undefined });
form.setFieldsValue({ entryDateEnd: undefined });
}
}
};
return (
<div >
<div className={styles.searchFormContainer}>
<Form
// ref={(ref) => this.formRef = ref}
// onFinish={this.handleSearch}
form={form}
onFinish={onFinish}
layout="vertical"
className={styles.searchForm}
>
<Row gutter={12}>
<Col span={8}>
<Row gutter={16}>
<Col span={6}>
<Form.Item
name="name"
label="姓名"
>
<Input
placeholder="请输入姓名"
allowClear
/>
</Form.Item>
</Col>
<Col span={8}>
<Col span={6}>
<Form.Item
name="phone"
label="电话"
>
<Input
placeholder="请输入电话"
// prefix={<PhoneOutlined />}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="idCard"
label="身份证号"
>
<Input
placeholder="请输入身份证号"
// prefix={<IdcardOutlined />}
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Col span={6}>
<Form.Item
name="department"
label="部门"
>
<Select placeholder="请选择部门">
<Select placeholder="请选择部门" allowClear>
<Option value="技术部">技术部</Option>
<Option value="产品部">产品部</Option>
<Option value="运营部">运营部</Option>
@ -94,44 +92,90 @@ const StaffInfoRenderSimpleForm = (props) => {
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="position"
label="岗位"
>
<Select placeholder="请选择岗位">
<Option value="高级工程师">高级工程师</Option>
<Option value="产品经理">产品经理</Option>
<Option value="运营专员">运营专员</Option>
<Option value="会计">会计</Option>
<Option value="HR专员">HR专员</Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Col span={6}>
<Form.Item label=" " colon={false}>
<Space size="middle">
<Button
type="primary"
htmlType="submit"
icon={<SearchOutlined />}
size="large"
className={styles.searchButton}
>
查询
</Button>
<Button
// onClick={this.handleReset}
onClick={myhandleFormReset}
icon={<ClearOutlined />}
size="large"
className={styles.resetButton}
>
重置
</Button>
<Button
type="link"
onClick={toggleExpandForm}
icon={expandForm ? <CompressOutlined /> : <ExpandOutlined />}
className={styles.expandButton}
>
{expandForm ? '收起' : '展开'}
</Button>
</Space>
</Form.Item>
</Col>
</Row>
{/* 展开的表单项 */}
{expandForm && (
<Row gutter={16}>
<Col span={6}>
<Form.Item
name="idCard"
label="身份证号"
>
<Input
placeholder="请输入身份证号"
allowClear
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="position"
label="岗位"
>
<Select placeholder="请选择岗位" allowClear>
<Option value="高级工程师">高级工程师</Option>
<Option value="产品经理">产品经理</Option>
<Option value="运营专员">运营专员</Option>
<Option value="会计">会计</Option>
<Option value="HR专员">HR专员</Option>
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="status"
label="状态"
>
<Select placeholder="请选择状态" allowClear>
<Option value="1">在职</Option>
<Option value="0">离职</Option>
<Option value="2">试用期</Option>
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="entryDateStart"
label="入职开始日期"
>
<DatePicker
placeholder="请选择开始日期"
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
</Row>
)}
</Form>
</div>
);

@ -0,0 +1,631 @@
import React, { PureComponent } from 'react';
import {
Card,
Tree,
Input,
Button,
DatePicker,
Select,
Row,
Col,
Space,
Radio,
Checkbox,
InputNumber,
Avatar,
message,
Timeline,
Tag,
Typography,
Divider,
Upload,
Form
} from 'antd';
import {
SaveOutlined,
CloseOutlined,
PlusCircleOutlined,
DeleteOutlined,
ExpandOutlined,
UserOutlined,
TeamOutlined,
ApartmentOutlined,
SyncOutlined,
DollarOutlined,
SettingOutlined,
CalendarOutlined,
UsergroupAddOutlined,
UserSwitchOutlined,
StopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ContactsOutlined,
PrinterOutlined,
CheckOutlined,
ClockCircleOutlined,
LoadingOutlined,
EyeOutlined,
SearchOutlined,
PictureOutlined,
UploadOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import styles from './Training.less';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
class Training extends PureComponent {
constructor(props) {
super(props);
this.state = {
selectedOrgKeys: ['0-0-4'], // 默认选中人事部
expandedKeys: ['0-0', '0-0-0', '0-0-1', '0-0-2'],
searchKeyword: '',
// 培训数据
trainingData: {
basicInfo: {
name: '',
cover: null,
description: ''
},
details: {
startDate: null,
endDate: null,
cost: ''
}
},
// 可选课程
availableCourses: [
{
id: 1,
name: '高效沟通技巧',
instructor: '张明远',
duration: '2小时',
selected: false
},
{
id: 2,
name: '团队协作与领导力',
instructor: '李思雨',
duration: '3小时',
selected: false
},
{
id: 3,
name: '项目管理实战',
instructor: '王建国',
duration: '4小时',
selected: false
},
{
id: 4,
name: 'Excel高级应用',
instructor: '陈晓芳',
duration: '2.5小时',
selected: false
},
{
id: 5,
name: '商务礼仪与职业形象',
instructor: '赵雅芝',
duration: '2小时',
selected: false
}
],
// 已选课程
selectedCourses: [
{
id: 1,
name: '高效沟通技巧',
instructor: '张明远',
duration: '2小时',
scheduleDate: null,
scheduleTime: null,
showSchedule: false
},
{
id: 2,
name: '团队协作与领导力',
instructor: '李思雨',
duration: '3小时',
scheduleDate: null,
scheduleTime: null,
showSchedule: false
}
],
organizationData: [
{
title: '飞利信科技有限公司',
key: '0-0',
count: 356,
children: [
{
title: '技术部',
key: '0-0-0',
count: 120,
children: [
{ title: '前端组', key: '0-0-0-0', count: 45 },
{ title: '后端组', key: '0-0-0-1', count: 52 },
{ title: '测试组', key: '0-0-0-2', count: 23 }
],
},
{
title: '产品部',
key: '0-0-1',
count: 68,
children: [
{ title: '产品设计组', key: '0-0-1-0', count: 28 },
{ title: '用户体验组', key: '0-0-1-1', count: 25 },
{ title: '产品运营组', key: '0-0-1-2', count: 15 }
],
},
{
title: '运营部',
key: '0-0-2',
count: 52,
children: [
{ title: '市场营销组', key: '0-0-2-0', count: 22 },
{ title: '客户服务组', key: '0-0-2-1', count: 18 },
{ title: '商务合作组', key: '0-0-2-2', count: 12 }
],
},
{
title: '财务部',
key: '0-0-3',
count: 32,
children: [
{ title: '会计组', key: '0-0-3-0', count: 18 },
{ title: '审计组', key: '0-0-3-1', count: 14 }
],
},
{
title: '人事部',
key: '0-0-4',
count: 84,
children: [
{ title: 'HR专员组', key: '0-0-4-0', count: 25 },
{ title: '招聘组', key: '0-0-4-1', count: 22 },
{ title: '培训组', key: '0-0-4-2', count: 18 },
{ title: '薪酬组', key: '0-0-4-3', count: 19 }
],
},
],
},
]
};
}
// 获取处理后的树形数据
getTreeData = () => {
const { organizationData } = this.state;
const processNode = (node) => ({
key: node.key,
title: (
<span className={styles['tree-node-title']}>
{this.getNodeIcon(node.title)}
<span className={styles['node-title']}>{node.title}</span>
<span className={styles['node-count']}>({node.count})</span>
</span>
),
children: node.children ? node.children.map(processNode) : undefined
});
return organizationData.map(processNode);
};
// 获取节点图标
getNodeIcon = (title) => {
if (title.includes('公司') || title.includes('集团')) return <ApartmentOutlined style={{ color: '#1890ff', fontSize: '14px' }} />;
if (title.includes('技术') || title.includes('开发') || title.includes('测试')) return <SettingOutlined style={{ color: '#52c41a', fontSize: '14px' }} />;
if (title.includes('产品') || title.includes('设计') || title.includes('体验')) return <TeamOutlined style={{ color: '#fa8c16', fontSize: '14px' }} />;
if (title.includes('运营') || title.includes('市场') || title.includes('客户') || title.includes('商务')) return <TeamOutlined style={{ color: '#eb2f96', fontSize: '14px' }} />;
if (title.includes('财务') || title.includes('会计') || title.includes('审计')) return <DollarOutlined style={{ color: '#722ed1', fontSize: '14px' }} />;
if (title.includes('人事') || title.includes('HR') || title.includes('招聘') || title.includes('培训') || title.includes('薪酬')) return <UserOutlined style={{ color: '#13c2c2', fontSize: '14px' }} />;
return <TeamOutlined style={{ color: '#666', fontSize: '14px' }} />;
};
// 刷新树数据
handleTreeRefresh = () => {
message.info('刷新组织架构数据');
};
// 展开/收缩所有节点
handleTreeToggle = () => {
const { expandedKeys, organizationData } = this.state;
if (expandedKeys.length > 0) {
this.setState({ expandedKeys: [] });
} else {
const getAllKeys = (nodes) => {
let keys = [];
nodes.forEach(node => {
keys.push(node.key);
if (node.children) {
keys = keys.concat(getAllKeys(node.children));
}
});
return keys;
};
this.setState({ expandedKeys: getAllKeys(organizationData) });
}
};
// 组织树选择
onTreeSelect = (selectedKeys, info) => {
console.log('选中节点:', selectedKeys, info);
this.setState({ selectedOrgKeys: selectedKeys });
};
// 树展开/收缩
onTreeExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
// 新建培训
handleCreateTraining = () => {
message.success('新建培训');
};
// 选择课程
handleSelectCourse = (course) => {
const { selectedCourses, availableCourses } = this.state;
// 检查是否已经选择
const isSelected = selectedCourses.find(item => item.id === course.id);
if (isSelected) {
message.warning('该课程已经选择');
return;
}
const newSelectedCourse = {
...course,
scheduleDate: null,
scheduleTime: null,
showSchedule: false
};
this.setState({
selectedCourses: [...selectedCourses, newSelectedCourse],
availableCourses: availableCourses.map(item =>
item.id === course.id ? { ...item, selected: true } : item
)
});
message.success('课程添加成功');
};
// 移除课程
handleRemoveCourse = (courseId) => {
const { selectedCourses, availableCourses } = this.state;
this.setState({
selectedCourses: selectedCourses.filter(item => item.id !== courseId),
availableCourses: availableCourses.map(item =>
item.id === courseId ? { ...item, selected: false } : item
)
});
message.success('课程移除成功');
};
// 设置课程时间
handleSetSchedule = (courseId) => {
const { selectedCourses } = this.state;
this.setState({
selectedCourses: selectedCourses.map(item =>
item.id === courseId ? { ...item, showSchedule: !item.showSchedule } : item
)
});
};
// 确认时间设置
handleConfirmSchedule = (courseId, date, time) => {
const { selectedCourses } = this.state;
this.setState({
selectedCourses: selectedCourses.map(item =>
item.id === courseId ? {
...item,
scheduleDate: date,
scheduleTime: time,
showSchedule: false
} : item
)
});
message.success('时间设置成功');
};
// 搜索课程
handleSearchCourse = (value) => {
this.setState({ searchKeyword: value });
};
// 保存培训
handleSave = () => {
message.success('培训保存成功');
};
// 取消
handleCancel = () => {
message.info('取消操作');
};
// 封面上传
handleCoverUpload = (info) => {
if (info.file.status === 'done') {
message.success('封面上传成功');
}
};
// 过滤课程
getFilteredCourses = () => {
const { availableCourses, searchKeyword } = this.state;
if (!searchKeyword) return availableCourses;
return availableCourses.filter(course =>
course.name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
course.instructor.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
render() {
const { selectedOrgKeys, expandedKeys, selectedCourses, searchKeyword } = this.state;
const filteredCourses = this.getFilteredCourses();
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<Card className={styles.contentCard}>
<Row gutter={24}>
{/* 左侧组织架构树 - 保持不变 */}
<Col span={5}>
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TeamOutlined style={{ marginRight: 8, color: '#1890ff' }} />
<span>组织架构</span>
</div>
<Space>
<Button
type="text"
icon={<SyncOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeRefresh}
title="刷新"
/>
<Button
type="text"
icon={<ExpandOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeToggle}
title={expandedKeys.length > 0 ? "收缩全部" : "展开全部"}
/>
</Space>
</div>
}
className={styles.treeCard}
>
<div className={styles.treeContainer}>
<Tree
showIcon
selectedKeys={selectedOrgKeys}
expandedKeys={expandedKeys}
treeData={this.getTreeData()}
onSelect={this.onTreeSelect}
onExpand={this.onTreeExpand}
className={styles.orgTree}
/>
</div>
</Card>
</Col>
{/* 右侧培训管理内容 */}
<Col span={19}>
<Card className={styles.mainCard}>
{/* 头部标题和操作按钮 */}
<div className={styles.header}>
<Title level={3} style={{ margin: 0, color: '#333', fontSize: '20px' }}>
培训管理
</Title>
<Button
type="primary"
icon={<PlusCircleOutlined />}
onClick={this.handleCreateTraining}
className={styles.createBtn}
>
新建培训
</Button>
</div>
{/* 滚动内容区 */}
<div className={styles.content}>
{/* 培训基本信息 */}
<Card className={styles.sectionCard} title="基本信息">
<Row gutter={16}>
<Col span={12}>
<div className={styles.formItem}>
<Text className={styles.formLabel}>培训名称</Text>
<Input
placeholder="请输入培训名称"
className={styles.formInput}
/>
</div>
</Col>
<Col span={12}>
<div className={styles.formItem}>
<Text className={styles.formLabel}>培训封面</Text>
<div className={styles.uploadContainer}>
<Upload
showUploadList={false}
onChange={this.handleCoverUpload}
className={styles.coverUpload}
>
<div className={styles.uploadArea}>
<PictureOutlined className={styles.uploadIcon} />
</div>
</Upload>
<Text className={styles.uploadText}>点击上传封面</Text>
</div>
</div>
</Col>
</Row>
<div className={styles.formItem}>
<Text className={styles.formLabel}>主题设计</Text>
<TextArea
rows={4}
placeholder="在此输入培训主题和描述..."
className={styles.formTextArea}
/>
</div>
</Card>
{/* 培训详情 */}
<Card className={styles.sectionCard} title="培训详情">
<Row gutter={16}>
<Col span={16}>
<div className={styles.formItem}>
<Text className={styles.formLabel}>培训时间</Text>
<RangePicker
style={{ width: '100%' }}
className={styles.formInput}
/>
</div>
</Col>
<Col span={8}>
<div className={styles.formItem}>
<Text className={styles.formLabel}>培训费用()</Text>
<InputNumber
placeholder="请输入费用"
style={{ width: '100%' }}
className={styles.formInput}
/>
</div>
</Col>
</Row>
</Card>
{/* 课程管理 */}
<Card className={styles.sectionCard} title="课程管理">
<Row gutter={16}>
{/* 可选课程 */}
<Col span={12}>
<div className={styles.courseSection}>
<div className={styles.courseSectionHeader}>
<Text className={styles.courseSectionTitle}>可选课程</Text>
<Input
placeholder="搜索课程"
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={(e) => this.handleSearchCourse(e.target.value)}
className={styles.searchInput}
/>
</div>
<div className={styles.courseList}>
{filteredCourses.map(course => (
<div
key={course.id}
className={`${styles.courseItem} ${course.selected ? styles.courseItemSelected : ''}`}
onClick={() => this.handleSelectCourse(course)}
>
<div className={styles.courseInfo}>
<Text className={styles.courseName}>{course.name}</Text>
<Text className={styles.courseInstructor}>讲师: {course.instructor}</Text>
</div>
<Tag className={styles.courseDuration}>{course.duration}</Tag>
</div>
))}
</div>
</div>
</Col>
{/* 已选课程 */}
<Col span={12}>
<div className={styles.courseSection}>
<Text className={styles.courseSectionTitle}>已选课程</Text>
<div className={styles.courseList}>
{selectedCourses.map(course => (
<div key={course.id} className={styles.selectedCourseItem}>
<div className={styles.courseItemHeader}>
<div className={styles.courseInfo}>
<Text className={styles.courseName}>{course.name}</Text>
<Text className={styles.courseInstructor}>讲师: {course.instructor}</Text>
</div>
<div className={styles.courseActions}>
<Tag className={styles.courseDuration}>{course.duration}</Tag>
<Button
type="link"
size="small"
onClick={() => this.handleSetSchedule(course.id)}
className={styles.scheduleBtn}
>
设置时间
</Button>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={() => this.handleRemoveCourse(course.id)}
className={styles.removeBtn}
/>
</div>
</div>
{course.showSchedule && (
<div className={styles.scheduleSettings}>
<div className={styles.scheduleRow}>
<Text className={styles.scheduleLabel}>上课时间:</Text>
<DatePicker
placeholder="选择日期"
className={styles.dateInput}
/>
<Input
type="time"
className={styles.timeInput}
/>
</div>
<div className={styles.scheduleActions}>
<Button
type="primary"
size="small"
onClick={() => this.handleConfirmSchedule(course.id)}
className={styles.confirmBtn}
>
确认
</Button>
</div>
</div>
)}
</div>
))}
</div>
</div>
</Col>
</Row>
</Card>
{/* 底部操作按钮 */}
<div className={styles.actionButtons}>
<Button onClick={this.handleCancel}>取消</Button>
<Button
type="primary"
onClick={this.handleSave}
className={styles.saveBtn}
>
保存
</Button>
</div>
</div>
</Card>
</Col>
</Row>
</Card>
</div>
</div>
);
}
}
export default Training;

@ -0,0 +1,525 @@
@import '~@/utils/utils.less';
// 使用与OnBoarding相同的基础样式
.container {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background-color: #f5f6fa;
}
.mainContent {
height: 100vh;
overflow: hidden;
.contentCard {
height: 100%;
border: none !important;
:global(.ant-card-body) {
padding: 12px 24px;
// height: calc(100vh - 24px);
// overflow: hidden;
}
}
}
// 组织架构树样式 - 与OnBoarding保持一致
.treeCard {
height: 600px;
border: 1px solid #e8e8e8;
border-radius: 8px;
.treeContainer {
height: 520px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
.orgTree {
.ant-tree-node-content-wrapper {
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px 8px;
&:hover {
background-color: #f0f9ff;
transform: translateX(2px);
}
&.ant-tree-node-selected {
background-color: #e6f7ff !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
}
}
}
.tree-node-title {
display: flex;
align-items: center;
width: 100%;
.node-title {
flex: 1;
font-size: 14px;
margin-left: 8px;
color: #333;
}
.node-count {
color: #999;
font-size: 12px;
margin-left: auto;
background: #f0f0f0;
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
}
// 主卡片样式
.mainCard {
height: calc(100vh - 60px);
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
.ant-card-body {
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
// 头部区域
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
height: 72px;
// background: #fafafa;
}
// 内容区域
.content {
flex: 1;
padding: 20px 24px 28px 24px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 关键允许flex子元素收缩
height: calc(100vh - 180px); // 减去头部高度和其他空间,强制设置高度
max-height: calc(100vh - 180px); // 明确最大高度,确保滚动条出现
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.28);
}
}
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.18) rgba(0, 0, 0, 0.02);
}
// 按钮样式
.createBtn {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
box-shadow: 0 2px 8px rgba(45, 92, 246, 0.3);
&:hover {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.4);
}
}
.saveBtn {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
box-shadow: 0 2px 8px rgba(45, 92, 246, 0.3);
&:hover {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.4);
}
}
// 区块卡片
.sectionCard {
margin-bottom: 20px;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&:last-child {
margin-bottom: 0;
}
.ant-card-head {
border-bottom: 1px solid #f0f0f0;
.ant-card-head-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.ant-card-body {
padding: 20px;
}
}
// 表单样式
.formItem {
margin-bottom: 16px;
.formLabel {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.formInput,
.formTextArea {
border-radius: 6px;
border: 1px solid #d9d9d9;
transition: all 0.3s ease;
&:hover {
border-color: #4c7bff;
}
&:focus,
&.ant-input-focused {
border-color: #2d5cf6;
box-shadow: 0 0 0 2px rgba(45, 92, 246, 0.2);
}
}
}
// 上传区域
.uploadContainer {
display: flex;
align-items: center;
.coverUpload {
margin-right: 8px;
.uploadArea {
width: 64px;
height: 64px;
border: 1px dashed #d9d9d9;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: #fafafa;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #2d5cf6;
background: #f0f9ff;
}
.uploadIcon {
font-size: 24px;
color: #bfbfbf;
}
}
}
.uploadText {
font-size: 12px;
color: #666;
}
}
// 课程管理区域
.courseSection {
.courseSectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.courseSectionTitle {
font-weight: 500;
color: #333;
font-size: 14px;
}
.searchInput {
width: 128px;
border-radius: 4px;
}
}
.courseList {
height: 256px;
overflow-y: auto;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 8px;
background: #fafafa;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 2px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
}
// 课程项
.courseItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin-bottom: 8px;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #e8e8e8;
}
&:last-child {
margin-bottom: 0;
}
.courseInfo {
flex: 1;
.courseName {
display: block;
font-weight: 500;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.courseInstructor {
font-size: 12px;
color: #666;
}
}
.courseDuration {
background: rgba(45, 92, 246, 0.1);
color: #2d5cf6;
border: none;
border-radius: 12px;
font-size: 12px;
padding: 2px 8px;
}
}
.courseItemSelected {
background: #f0f0f0 !important;
cursor: not-allowed;
&:hover {
transform: none;
box-shadow: none;
}
}
// 已选课程项
.selectedCourseItem {
margin-bottom: 8px;
padding: 12px;
background: rgba(45, 92, 246, 0.05);
border-radius: 6px;
border: 1px solid rgba(45, 92, 246, 0.1);
&:last-child {
margin-bottom: 0;
}
.courseItemHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.courseActions {
display: flex;
align-items: center;
gap: 4px;
.scheduleBtn {
color: #2d5cf6;
font-size: 12px;
padding: 0;
height: auto;
&:hover {
text-decoration: underline;
}
}
.removeBtn {
color: #ff4d4f;
&:hover {
background: rgba(255, 77, 79, 0.1);
}
}
}
}
}
// 时间设置区域
.scheduleSettings {
background: #f5f5f5;
padding: 8px 12px;
border-radius: 4px;
.scheduleRow {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.scheduleLabel {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.dateInput {
flex: 1;
height: 28px;
font-size: 12px;
}
.timeInput {
width: 96px;
height: 28px;
font-size: 12px;
}
}
.scheduleActions {
display: flex;
justify-content: flex-end;
.confirmBtn {
height: 24px;
font-size: 12px;
padding: 0 12px;
}
}
}
// 底部操作按钮
.actionButtons {
margin-top: 24px;
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
// 响应式设计
@media (max-width: 1200px) {
.treeCard {
height: 300px;
}
.mainCard {
height: auto;
max-height: 80vh;
min-height: 500px;
.content {
max-height: 65vh;
&::-webkit-scrollbar {
width: 4px;
}
}
}
.courseList {
height: 200px;
}
}
// 页面特定样式
.container {
.ant-btn-primary {
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.ant-btn:not(.ant-btn-primary) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover,
&:focus {
border-color: #4c7bff;
color: #2d5cf6;
}
}
}

@ -0,0 +1,599 @@
import React, { PureComponent } from 'react';
import {
Card,
Tree,
Input,
Button,
DatePicker,
Select,
Row,
Col,
Space,
Radio,
Checkbox,
InputNumber,
Avatar,
message,
Timeline,
Tag,
Typography,
Divider,
Upload,
Form,
Table,
Pagination
} from 'antd';
import {
SaveOutlined,
CloseOutlined,
PlusCircleOutlined,
DeleteOutlined,
ExpandOutlined,
UserOutlined,
TeamOutlined,
ApartmentOutlined,
SyncOutlined,
DollarOutlined,
SettingOutlined,
CalendarOutlined,
UsergroupAddOutlined,
UserSwitchOutlined,
StopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ContactsOutlined,
PrinterOutlined,
CheckOutlined,
ClockCircleOutlined,
LoadingOutlined,
EyeOutlined,
SearchOutlined,
PictureOutlined,
UploadOutlined,
EditOutlined,
ShareAltOutlined,
FilterOutlined,
DownloadOutlined,
PlusOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import styles from './TrainingPlan.less';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
class TrainingPlan extends PureComponent {
constructor(props) {
super(props);
this.state = {
selectedOrgKeys: ['0-0-4'], // 默认选中人事部
expandedKeys: ['0-0', '0-0-0', '0-0-1', '0-0-2'],
// 培训计划详情数据
trainingPlanData: {
id: 'TP2023Q3001',
title: '2023年第三季度新员工培训计划',
dateRange: '2023-07-01 至 2023-09-30',
participants: 86,
manager: '张明远',
status: '进行中',
completedCourses: 12,
totalCourses: 24,
participationRate: 87.5,
description: '本培训计划针对2023年第三季度新入职员工设计包含公司文化、产品知识、业务流程等全方位培训内容。通过系统化的培训帮助新员工快速融入团队掌握工作所需的基本技能和知识提升工作效率和团队协作能力。'
},
// 课程列表数据
courseList: [
{
key: '1',
courseName: '公司文化与价值观',
instructor: '李华',
courseTime: '2023-07-03 09:00-12:00',
courseType: '现场',
courseUrl: '进入课程',
location: '总部大楼3层培训室A'
},
{
key: '2',
courseName: '产品知识入门',
instructor: '王伟',
courseTime: '2023-07-05 14:00-17:00',
courseType: '在线',
courseUrl: '进入课程',
location: 'https://meeting.zoom.us/123456'
},
{
key: '3',
courseName: '业务流程概述',
instructor: '张丽',
courseTime: '2023-07-07 09:00-12:00',
courseType: '现场',
courseUrl: '进入课程',
location: '总部大楼3层培训室B'
},
{
key: '4',
courseName: '团队协作与沟通',
instructor: '陈明',
courseTime: '2023-07-10 14:00-17:00',
courseType: '在线',
courseUrl: '进入课程',
location: 'https://meeting.zoom.us/789012'
},
{
key: '5',
courseName: '办公软件高级应用',
instructor: '刘芳',
courseTime: '2023-07-12 09:00-12:00',
courseType: '现场',
courseUrl: '进入课程',
location: '总部大楼3层培训室A'
}
],
pagination: {
current: 1,
pageSize: 5,
total: 24
},
organizationData: [
{
title: '飞利信科技有限公司',
key: '0-0',
count: 356,
children: [
{
title: '技术部',
key: '0-0-0',
count: 120,
children: [
{ title: '前端组', key: '0-0-0-0', count: 45 },
{ title: '后端组', key: '0-0-0-1', count: 52 },
{ title: '测试组', key: '0-0-0-2', count: 23 }
],
},
{
title: '产品部',
key: '0-0-1',
count: 68,
children: [
{ title: '产品设计组', key: '0-0-1-0', count: 28 },
{ title: '用户体验组', key: '0-0-1-1', count: 25 },
{ title: '产品运营组', key: '0-0-1-2', count: 15 }
],
},
{
title: '运营部',
key: '0-0-2',
count: 52,
children: [
{ title: '市场营销组', key: '0-0-2-0', count: 22 },
{ title: '客户服务组', key: '0-0-2-1', count: 18 },
{ title: '商务合作组', key: '0-0-2-2', count: 12 }
],
},
{
title: '财务部',
key: '0-0-3',
count: 32,
children: [
{ title: '会计组', key: '0-0-3-0', count: 18 },
{ title: '审计组', key: '0-0-3-1', count: 14 }
],
},
{
title: '人事部',
key: '0-0-4',
count: 84,
children: [
{ title: 'HR专员组', key: '0-0-4-0', count: 25 },
{ title: '招聘组', key: '0-0-4-1', count: 22 },
{ title: '培训组', key: '0-0-4-2', count: 18 },
{ title: '薪酬组', key: '0-0-4-3', count: 19 }
],
},
],
},
]
};
}
// 获取处理后的树形数据
getTreeData = () => {
const { organizationData } = this.state;
const processNode = (node) => ({
key: node.key,
title: (
<span className={styles['tree-node-title']}>
{this.getNodeIcon(node.title)}
<span className={styles['node-title']}>{node.title}</span>
<span className={styles['node-count']}>({node.count})</span>
</span>
),
children: node.children ? node.children.map(processNode) : undefined
});
return organizationData.map(processNode);
};
// 获取节点图标
getNodeIcon = (title) => {
if (title.includes('公司') || title.includes('集团')) return <ApartmentOutlined style={{ color: '#1890ff', fontSize: '14px' }} />;
if (title.includes('技术') || title.includes('开发') || title.includes('测试')) return <SettingOutlined style={{ color: '#52c41a', fontSize: '14px' }} />;
if (title.includes('产品') || title.includes('设计') || title.includes('体验')) return <TeamOutlined style={{ color: '#fa8c16', fontSize: '14px' }} />;
if (title.includes('运营') || title.includes('市场') || title.includes('客户') || title.includes('商务')) return <TeamOutlined style={{ color: '#eb2f96', fontSize: '14px' }} />;
if (title.includes('财务') || title.includes('会计') || title.includes('审计')) return <DollarOutlined style={{ color: '#722ed1', fontSize: '14px' }} />;
if (title.includes('人事') || title.includes('HR') || title.includes('招聘') || title.includes('培训') || title.includes('薪酬')) return <UserOutlined style={{ color: '#13c2c2', fontSize: '14px' }} />;
return <TeamOutlined style={{ color: '#666', fontSize: '14px' }} />;
};
// 刷新树数据
handleTreeRefresh = () => {
message.info('刷新组织架构数据');
};
// 展开/收缩所有节点
handleTreeToggle = () => {
const { expandedKeys, organizationData } = this.state;
if (expandedKeys.length > 0) {
this.setState({ expandedKeys: [] });
} else {
const getAllKeys = (nodes) => {
let keys = [];
nodes.forEach(node => {
keys.push(node.key);
if (node.children) {
keys = keys.concat(getAllKeys(node.children));
}
});
return keys;
};
this.setState({ expandedKeys: getAllKeys(organizationData) });
}
};
// 组织树选择
onTreeSelect = (selectedKeys, info) => {
console.log('选中节点:', selectedKeys, info);
this.setState({ selectedOrgKeys: selectedKeys });
};
// 树展开/收缩
onTreeExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
// 编辑培训计划
handleEdit = () => {
message.success('编辑培训计划');
};
// 分享培训计划
handleShare = () => {
message.success('分享培训计划');
};
// 筛选课程
handleFilter = () => {
message.info('筛选课程');
};
// 导出课程
handleExport = () => {
message.success('导出课程列表');
};
// 添加课程
handleAddCourse = () => {
message.success('添加新课程');
};
// 编辑课程
handleEditCourse = (record) => {
message.success(`编辑课程: ${record.courseName}`);
};
// 删除课程
handleDeleteCourse = (record) => {
message.success(`删除课程: ${record.courseName}`);
};
// 进入课程
handleEnterCourse = (record) => {
message.success(`进入课程: ${record.courseName}`);
};
// 分页变化
handlePaginationChange = (page, pageSize) => {
this.setState({
pagination: {
...this.state.pagination,
current: page,
pageSize: pageSize
}
});
};
// 获取课程类型标签
getCourseTypeTag = (type) => {
return type === '现场' ?
<Tag color="blue" className={styles.typeTag}>现场</Tag> :
<Tag color="green" className={styles.typeTag}>在线</Tag>;
};
render() {
const { selectedOrgKeys, expandedKeys, trainingPlanData, courseList, pagination } = this.state;
// 课程表格列配置
const columns = [
{
title: '课程名称',
dataIndex: 'courseName',
key: 'courseName',
render: (text) => <Text strong>{text}</Text>
},
{
title: '讲师',
dataIndex: 'instructor',
key: 'instructor'
},
{
title: '课程时间',
dataIndex: 'courseTime',
key: 'courseTime'
},
{
title: '课程类型',
dataIndex: 'courseType',
key: 'courseType',
render: (type) => this.getCourseTypeTag(type)
},
{
title: '课程入口',
dataIndex: 'courseUrl',
key: 'courseUrl',
render: (text, record) => (
<Button
type="link"
onClick={() => this.handleEnterCourse(record)}
className={styles.enterBtn}
>
{text}
</Button>
)
},
{
title: '课程地址',
dataIndex: 'location',
key: 'location'
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => this.handleEditCourse(record)}
className={styles.actionBtn}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={() => this.handleDeleteCourse(record)}
className={styles.deleteBtn}
/>
</Space>
)
}
];
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<Card className={styles.contentCard}>
<Row gutter={24}>
{/* 左侧组织架构树 - 保持不变 */}
<Col span={5}>
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TeamOutlined style={{ marginRight: 8, color: '#1890ff' }} />
<span>组织架构</span>
</div>
<Space>
<Button
type="text"
icon={<SyncOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeRefresh}
title="刷新"
/>
<Button
type="text"
icon={<ExpandOutlined />}
size="small"
style={{ color: '#1890ff' }}
onClick={this.handleTreeToggle}
title={expandedKeys.length > 0 ? "收缩全部" : "展开全部"}
/>
</Space>
</div>
}
className={styles.treeCard}
>
<div className={styles.treeContainer}>
<Tree
showIcon
selectedKeys={selectedOrgKeys}
expandedKeys={expandedKeys}
treeData={this.getTreeData()}
onSelect={this.onTreeSelect}
onExpand={this.onTreeExpand}
className={styles.orgTree}
/>
</div>
</Card>
</Col>
{/* 右侧培训计划详情 */}
<Col span={19}>
<Card className={styles.mainCard}>
{/* 滚动内容区 */}
<div className={styles.content}>
{/* 培训基本信息 */}
<Card className={styles.infoCard}>
<div className={styles.planHeader}>
<div className={styles.planInfo}>
<Title level={2} className={styles.planTitle}>
{trainingPlanData.title}
</Title>
<div className={styles.planMeta}>
<Space size="large">
<span className={styles.metaItem}>
<CalendarOutlined className={styles.metaIcon} />
{trainingPlanData.dateRange}
</span>
<span className={styles.metaItem}>
<TeamOutlined className={styles.metaIcon} />
参与人数: {trainingPlanData.participants}
</span>
<span className={styles.metaItem}>
<ContactsOutlined className={styles.metaIcon} />
负责人: {trainingPlanData.manager}
</span>
</Space>
</div>
</div>
<div className={styles.planActions}>
<Button
type="primary"
icon={<EditOutlined />}
onClick={this.handleEdit}
className={styles.editBtn}
>
编辑
</Button>
<Button
icon={<ShareAltOutlined />}
onClick={this.handleShare}
className={styles.shareBtn}
>
分享
</Button>
</div>
</div>
{/* 统计信息 */}
<Row gutter={16} className={styles.statsRow}>
<Col span={8}>
<div className={styles.statCard}>
<div className={styles.statLabel}>培训状态</div>
<div className={styles.statValue}>
<Tag color="processing">{trainingPlanData.status}</Tag>
</div>
</div>
</Col>
<Col span={8}>
<div className={styles.statCard}>
<div className={styles.statLabel}>已完成课程</div>
<div className={styles.statValue}>
{trainingPlanData.completedCourses}/{trainingPlanData.totalCourses}
</div>
</div>
</Col>
<Col span={8}>
<div className={styles.statCard}>
<div className={styles.statLabel}>平均参与率</div>
<div className={styles.statValue}>
{trainingPlanData.participationRate}%
</div>
</div>
</Col>
</Row>
{/* 培训描述 */}
<div className={styles.description}>
<Title level={4} className={styles.descTitle}>培训描述</Title>
<Paragraph className={styles.descContent}>
{trainingPlanData.description}
</Paragraph>
</div>
</Card>
{/* 课程计划列表 */}
<Card className={styles.courseCard}>
<div className={styles.courseHeader}>
<Title level={3} className={styles.courseTitle}>课程计划</Title>
<Space>
<Button
icon={<FilterOutlined />}
onClick={this.handleFilter}
className={styles.filterBtn}
>
筛选
</Button>
<Button
icon={<DownloadOutlined />}
onClick={this.handleExport}
className={styles.exportBtn}
>
导出
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={this.handleAddCourse}
className={styles.addBtn}
>
添加课程
</Button>
</Space>
</div>
{/* 课程表格 */}
<div className={styles.courseTable}>
<Table
columns={columns}
dataSource={courseList}
pagination={false}
className={styles.table}
onRow={(record) => ({
className: styles.tableRow
})}
/>
{/* 分页 */}
<div className={styles.paginationContainer}>
<div className={styles.paginationInfo}>
显示 {((pagination.current - 1) * pagination.pageSize) + 1} {' '}
{Math.min(pagination.current * pagination.pageSize, pagination.total)}
{pagination.total}
</div>
<Pagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={this.handlePaginationChange}
showSizeChanger={false}
className={styles.pagination}
/>
</div>
</div>
</Card>
</div>
</Card>
</Col>
</Row>
</Card>
</div>
</div>
);
}
}
export default TrainingPlan;

@ -0,0 +1,455 @@
@import '~@/utils/utils.less';
// 使用与OnBoarding相同的基础样式
.container {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background-color: #f5f6fa;
}
.mainContent {
height: 100vh;
overflow: hidden;
.contentCard {
height: 100%;
border: none !important;
:global(.ant-card-body) {
padding: 12px 24px;
// height: calc(100vh - 24px);
// overflow: hidden;
}
}
}
// 组织架构树样式 - 与OnBoarding保持一致
.treeCard {
height: 600px;
border: 1px solid #e8e8e8;
border-radius: 8px;
.treeContainer {
height: 520px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
.orgTree {
.ant-tree-node-content-wrapper {
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px 8px;
&:hover {
background-color: #f0f9ff;
transform: translateX(2px);
}
&.ant-tree-node-selected {
background-color: #e6f7ff !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
}
}
}
.tree-node-title {
display: flex;
align-items: center;
width: 100%;
.node-title {
flex: 1;
font-size: 14px;
margin-left: 8px;
color: #333;
}
.node-count {
color: #999;
font-size: 12px;
margin-left: auto;
background: #f0f0f0;
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
}
// 主卡片样式
.mainCard {
height: calc(100vh - 60px);
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
.ant-card-body {
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
// 内容区域
.content {
flex: 1;
padding: 20px 24px 28px 24px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 关键允许flex子元素收缩
height: calc(100vh - 110px); // 减去头部高度和其他空间,强制设置高度
max-height: calc(100vh - 110px); // 明确最大高度,确保滚动条出现
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.28);
}
}
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.18) rgba(0, 0, 0, 0.02);
}
// 信息卡片
.infoCard {
margin-bottom: 24px;
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.ant-card-body {
padding: 24px;
}
}
.courseCard {
border-radius: 8px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.ant-card-body {
padding: 24px;
}
}
// 培训计划头部
.planHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
.planInfo {
flex: 1;
.planTitle {
margin: 0 0 8px 0 !important;
font-size: 24px !important;
font-weight: 700 !important;
color: #333 !important;
}
.planMeta {
.metaItem {
color: #666;
font-size: 14px;
.metaIcon {
margin-right: 4px;
color: #999;
}
}
}
}
.planActions {
display: flex;
gap: 12px;
}
}
// 按钮样式
.editBtn {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
box-shadow: 0 2px 8px rgba(45, 92, 246, 0.3);
&:hover {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.4);
}
}
.shareBtn,
.filterBtn,
.exportBtn {
border-color: #d9d9d9;
transition: all 0.3s ease;
&:hover {
border-color: #2d5cf6;
color: #2d5cf6;
transform: translateY(-1px);
}
}
.addBtn {
background: linear-gradient(135deg, #2d5cf6 0%, #4c7bff 100%);
border: none;
box-shadow: 0 2px 8px rgba(45, 92, 246, 0.3);
&:hover {
background: linear-gradient(135deg, #4c7bff 0%, #6b8fff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(45, 92, 246, 0.4);
}
}
// 统计卡片
.statsRow {
margin-bottom: 24px;
.statCard {
padding: 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
text-align: center;
.statLabel {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.statValue {
font-weight: 600;
font-size: 16px;
color: #333;
}
}
}
// 描述区域
.description {
.descTitle {
margin: 0 0 8px 0 !important;
font-size: 16px !important;
font-weight: 600 !important;
color: #333 !important;
}
.descContent {
margin: 0 !important;
color: #666;
font-size: 14px;
line-height: 1.6;
}
}
// 课程区域
.courseHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.courseTitle {
margin: 0 !important;
font-size: 20px !important;
font-weight: 700 !important;
color: #333 !important;
}
}
// 表格样式
.courseTable {
.table {
.ant-table-thead > tr > th {
background-color: #fafafa;
border-bottom: 1px solid #e8e8e8;
font-weight: 600;
color: #333;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ant-table-tbody > tr {
transition: all 0.3s ease;
&:hover {
background-color: #f8f9fa !important;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
> td {
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #333;
padding: 12px 24px;
}
}
}
}
// 类型标签
.typeTag {
border-radius: 12px;
font-size: 12px;
padding: 2px 8px;
border: none;
}
// 操作按钮
.actionBtn {
color: #2d5cf6;
&:hover {
background: rgba(45, 92, 246, 0.1);
color: #2d5cf6;
}
}
.deleteBtn {
color: #ff4d4f;
&:hover {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
}
.enterBtn {
color: #2d5cf6;
padding: 0;
font-size: 14px;
&:hover {
text-decoration: underline;
}
}
// 分页容器
.paginationContainer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
.paginationInfo {
color: #666;
font-size: 14px;
}
.pagination {
.ant-pagination-item {
border-radius: 4px;
&.ant-pagination-item-active {
background: #2d5cf6;
border-color: #2d5cf6;
}
}
.ant-pagination-prev,
.ant-pagination-next {
border-radius: 4px;
}
}
}
// 响应式设计
@media (max-width: 1200px) {
.treeCard {
height: 300px;
}
.mainCard {
height: auto;
max-height: 80vh;
min-height: 500px;
.content {
max-height: 65vh;
&::-webkit-scrollbar {
width: 4px;
}
}
}
.planHeader {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.statsRow {
.statCard {
margin-bottom: 12px;
}
}
.courseHeader {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
}
// 页面特定样式
.container {
.ant-btn-primary {
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.ant-btn:not(.ant-btn-primary) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover,
&:focus {
border-color: #4c7bff;
color: #2d5cf6;
}
}
}
Loading…
Cancel
Save