|
@@ -0,0 +1,769 @@
|
|
|
|
|
+import React, { useState } from 'react';
|
|
|
|
|
+import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown, Briefcase } from 'lucide-react';
|
|
|
|
|
+
|
|
|
|
|
+interface PositionNode {
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ code?: string;
|
|
|
|
|
+ department?: string;
|
|
|
|
|
+ description?: string;
|
|
|
|
|
+ status?: string;
|
|
|
|
|
+ userCount?: number;
|
|
|
|
|
+ createTime?: string;
|
|
|
|
|
+ parentId?: number;
|
|
|
|
|
+ children?: PositionNode[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function PositionManagement() {
|
|
|
|
|
+ const [expandedPositionIds, setExpandedPositionIds] = useState<number[]>([1]); // 默认展开
|
|
|
|
|
+ const [selectedPositionId, setSelectedPositionId] = useState<number | null>(1); // 选中的岗位ID
|
|
|
|
|
+ const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
+ const [showAddModal, setShowAddModal] = useState(false);
|
|
|
|
|
+ const [editingItem, setEditingItem] = useState<any>(null);
|
|
|
|
|
+
|
|
|
|
|
+ // 岗位树形数据
|
|
|
|
|
+ const positionTree: PositionNode[] = [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 1,
|
|
|
|
|
+ name: '管理岗位',
|
|
|
|
|
+ code: 'MGMT',
|
|
|
|
|
+ department: '总公司',
|
|
|
|
|
+ description: '管理类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 15,
|
|
|
|
|
+ createTime: '2024-01-01',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 2,
|
|
|
|
|
+ name: '高级管理',
|
|
|
|
|
+ code: 'MGMT-HIGH',
|
|
|
|
|
+ department: '总公司',
|
|
|
|
|
+ description: '高级管理岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 5,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 1,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 21,
|
|
|
|
|
+ name: 'CEO',
|
|
|
|
|
+ code: 'CEO',
|
|
|
|
|
+ department: '总公司',
|
|
|
|
|
+ description: '首席执行官',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 1,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 22,
|
|
|
|
|
+ name: 'CTO',
|
|
|
|
|
+ code: 'CTO',
|
|
|
|
|
+ department: '技术中心',
|
|
|
|
|
+ description: '首席技术官',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 1,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 23,
|
|
|
|
|
+ name: 'CFO',
|
|
|
|
|
+ code: 'CFO',
|
|
|
|
|
+ department: '行政中心',
|
|
|
|
|
+ description: '首席财务官',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 1,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 2
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 3,
|
|
|
|
|
+ name: '部门管理',
|
|
|
|
|
+ code: 'MGMT-DEPT',
|
|
|
|
|
+ department: '各部门',
|
|
|
|
|
+ description: '部门管理岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 8,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 1,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 31,
|
|
|
|
|
+ name: '部门经理',
|
|
|
|
|
+ code: 'DEPT-MGR',
|
|
|
|
|
+ department: '各部门',
|
|
|
|
|
+ description: '部门经理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 5,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 3
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 32,
|
|
|
|
|
+ name: '部门副经理',
|
|
|
|
|
+ code: 'DEPT-DMGR',
|
|
|
|
|
+ department: '各部门',
|
|
|
|
|
+ description: '部门副经理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 3,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 3
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 4,
|
|
|
|
|
+ name: '技术岗位',
|
|
|
|
|
+ code: 'TECH',
|
|
|
|
|
+ department: '技术中心',
|
|
|
|
|
+ description: '技术类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 45,
|
|
|
|
|
+ createTime: '2024-01-01',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 5,
|
|
|
|
|
+ name: '研发岗位',
|
|
|
|
|
+ code: 'TECH-RD',
|
|
|
|
|
+ department: '研发部',
|
|
|
|
|
+ description: '研发类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 25,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 4,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 51,
|
|
|
|
|
+ name: '高级工程师',
|
|
|
|
|
+ code: 'RD-SENIOR',
|
|
|
|
|
+ department: '研发部',
|
|
|
|
|
+ description: '高级研发工程师',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 8,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 5
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 52,
|
|
|
|
|
+ name: '工程师',
|
|
|
|
|
+ code: 'RD-ENG',
|
|
|
|
|
+ department: '研发部',
|
|
|
|
|
+ description: '研发工程师',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 12,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 5
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 53,
|
|
|
|
|
+ name: '助理工程师',
|
|
|
|
|
+ code: 'RD-ASSIST',
|
|
|
|
|
+ department: '研发部',
|
|
|
|
|
+ description: '助理研发工程师',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 5,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 5
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 6,
|
|
|
|
|
+ name: '测试岗位',
|
|
|
|
|
+ code: 'TECH-QA',
|
|
|
|
|
+ department: '测试部',
|
|
|
|
|
+ description: '测试类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 12,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 4,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 61,
|
|
|
|
|
+ name: '测试工程师',
|
|
|
|
|
+ code: 'QA-ENG',
|
|
|
|
|
+ department: '测试部',
|
|
|
|
|
+ description: '测试工程师',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 8,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 6
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 62,
|
|
|
|
|
+ name: '测试助理',
|
|
|
|
|
+ code: 'QA-ASSIST',
|
|
|
|
|
+ department: '测试部',
|
|
|
|
|
+ description: '测试助理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 4,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 6
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 7,
|
|
|
|
|
+ name: '运维岗位',
|
|
|
|
|
+ code: 'TECH-OPS',
|
|
|
|
|
+ department: '运维部',
|
|
|
|
|
+ description: '运维类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 8,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 4,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 71,
|
|
|
|
|
+ name: '运维工程师',
|
|
|
|
|
+ code: 'OPS-ENG',
|
|
|
|
|
+ department: '运维部',
|
|
|
|
|
+ description: '运维工程师',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 6,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 7
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 72,
|
|
|
|
|
+ name: '运维助理',
|
|
|
|
|
+ code: 'OPS-ASSIST',
|
|
|
|
|
+ department: '运维部',
|
|
|
|
|
+ description: '运维助理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 2,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 7
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 8,
|
|
|
|
|
+ name: '行政岗位',
|
|
|
|
|
+ code: 'ADMIN',
|
|
|
|
|
+ department: '行政中心',
|
|
|
|
|
+ description: '行政类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 25,
|
|
|
|
|
+ createTime: '2024-01-01',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 9,
|
|
|
|
|
+ name: '人力资源',
|
|
|
|
|
+ code: 'ADMIN-HR',
|
|
|
|
|
+ department: '人力资源部',
|
|
|
|
|
+ description: '人力资源岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 10,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 8,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 91,
|
|
|
|
|
+ name: 'HR经理',
|
|
|
|
|
+ code: 'HR-MGR',
|
|
|
|
|
+ department: '人力资源部',
|
|
|
|
|
+ description: '人力资源经理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 1,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 9
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 92,
|
|
|
|
|
+ name: 'HR专员',
|
|
|
|
|
+ code: 'HR-SPEC',
|
|
|
|
|
+ department: '人力资源部',
|
|
|
|
|
+ description: '人力资源专员',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 6,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 9
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 93,
|
|
|
|
|
+ name: 'HR助理',
|
|
|
|
|
+ code: 'HR-ASSIST',
|
|
|
|
|
+ department: '人力资源部',
|
|
|
|
|
+ description: '人力资源助理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 3,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 9
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 10,
|
|
|
|
|
+ name: '财务岗位',
|
|
|
|
|
+ code: 'ADMIN-FIN',
|
|
|
|
|
+ department: '财务部',
|
|
|
|
|
+ description: '财务类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 8,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 8,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 101,
|
|
|
|
|
+ name: '财务经理',
|
|
|
|
|
+ code: 'FIN-MGR',
|
|
|
|
|
+ department: '财务部',
|
|
|
|
|
+ description: '财务经理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 1,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 10
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 102,
|
|
|
|
|
+ name: '会计',
|
|
|
|
|
+ code: 'FIN-ACC',
|
|
|
|
|
+ department: '财务部',
|
|
|
|
|
+ description: '会计',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 4,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 10
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 103,
|
|
|
|
|
+ name: '出纳',
|
|
|
|
|
+ code: 'FIN-CASH',
|
|
|
|
|
+ department: '财务部',
|
|
|
|
|
+ description: '出纳',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 3,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 10
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 11,
|
|
|
|
|
+ name: '行政岗位',
|
|
|
|
|
+ code: 'ADMIN-OFF',
|
|
|
|
|
+ department: '行政部',
|
|
|
|
|
+ description: '行政类岗位',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 7,
|
|
|
|
|
+ createTime: '2024-01-05',
|
|
|
|
|
+ parentId: 8,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 111,
|
|
|
|
|
+ name: '行政经理',
|
|
|
|
|
+ code: 'OFF-MGR',
|
|
|
|
|
+ department: '行政部',
|
|
|
|
|
+ description: '行政经理',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 1,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 11
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 112,
|
|
|
|
|
+ name: '行政专员',
|
|
|
|
|
+ code: 'OFF-SPEC',
|
|
|
|
|
+ department: '行政部',
|
|
|
|
|
+ description: '行政专员',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 4,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 11
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 113,
|
|
|
|
|
+ name: '前台',
|
|
|
|
|
+ code: 'OFF-REC',
|
|
|
|
|
+ department: '行政部',
|
|
|
|
|
+ description: '前台接待',
|
|
|
|
|
+ status: '启用',
|
|
|
|
|
+ userCount: 2,
|
|
|
|
|
+ createTime: '2024-01-10',
|
|
|
|
|
+ parentId: 11
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 切换展开/收起
|
|
|
|
|
+ const togglePositionExpand = (id: number) => {
|
|
|
|
|
+ setExpandedPositionIds(prev =>
|
|
|
|
|
+ prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取所有岗位(扁平化)
|
|
|
|
|
+ const getAllPositions = (nodes: PositionNode[], result: PositionNode[] = []): PositionNode[] => {
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ result.push(node);
|
|
|
|
|
+ if (node.children) {
|
|
|
|
|
+ getAllPositions(node.children, result);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return result;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const allPositions = getAllPositions(positionTree);
|
|
|
|
|
+
|
|
|
|
|
+ // 根据选中的岗位ID过滤显示的岗位列表
|
|
|
|
|
+ const getFilteredPositions = () => {
|
|
|
|
|
+ if (!selectedPositionId) return allPositions;
|
|
|
|
|
+
|
|
|
|
|
+ // 找到选中的岗位
|
|
|
|
|
+ const selectedPosition = allPositions.find(p => p.id === selectedPositionId);
|
|
|
|
|
+ if (!selectedPosition) return allPositions;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有子岗位,只显示直接子岗位
|
|
|
|
|
+ if (selectedPosition.children && selectedPosition.children.length > 0) {
|
|
|
|
|
+ return selectedPosition.children;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有子岗位,返回自己
|
|
|
|
|
+ return [selectedPosition];
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const displayedPositions = getFilteredPositions().filter((position) =>
|
|
|
|
|
+ position.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
+ position.code?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
+ position.department?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 递归渲染树形节点
|
|
|
|
|
+ const renderTreeNode = (node: PositionNode, level: number = 0) => {
|
|
|
|
|
+ const isExpanded = expandedPositionIds.includes(node.id);
|
|
|
|
|
+ const isSelected = selectedPositionId === node.id;
|
|
|
|
|
+ const hasChildren = node.children && node.children.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={node.id}>
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer transition-all rounded-lg ${
|
|
|
|
|
+ isSelected
|
|
|
|
|
+ ? 'bg-blue-100 text-blue-700'
|
|
|
|
|
+ : 'hover:bg-gray-100 text-gray-700'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ style={{ paddingLeft: `${level * 16 + 12}px` }}
|
|
|
|
|
+ onClick={() => setSelectedPositionId(node.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {hasChildren ? (
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={(e) => {
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ togglePositionExpand(node.id);
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="p-0.5 hover:bg-gray-200 rounded transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isExpanded ? (
|
|
|
|
|
+ <ChevronDown className="w-4 h-4" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <ChevronRight className="w-4 h-4" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="w-5"></span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <Briefcase className="w-4 h-4" />
|
|
|
|
|
+ <span className="text-sm flex-1">{node.name}</span>
|
|
|
|
|
+ {node.userCount && (
|
|
|
|
|
+ <span className="text-xs px-2 py-0.5 bg-gray-200 rounded">
|
|
|
|
|
+ {node.userCount}人
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {isExpanded && hasChildren && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ {node.children!.map(child => renderTreeNode(child, level + 1))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleDelete = (id: number) => {
|
|
|
|
|
+ if (confirm('确定要删除这个岗位吗?')) {
|
|
|
|
|
+ console.log('删除岗位:', id);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleEdit = (item: any) => {
|
|
|
|
|
+ setEditingItem(item);
|
|
|
|
|
+ setShowAddModal(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取所有部门(用于下拉选择)
|
|
|
|
|
+ const departments = ['总公司', '技术中心', '市场中心', '行政中心', '安全部', '研发部', '测试部', '运维部', '市场部', '销售部', '人力资源部', '财务部', '行政部'];
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex gap-6 h-full">
|
|
|
|
|
+ {/* 左侧树形结构 */}
|
|
|
|
|
+ <div className="w-80 flex-shrink-0">
|
|
|
|
|
+ <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4 h-full flex flex-col">
|
|
|
|
|
+ <div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-200">
|
|
|
|
|
+ <h3 className="text-base text-gray-900">岗位分类</h3>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setEditingItem(null);
|
|
|
|
|
+ setShowAddModal(true);
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
|
|
|
+ title="新增岗位"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Plus className="w-4 h-4" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex-1 overflow-y-auto">
|
|
|
|
|
+ {positionTree.map(node => renderTreeNode(node))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 右侧列表 */}
|
|
|
|
|
+ <div className="flex-1 space-y-6">
|
|
|
|
|
+ {/* 工具栏 */}
|
|
|
|
|
+ <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="relative w-80">
|
|
|
|
|
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ placeholder="搜索岗位..."
|
|
|
|
|
+ value={searchTerm}
|
|
|
|
|
+ onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
+ className="w-full h-10 pl-10 pr-4 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setEditingItem(null);
|
|
|
|
|
+ setShowAddModal(true);
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Plus className="w-4 h-4" strokeWidth={2.5} />
|
|
|
|
|
+ <span className="text-sm">新增岗位</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 表格 */}
|
|
|
|
|
+ <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
|
|
|
|
|
+ <div className="overflow-x-auto">
|
|
|
|
|
+ <table className="w-full">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
|
|
|
|
|
+ 序号
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
|
|
|
|
|
+ 岗位名称
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '12%' }}>
|
|
|
|
|
+ 岗位编码
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
|
|
|
|
|
+ 所属部门
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '20%' }}>
|
|
|
|
|
+ 岗位描述
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
|
|
|
|
|
+ 人员数
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
|
|
+ 状态
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
|
|
+ 创建时间
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
|
|
+ 操作
|
|
|
|
|
+ </th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody className="divide-y divide-gray-100">
|
|
|
|
|
+ {displayedPositions.map((position, index) => (
|
|
|
|
|
+ <tr
|
|
|
|
|
+ key={position.id}
|
|
|
|
|
+ className="hover:bg-blue-50/30 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
|
|
+ {index + 1}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{position.name}</td>
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{position.code}</td>
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{position.department}</td>
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{position.description}</td>
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{position.userCount}</td>
|
|
|
|
|
+ <td className="px-6 py-4">
|
|
|
|
|
+ <span className={`inline-flex px-3 py-1 rounded-lg text-xs ${
|
|
|
|
|
+ position.status === '启用' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {position.status}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{position.createTime}</td>
|
|
|
|
|
+ <td className="px-6 py-4">
|
|
|
|
|
+ <div className="flex items-center justify-center gap-2">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => handleEdit(position)}
|
|
|
|
|
+ className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
|
|
|
|
+ title="编辑"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Edit2 className="w-4 h-4" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => handleDelete(position.id)}
|
|
|
|
|
+ className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
|
|
|
|
+ title="删除"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
|
|
+ title="更多"
|
|
|
|
|
+ >
|
|
|
|
|
+ <MoreVertical className="w-4 h-4" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 分页 */}
|
|
|
|
|
+ <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="text-sm text-gray-600">
|
|
|
|
|
+ 共 <span className="text-blue-600">{displayedPositions.length}</span> 个岗位
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex gap-2">
|
|
|
|
|
+ <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
|
|
+ 上一页
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button className="px-4 py-2 text-sm text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors">
|
|
|
|
|
+ 1
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
|
|
+ 下一页
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 新增/编辑弹窗 */}
|
|
|
|
|
+ {showAddModal && (
|
|
|
|
|
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
|
|
|
|
|
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 animate-in zoom-in duration-200">
|
|
|
|
|
+ {/* 弹窗标题 */}
|
|
|
|
|
+ <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
|
|
|
+ <h3 className="text-lg text-gray-900">
|
|
|
|
|
+ {editingItem ? '编辑岗位' : '新增岗位'}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setShowAddModal(false)}
|
|
|
|
|
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 弹窗内容 */}
|
|
|
|
|
+ <div className="px-6 py-6">
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">岗位名称 *</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
|
|
+ placeholder="请输入岗位名称"
|
|
|
|
|
+ defaultValue={editingItem?.name || ''}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">岗位编码 *</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
|
|
+ placeholder="请输入岗位编码"
|
|
|
|
|
+ defaultValue={editingItem?.code || ''}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">所属部门 *</label>
|
|
|
|
|
+ <select
|
|
|
|
|
+ className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
|
|
+ defaultValue={editingItem?.department || ''}
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="">请选择部门</option>
|
|
|
|
|
+ {departments.map(dept => (
|
|
|
|
|
+ <option key={dept} value={dept}>{dept}</option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">状态</label>
|
|
|
|
|
+ <select
|
|
|
|
|
+ className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
|
|
+ defaultValue={editingItem?.status || '启用'}
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="启用">启用</option>
|
|
|
|
|
+ <option value="禁用">禁用</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="col-span-2">
|
|
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">岗位描述</label>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ rows={3}
|
|
|
|
|
+ className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm resize-none"
|
|
|
|
|
+ placeholder="请输入岗位描述"
|
|
|
|
|
+ defaultValue={editingItem?.description || ''}
|
|
|
|
|
+ ></textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 弹窗底部 */}
|
|
|
|
|
+ <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setShowAddModal(false)}
|
|
|
|
|
+ className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ console.log('保存岗位:', editingItem);
|
|
|
|
|
+ setShowAddModal(false);
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
|
|
|
|
|
+ >
|
|
|
|
|
+ 确定
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|