lin
This commit is contained in:
parent
e98f1dcfa0
commit
66fc63c682
|
@ -1,9 +1,9 @@
|
|||
import React from "react"
|
||||
|
||||
export default function HomePage() {
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div >
|
||||
首页
|
||||
数据看板(待开发)
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,43 +1,30 @@
|
|||
import { Layout, Menu, Avatar, Button } from 'antd';
|
||||
import { UserOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { Layout, Avatar, Dropdown, Menu } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import NavigationMenu from './NavigationMenu';
|
||||
import { Header } from 'antd/es/layout/layout';
|
||||
import { useState } from 'react';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
export default function MainHeader() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
{/* 顶部Header */}
|
||||
<Header className="flex justify-end items-center bg-white shadow-sm h-16 px-6">
|
||||
<Avatar className='bg-black'></Avatar>
|
||||
</Header>
|
||||
|
||||
{/* 主体布局 */}
|
||||
<Layout className="h-screen">
|
||||
{/* 左侧导航栏 */}
|
||||
<Sider
|
||||
width={280}
|
||||
className=" flex flex-col"
|
||||
>
|
||||
{/* 用户头像区域 */}
|
||||
<div className="p-6 border-b">
|
||||
<div className="relative group flex justify-center">
|
||||
<Avatar
|
||||
size={120}
|
||||
icon={<UserOutlined />}
|
||||
className=" transition-all duration-300"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
className="!absolute bottom-0 right-6 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
onClick={() => navigate('/profile')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NavigationMenu></NavigationMenu>
|
||||
<Sider theme="light" width={240} className="shadow-sm">
|
||||
<NavigationMenu />
|
||||
</Sider>
|
||||
{/* 新增可滚动内容区域 */}
|
||||
<Layout className="flex-1">
|
||||
<Content className="overflow-auto">
|
||||
|
||||
{/* 内容区域 */}
|
||||
<Content className="overflow-auto p-6 bg-gray-50">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
|
|
|
@ -1,40 +1,325 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Menu } from "antd";
|
||||
// import { useSelector } from "react-redux";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
// import styles from "./index.module.less";
|
||||
// import logo from "../../assets/logo.png";
|
||||
|
||||
export default function NavigationMenu() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
console.log(location.pathname);
|
||||
// 导航菜单项配置
|
||||
const menuItems = [
|
||||
{ key: 'staff', label: '人员总览', path: '/' }, // 将path改为根路径
|
||||
{ key: 'plan', label: '培训计划', path: '/plan' },
|
||||
{ key: 'day', label: '每日填报', path: '/daily' },
|
||||
{ key: 'exam', label: '考核成绩', path: '/exam' },
|
||||
function getItem(
|
||||
label: any,
|
||||
key: any,
|
||||
icon: any,
|
||||
children: any,
|
||||
type: any,
|
||||
// permission: any
|
||||
) {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
// permission,
|
||||
};
|
||||
}
|
||||
const items = [
|
||||
getItem(
|
||||
"首页概览",
|
||||
"/",
|
||||
<i className={`iconfont icon-icon-home`} />,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
getItem(
|
||||
"人员信息",
|
||||
"/staffinformation",
|
||||
<i className="iconfont icon-icon-category" />,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
getItem(
|
||||
"人员总览",
|
||||
"/staff",
|
||||
<i className="iconfont icon-icon-category" />,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
getItem(
|
||||
"训练计划",
|
||||
"/plan",
|
||||
<i className="iconfont icon-icon-user" />,
|
||||
[
|
||||
getItem("周训练计划", "/plan/weekplan", null, null, null),
|
||||
getItem("月训练计划", "/plan/monthplan", null, null, null),
|
||||
],
|
||||
null,
|
||||
),
|
||||
getItem(
|
||||
"每日填报",
|
||||
"/daily",
|
||||
<i className="iconfont icon-icon-user" />,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
getItem(
|
||||
"考核成绩",
|
||||
"/assessment",
|
||||
<i className="iconfont icon-icon-user" />,
|
||||
[
|
||||
getItem("岗位", "/assessment/positionassessment", null, null, null),
|
||||
getItem("共同", "/assessment/commonassessment", null, null, null),
|
||||
getItem("体育", "/assessment/sportsassessment", null, null, null),
|
||||
],
|
||||
null,
|
||||
)
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
className="!bg-transparent !border-0 pt-4 [&_.ant-menu-item]:!mt-2"
|
||||
selectedKeys={[ // 改用selectedKeys替代defaultSelectedKeys
|
||||
menuItems.find((item) => location.pathname.startsWith(item.path))?.key,
|
||||
]}
|
||||
>
|
||||
{menuItems.map((item) => (
|
||||
<Menu.Item
|
||||
key={item.key}
|
||||
className="!h-14 !flex !items-center !text-gray-300 hover:!text-white group !rounded-lg mx-4"
|
||||
onClick={() => navigate(item.path)}
|
||||
>
|
||||
<div className="flex items-center justify-center w-full px-4 transition-all duration-300 h-full rounded-lg">
|
||||
<span className="text-xl font-medium">{item.label}</span>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
const NavigationMenu: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const children2Parent: any = {
|
||||
"^/plan/weekplan": ["/plan"],
|
||||
"^/plan/monthplan": ["/plan"],
|
||||
// 添加考核成绩子路径的匹配规则
|
||||
"^/assessment/positionassessment": ["/assessment"],
|
||||
"^/assessment/commonassessment": ["/assessment"],
|
||||
"^/assessment/sportsassessment": ["/assessment"]
|
||||
};
|
||||
|
||||
// 选中的菜单
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([
|
||||
location.pathname,
|
||||
]);
|
||||
// 展开菜单
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
// const permissions = useSelector(
|
||||
// (state: any) => state.loginUser.value.permissions
|
||||
// );
|
||||
const [activeMenus, setActiveMenus] = useState<any>(items);
|
||||
|
||||
const onClick = (e: any) => {
|
||||
const path = e.key;
|
||||
// 立即更新选中状态
|
||||
setSelectedKeys([path]);
|
||||
|
||||
// 根据不同路径设置展开状态
|
||||
if (path === "/staffinformation") {
|
||||
setOpenKeys(["/staffinformation"]);
|
||||
} else if (path === "/staff") {
|
||||
setOpenKeys(["/staff"]);
|
||||
} else if (path.startsWith("/assessment/") || path.startsWith("/plan/")) {
|
||||
setOpenKeys([path.split('/').slice(0, 2).join('/')]);
|
||||
} else {
|
||||
setOpenKeys(openKeyMerge(path));
|
||||
}
|
||||
|
||||
// 执行导航
|
||||
navigate(path);
|
||||
};
|
||||
// console.log(items)
|
||||
useEffect(() => {
|
||||
setActiveMenus(items);
|
||||
// console.log(activeMenus)
|
||||
},[items])
|
||||
// useEffect(() => {
|
||||
// checkMenuPermissions(items, permissions);
|
||||
// }, [items, permissions]);
|
||||
|
||||
// const checkMenuPermissions = (items: any, permissions: any) => {
|
||||
// let menus: any = [];
|
||||
// if (permissions.length === 0) {
|
||||
// setActiveMenus(menus);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// for (let i in items) {
|
||||
// let menuItem = items[i];
|
||||
// // 一级菜单=>没有子菜单&配置了权限
|
||||
// if (menuItem.children === null) {
|
||||
// if (
|
||||
// menuItem.permission !== null &&
|
||||
// typeof permissions[menuItem.permission] === "undefined"
|
||||
// ) {
|
||||
// continue;
|
||||
// }
|
||||
// menus.push(menuItem);
|
||||
// continue;
|
||||
// }
|
||||
// let children = [];
|
||||
|
||||
// for (let j in menuItem.children) {
|
||||
// let childrenItem = menuItem.children[j];
|
||||
|
||||
// if (
|
||||
// typeof permissions[childrenItem.permission] !== "undefined" ||
|
||||
// !childrenItem.permission
|
||||
// ) {
|
||||
// // 存在权限
|
||||
// children.push(childrenItem);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (children.length > 0) {
|
||||
// menus.push(Object.assign({}, menuItem, { children: children }));
|
||||
// }
|
||||
// }
|
||||
// setActiveMenus(menus);
|
||||
// };
|
||||
|
||||
const hit = (pathname: string): string[] => {
|
||||
// 使用精确的路径匹配
|
||||
const exactPaths = {
|
||||
"/staffinformation": ["/staffinformation"],
|
||||
"/staff": ["/staff"],
|
||||
"/": ["/"]
|
||||
};
|
||||
|
||||
// 先检查精确匹配
|
||||
if (exactPaths[pathname]) {
|
||||
return exactPaths[pathname];
|
||||
}
|
||||
|
||||
// 再检查正则匹配的子路径
|
||||
for (let p in children2Parent) {
|
||||
const regex = new RegExp(p);
|
||||
if (regex.test(pathname)) {
|
||||
return children2Parent[p];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const openKeyMerge = (pathname: string): string[] => {
|
||||
let newOpenKeys = hit(pathname);
|
||||
for (let i = 0; i < openKeys.length; i++) {
|
||||
let isIn = false;
|
||||
for (let j = 0; j < newOpenKeys.length; j++) {
|
||||
if (newOpenKeys[j] === openKeys[i]) {
|
||||
isIn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isIn) {
|
||||
continue;
|
||||
}
|
||||
newOpenKeys.push(openKeys[i]);
|
||||
}
|
||||
return newOpenKeys;
|
||||
};
|
||||
|
||||
// 修改 useEffect 中的路径判断逻辑
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
setSelectedKeys([currentPath]);
|
||||
|
||||
// 对于根路径特殊处理
|
||||
if (currentPath === "/") {
|
||||
setOpenKeys([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用修改后的 hit 函数获取正确的 openKeys
|
||||
const newOpenKeys = openKeyMerge(currentPath);
|
||||
setOpenKeys(newOpenKeys);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className='w-[200px] h-full bg-#fff'>
|
||||
<div
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
background: "#fff",
|
||||
}}
|
||||
onClick={() => {
|
||||
window.location.href = "/";
|
||||
}}
|
||||
>
|
||||
{/* 此处为版权标识,严禁删改
|
||||
<img src={logo} className="w-[124px] h-[40px]"/> */}
|
||||
</div>
|
||||
<div className='w-[200px] h-[calc(100%-74px)] overflow-y-auto overflow-x-hidden'>
|
||||
<Menu
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: 200,
|
||||
background: "#ffffff",
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
openKeys={openKeys}
|
||||
mode="inline"
|
||||
items={activeMenus}
|
||||
onSelect={(data: any) => {
|
||||
setSelectedKeys(data.selectedKeys);
|
||||
}}
|
||||
onOpenChange={(keys: any) => {
|
||||
setOpenKeys(keys);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationMenu;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// import { Menu } from "antd";
|
||||
// import { useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
// export default function NavigationMenu() {
|
||||
// const navigate = useNavigate();
|
||||
// const location = useLocation();
|
||||
// console.log(location.pathname);
|
||||
// // 导航菜单项配置
|
||||
// const menuItems = [
|
||||
// { key: 'staff', label: '人员总览', path: '/' }, // 将path改为根路径
|
||||
// { key: 'plan', label: '培训计划', path: '/plan' },
|
||||
// { key: 'day', label: '每日填报', path: '/daily' },
|
||||
// { key: 'exam', label: '考核成绩', path: '/exam' },
|
||||
// ];
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <Menu
|
||||
// theme="dark"
|
||||
// mode="inline"
|
||||
// className="!bg-transparent !border-0 pt-4 [&_.ant-menu-item]:!mt-2"
|
||||
// selectedKeys={[ // 改用selectedKeys替代defaultSelectedKeys
|
||||
// menuItems.find((item) => location.pathname.startsWith(item.path))?.key,
|
||||
// ]}
|
||||
// >
|
||||
// {menuItems.map((item) => (
|
||||
// <Menu.Item
|
||||
// key={item.key}
|
||||
// className="!h-14 !flex !items-center !text-gray-300 hover:!text-white group !rounded-lg mx-4"
|
||||
// onClick={() => navigate(item.path)}
|
||||
// >
|
||||
// <div className="flex items-center justify-center w-full px-4 transition-all duration-300 h-full rounded-lg">
|
||||
// <span className="text-xl font-medium">{item.label}</span>
|
||||
// </div>
|
||||
// </Menu.Item>
|
||||
// ))}
|
||||
// </Menu>
|
||||
// </>
|
||||
// );
|
||||
// }
|
|
@ -0,0 +1,227 @@
|
|||
import { useState } from 'react'
|
||||
import * as XLSX from 'xlsx'
|
||||
import { Table, Select, Pagination } from 'antd'
|
||||
import type { ColumnsType, ColumnType } from 'antd/es/table'
|
||||
import { UploadOutlined } from '@ant-design/icons'
|
||||
|
||||
interface TableData {
|
||||
key: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default function WeekPlanPage() {
|
||||
const [data, setData] = useState<TableData[]>([])
|
||||
const [columns, setColumns] = useState<ColumnsType<TableData>>([])
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [trainingStatus, setTrainingStatus] = useState<Record<string, string>>({})
|
||||
const pageSize = 1 // 每页显示一个第一列的唯一值
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const data = new Uint8Array(e.target?.result as ArrayBuffer)
|
||||
const workbook = XLSX.read(data, { type: 'array' })
|
||||
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||
|
||||
// 处理合并单元格
|
||||
if (firstSheet['!merges']) {
|
||||
firstSheet['!merges'].forEach(merge => {
|
||||
// 获取合并区域的起始单元格的值
|
||||
const firstCell = XLSX.utils.encode_cell({ r: merge.s.r, c: merge.s.c })
|
||||
const firstCellValue = firstSheet[firstCell]?.v
|
||||
|
||||
// 将合并区域内的所有单元格设置为相同的值
|
||||
for (let row = merge.s.r; row <= merge.e.r; row++) {
|
||||
for (let col = merge.s.c; col <= merge.e.c; col++) {
|
||||
const cell = XLSX.utils.encode_cell({ r: row, c: col })
|
||||
if (!firstSheet[cell]) {
|
||||
firstSheet[cell] = { t: 's', v: firstCellValue }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const jsonData: any[] = XLSX.utils.sheet_to_json(firstSheet, {
|
||||
header: 1,
|
||||
defval: '',
|
||||
blankrows: false,
|
||||
raw: false
|
||||
})
|
||||
|
||||
// 处理表头和数据
|
||||
const headers = jsonData[0]
|
||||
const tableData: TableData[] = jsonData.slice(1).map((row, index) => ({
|
||||
key: index.toString(),
|
||||
...headers.reduce((acc: any, header: string, idx: number) => {
|
||||
acc[header] = row[idx]
|
||||
return acc
|
||||
}, {})
|
||||
}))
|
||||
|
||||
// 创建列配置
|
||||
const tableColumns: ColumnsType<TableData> = headers.map((header: string, index: number) => ({
|
||||
title: header,
|
||||
dataIndex: header,
|
||||
key: header,
|
||||
width: 150,
|
||||
align: 'center',
|
||||
filterMultiple: true,
|
||||
filters: Array.from(new Set(tableData.map(item => item[header])))
|
||||
.filter(Boolean)
|
||||
.map(value => ({ text: String(value), value: String(value) })),
|
||||
onFilter: (value, record) => String(record[header]) === value,
|
||||
...(index < headers.length - 1 && {
|
||||
render: (text, record, index) => ({
|
||||
children: text,
|
||||
props: {
|
||||
rowSpan: calculateRowSpan(tableData, index, header),
|
||||
style: {
|
||||
border: '1px solid #f0f0f0',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
// 添加是否参训列
|
||||
tableColumns.push({
|
||||
title: '是否参训',
|
||||
dataIndex: 'isTraining',
|
||||
key: 'isTraining',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Select
|
||||
value={trainingStatus[record.key] || undefined}
|
||||
onChange={(value) => {
|
||||
setTrainingStatus(prev => ({
|
||||
...prev,
|
||||
[record.key]: value
|
||||
}))
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: '参训', label: '参训' },
|
||||
{ value: '不参训', label: '不参训' }
|
||||
]}
|
||||
placeholder="请选择"
|
||||
/>
|
||||
)
|
||||
})
|
||||
setColumns(tableColumns)
|
||||
setData(tableData)
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
|
||||
// 计算行合并
|
||||
const calculateRowSpan = (data: TableData[], rowIndex: number, column: string) => {
|
||||
if (rowIndex === 0) {
|
||||
let count = 1
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i][column] === data[rowIndex][column]) {
|
||||
count++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
if (rowIndex > 0 && data[rowIndex][column] === data[rowIndex - 1][column]) {
|
||||
return 0
|
||||
}
|
||||
let count = 1
|
||||
for (let i = rowIndex + 1; i < data.length; i++) {
|
||||
if (data[i][column] === data[rowIndex][column]) {
|
||||
count++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// 修改分页数据获取逻辑
|
||||
const getPageData = () => {
|
||||
const firstColumn = columns[0] as ColumnType<TableData>
|
||||
const firstColumnName = firstColumn?.dataIndex as string
|
||||
if (!firstColumnName) return []
|
||||
|
||||
// 获取第一列的所有唯一值
|
||||
const uniqueValues = Array.from(
|
||||
new Set(data.map(item => item[firstColumnName]))
|
||||
).filter(Boolean)
|
||||
|
||||
// 获取当前页应该显示的值
|
||||
const currentValue = uniqueValues[(currentPage - 1)]
|
||||
|
||||
// 返回第一列等于当前值的所有行
|
||||
return data.filter(item => item[firstColumnName] === currentValue)
|
||||
}
|
||||
console.log(data)
|
||||
|
||||
const handleSave = () => {
|
||||
const saveData = data.map(item => ({
|
||||
...item,
|
||||
isTraining: trainingStatus[item.key] || ''
|
||||
}))
|
||||
console.log('要保存的数据:', saveData)
|
||||
// 这里添加保存到后端的逻辑
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="w-full mb-6 p-8 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleFileUpload}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center text-gray-600">
|
||||
<UploadOutlined className="text-3xl mb-2" />
|
||||
<p className="text-lg font-medium">点击或拖拽文件到此处上传</p>
|
||||
<p className="text-sm text-gray-500">支持 .xlsx, .xls 格式的Excel文件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.length > 0 && (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getPageData()}
|
||||
pagination={false}
|
||||
bordered
|
||||
style={{
|
||||
border: '1px solid #f0f0f0'
|
||||
}}
|
||||
className="!border-collapse [&_th]:!border [&_td]:!border [&_th]:!border-solid [&_td]:!border-solid [&_th]:!border-[#f0f0f0] [&_td]:!border-[#f0f0f0]"
|
||||
/>
|
||||
<div className="flex justify-end mt-4">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
<Pagination
|
||||
className="mt-4"
|
||||
current={currentPage}
|
||||
total={Array.from(new Set(data.map(item =>
|
||||
item[(columns[0] as ColumnType<TableData>)?.dataIndex as string]
|
||||
))).filter(Boolean).length}
|
||||
pageSize={1}
|
||||
onChange={(page) => {
|
||||
setCurrentPage(page)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import areaData from 'china-area-data/data';
|
||||
// 修改导入语句
|
||||
import type { CascaderProps, DefaultOptionType } from 'antd/es/cascader';
|
||||
|
||||
// 修改转换函数类型
|
||||
function transformAreaData(): CascaderProps['options'] {
|
||||
const provinces = areaData['86'] as Record<string, string>;
|
||||
|
||||
return Object.entries(provinces).map(([provinceCode, provinceName]): DefaultOptionType => {
|
||||
const cities = areaData[provinceCode] as Record<string, string>;
|
||||
|
||||
return {
|
||||
value: provinceCode,
|
||||
label: provinceName as string,
|
||||
children: Object.entries(cities).map(([cityCode, cityName]): DefaultOptionType => {
|
||||
const districts = areaData[cityCode] as Record<string, string>;
|
||||
|
||||
return {
|
||||
value: cityCode,
|
||||
label: cityName as string,
|
||||
children: districts
|
||||
? Object.entries(districts).map(([districtCode, districtName]): DefaultOptionType => ({
|
||||
value: districtCode,
|
||||
label: districtName as string,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const areaOptions: DefaultOptionType[] = transformAreaData();
|
|
@ -0,0 +1,517 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader } from "antd";
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { useStaff } from "@nice/client";
|
||||
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
||||
import { areaOptions } from './area-options';
|
||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||
const { TextArea } = Input;
|
||||
|
||||
const StaffInformation = () => {
|
||||
const [modalForm] = Form.useForm();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modalType, setModalType] = useState<'awards' | 'punishments' | 'equipment' | 'projects'>('awards');
|
||||
const [rewardsList, setRewardsList] = useState<string[]>([]);
|
||||
const [punishmentsList, setPunishmentsList] = useState<string[]>([]);
|
||||
const [equipmentList, setEquipmentList] = useState<string[]>([]); // 新增装备列表
|
||||
const [projectsList, setProjectsList] = useState<string[]>([]); // 新增任务列表
|
||||
const {create, update} = useStaff();
|
||||
|
||||
const showModal = (type: 'awards' | 'punishments' | 'equipment' | 'projects') => {
|
||||
setModalType(type);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await modalForm.validateFields();
|
||||
const value = values[modalType];
|
||||
|
||||
if (value) {
|
||||
switch(modalType) {
|
||||
case 'awards':
|
||||
setRewardsList([...rewardsList, value]);
|
||||
break;
|
||||
case 'punishments':
|
||||
setPunishmentsList([...punishmentsList, value]);
|
||||
break;
|
||||
case 'equipment':
|
||||
setEquipmentList([...equipmentList, value]);
|
||||
break;
|
||||
case 'projects':
|
||||
setProjectsList([...projectsList, value]);
|
||||
break;
|
||||
}
|
||||
modalForm.resetFields();
|
||||
setIsModalVisible(false);
|
||||
message.success(
|
||||
modalType === 'awards' ? '奖励信息添加成功' :
|
||||
modalType === 'punishments' ? '处分信息添加成功' :
|
||||
modalType === 'equipment' ? '装备信息添加成功' : '任务信息添加成功'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
message.warning('请输入内容');
|
||||
}
|
||||
};
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
console.log('开始提交表单');
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const formattedValues = {
|
||||
...values,
|
||||
birthplace: values.birthplace?.join('/'), // 将数组转换为以'/'分隔的字符串
|
||||
awards: rewardsList.join(','),
|
||||
punishments: punishmentsList.join(','),
|
||||
equipment: equipmentList.join(','),
|
||||
projects: projectsList.join(','),
|
||||
hireDate: values.hireDate?.toISOString(),
|
||||
seniority: values.seniority?.toISOString(),
|
||||
currentPositionDate: values.currentPositionDate?.toISOString(),
|
||||
rankDate: values.rankDate?.toISOString(), // 修改这里
|
||||
};
|
||||
|
||||
await create.mutateAsync(
|
||||
{
|
||||
data: formattedValues
|
||||
}
|
||||
);
|
||||
|
||||
console.log('提交的表单数据:', formattedValues);
|
||||
console.log('奖励列表:', rewardsList);
|
||||
console.log('处分列表:', punishmentsList);
|
||||
|
||||
message.success("信息提交成功");
|
||||
} catch (error) {
|
||||
console.error('提交出错:', error);
|
||||
message.error("提交失败,请重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.MouseEvent) => {
|
||||
e.preventDefault(); // 阻止默认行为
|
||||
console.log('提交按钮被点击');
|
||||
form.submit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">人员信息填报</h1>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={(errorInfo) => {
|
||||
console.log('表单验证失败:', errorInfo);
|
||||
}}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* 个人基本信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">个人基本信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="姓名" name="username" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="身份证号" name="idNumber" rules={[{required: true}]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="警号" name="officerId">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="phoneNumber">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="人员类型" name="type">
|
||||
<Select>
|
||||
<Select.Option value="警官">警官</Select.Option>
|
||||
<Select.Option value="文职">文职</Select.Option>
|
||||
<Select.Option value="警士">警士</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Radio.Group>
|
||||
<Radio value={true}>男</Radio>
|
||||
<Radio value={false}>女</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label='年龄' name='age'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="血型" name="bloodType">
|
||||
<Select>
|
||||
<Select.Option value="A">A型</Select.Option>
|
||||
<Select.Option value="B">B型</Select.Option>
|
||||
<Select.Option value="O">O型</Select.Option>
|
||||
<Select.Option value="AB">AB型</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="籍贯"
|
||||
name="birthplace"
|
||||
rules={[{ required: true, message: '请选择籍贯' }]}
|
||||
>
|
||||
<Cascader
|
||||
options={areaOptions}
|
||||
placeholder="请选择省/市/区"
|
||||
className="w-full"
|
||||
expandTrigger="hover"
|
||||
showSearch={{
|
||||
filter: (inputValue, path) => {
|
||||
return path.some(option =>
|
||||
typeof option.label === 'string' &&
|
||||
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
|
||||
);
|
||||
}
|
||||
}}
|
||||
changeOnSelect
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 政治信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">政治信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="政治面貌" name="politicalStatus">
|
||||
<Select>
|
||||
<Select.Option value="中共党员">中共党员</Select.Option>
|
||||
<Select.Option value="中共预备党员">中共预备党员</Select.Option>
|
||||
<Select.Option value="共青团员">共青团员</Select.Option>
|
||||
<Select.Option value="群众">群众</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="党内职务" name="partyPosition">
|
||||
<Select>
|
||||
<Select.Option value="党委书记">党委书记</Select.Option>
|
||||
<Select.Option value="委员">委员</Select.Option>
|
||||
<Select.Option value="党支部书记">党支部书记</Select.Option>
|
||||
<Select.Option value="党支部委员">党支部委员</Select.Option>
|
||||
<Select.Option value="无">无</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 职务信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">职务信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label = "部门" name="deptId" >
|
||||
<DepartmentSelect/>
|
||||
</Form.Item>
|
||||
<Form.Item label="衔职级别" name="rank">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="衔职时间" name="rankDate">
|
||||
<DatePicker className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item label="代理职务" name="proxyPosition">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 入职信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">入职信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="入职时间" name="hireDate">
|
||||
<DatePicker className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item label="工龄认定时间" name="seniority">
|
||||
<DatePicker className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item label="是否二次入职" name="isReentry" initialValue={false}>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="来源类别" name="source">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="现岗位开始时间" name="currentPositionDate">
|
||||
<DatePicker className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item label="是否延期服役" name="isExtended" initialValue={false}>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 教育背景 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">教育背景</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="学历" name="education">
|
||||
<Select>
|
||||
<Select.Option value="高中">高中</Select.Option>
|
||||
<Select.Option value="专科">专科</Select.Option>
|
||||
<Select.Option value="本科">本科</Select.Option>
|
||||
<Select.Option value="硕士">硕士</Select.Option>
|
||||
<Select.Option value="博士">博士</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="学历形式" name="educationType">
|
||||
<Select>
|
||||
<Select.Option value="全日制">全日制</Select.Option>
|
||||
<Select.Option value="非全日制">非全日制</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否毕业" name="isGraduated">
|
||||
<Radio.Group>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="专业" name="major">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="外语能力" name="foreignLang">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 培训信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">培训信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="是否参加培训" name="hasTrain" initialValue={false}>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.hasTrain !== currentValues.hasTrain}
|
||||
>
|
||||
{({ getFieldValue }) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
|
||||
<Form.Item label="培训类型" name="trainType">
|
||||
<Input disabled={!getFieldValue('hasTrain')} />
|
||||
</Form.Item>
|
||||
<Form.Item label="培训机构" name="trainInstitute">
|
||||
<Input disabled={!getFieldValue('hasTrain')} />
|
||||
</Form.Item>
|
||||
<Form.Item label="培训专业" name="trainMajor">
|
||||
<Input disabled={!getFieldValue('hasTrain')} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 鉴定信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">鉴定信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="是否参加鉴定" name="hasCert" initialValue={false}>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.hasCert !== currentValues.hasCert}
|
||||
>
|
||||
{({ getFieldValue }) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
|
||||
<Form.Item label="鉴定等级" name="certRank">
|
||||
<Input disabled={!getFieldValue('hasCert')} />
|
||||
</Form.Item>
|
||||
<Form.Item label="鉴定工种" name="certWork">
|
||||
<Input disabled={!getFieldValue('hasCert')} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工作信息 */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">工作信息</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label>操作维护装备</label>
|
||||
<Button type="primary" onClick={() => showModal('equipment')}>
|
||||
添加装备
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border p-2 min-h-[100px] rounded">
|
||||
{equipmentList.map((equipment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
||||
>
|
||||
<span>{equipment}</span>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => {
|
||||
setEquipmentList(equipmentList.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label>任务经历</label>
|
||||
<Button type="primary" onClick={() => showModal('projects')}>
|
||||
添加任务
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border p-2 min-h-[100px] rounded">
|
||||
{projectsList.map((mission, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
||||
>
|
||||
<span>{mission}</span>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => {
|
||||
setProjectsList(projectsList.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label>奖励信息</label>
|
||||
<Button type="primary" onClick={() => showModal('awards')}>
|
||||
添加奖励
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border p-2 min-h-[100px] rounded">
|
||||
{rewardsList.map((reward, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
||||
>
|
||||
<span>{reward}</span>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => {
|
||||
setRewardsList(rewardsList.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label>处分信息</label>
|
||||
<Button type="primary" onClick={() => showModal('punishments')}>
|
||||
添加处分
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border p-2 min-h-[100px] rounded">
|
||||
{punishmentsList.map((punishment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
||||
>
|
||||
<span>{punishment}</span>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => {
|
||||
setPunishmentsList(punishmentsList.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button onClick={() => form.resetFields()}>重置</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
modalType === 'awards' ? '添加奖励信息' :
|
||||
modalType === 'punishments' ? '添加处分信息' :
|
||||
modalType === 'equipment' ? '添加装备信息' : '添加任务信息'
|
||||
}
|
||||
open={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => {
|
||||
setIsModalVisible(false);
|
||||
modalForm.resetFields();
|
||||
}}
|
||||
>
|
||||
<Form form={modalForm}>
|
||||
<Form.Item
|
||||
label={
|
||||
modalType === 'awards' ? '奖励内容' :
|
||||
modalType === 'punishments' ? '处分内容' :
|
||||
modalType === 'equipment' ? '装备信息' : '任务信息'
|
||||
}
|
||||
name={modalType}
|
||||
rules={[{ required: true, message: '请输入内容' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder={
|
||||
modalType === 'awards' ? '请输入获得的表彰奖励信息' :
|
||||
modalType === 'punishments' ? '请输入处分信息' :
|
||||
modalType === 'equipment' ? '请输入装备信息' : '请输入任务经历详情'
|
||||
}
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StaffInformation;
|
|
@ -1,66 +1,41 @@
|
|||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { api, useStaff } from "@nice/client";
|
||||
import { Button, Form, Input, Modal, Select, Table } from "antd";
|
||||
import { StaffDto, trainSituationDetailSelect } from "@nice/common";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Input } from "antd";
|
||||
import { useCallback, useEffect} from "react";
|
||||
import _ from "lodash";
|
||||
import { useMainContext } from "../layout/MainProvider";
|
||||
import StaffTable from "./stafftable/page";
|
||||
import StaffModal from "./staffmodal/page";
|
||||
|
||||
export default function StaffMessage() {
|
||||
const {form, formValue, setFormValue, setVisible, setSearchValue} = useMainContext();
|
||||
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFormValue(
|
||||
{
|
||||
setFormValue({
|
||||
username: "",
|
||||
deptId: "",
|
||||
absent: false,
|
||||
position: "",
|
||||
positionId: "",
|
||||
trainSituations: "",
|
||||
}
|
||||
)
|
||||
},[])
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleNew = () => {
|
||||
form.setFieldsValue(formValue);
|
||||
setVisible(true);
|
||||
}
|
||||
// 添加防抖的搜索处理函数
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
_.debounce((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-2 min-h-screen bg-gradient-to-br">
|
||||
<Form>
|
||||
<div className="p-4 h-full flex flex-col"> {/* 修改为flex布局 */}
|
||||
<div className="max-w-full mx-auto flex-1 flex flex-col"> {/* 添加flex容器 */}
|
||||
{/* 头部区域保持不变... */}
|
||||
<div className="flex justify-between mb-4 space-x-4 items-center">
|
||||
<div className="text-2xl">人员总览</div>
|
||||
<div className="relative w-1/3">
|
||||
<Input
|
||||
placeholder="输入姓名搜索"
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10 w-full border"
|
||||
/>
|
||||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 " />
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNew}
|
||||
className=" font-bold py-2 px-4 rounded"
|
||||
>
|
||||
新建人员
|
||||
</Button>
|
||||
</div>
|
||||
<StaffTable></StaffTable>
|
||||
<StaffModal></StaffModal>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
<>
|
||||
<StaffTable />
|
||||
<StaffModal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import { api, useStaff } from "@nice/client";
|
|||
import { useMainContext } from "../../layout/MainProvider";
|
||||
import toast from "react-hot-toast";
|
||||
import { Button, Form, Input, Modal, Select } from "antd";
|
||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||
import { useEffect } from "react";
|
||||
import TrainContentTreeSelect from "@web/src/components/models/trainContent/train-content-tree-select";
|
||||
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
||||
|
||||
export default function StaffModal() {
|
||||
const { data: traincontents} = api.trainSituation.findMany.useQuery({
|
||||
|
||||
select:{
|
||||
id: true,
|
||||
trainContent:{
|
||||
|
@ -18,67 +18,55 @@ export default function StaffModal() {
|
|||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
traincontents?.forEach((situation)=>{
|
||||
console.log(situation.trainContent.title);
|
||||
});
|
||||
}, [traincontents]);
|
||||
|
||||
const { form, formValue, setVisible, visible, editingRecord } = useMainContext()
|
||||
});
|
||||
// useEffect(() => {
|
||||
// traincontents?.forEach((situation)=>{
|
||||
// console.log(situation.id);
|
||||
// });
|
||||
// }, [traincontents]);
|
||||
|
||||
const { form, formValue, setVisible, visible, editingRecord, setEditingRecord } = useMainContext()
|
||||
const { create, update } = useStaff();
|
||||
const handleOk = async () => {
|
||||
const values = await form.getFieldsValue();
|
||||
console.log(values.username);
|
||||
// console.log(values);
|
||||
// console.log(values.position);
|
||||
try {
|
||||
if (editingRecord && editingRecord.id) {
|
||||
const result = await update.mutateAsync(
|
||||
{
|
||||
where: {
|
||||
id: editingRecord.id,
|
||||
},
|
||||
await update.mutateAsync({
|
||||
where: { id: editingRecord.id },
|
||||
data: {
|
||||
username: values.username,
|
||||
deptId: values.deptId,
|
||||
position: values.position,
|
||||
absent: values.absent,
|
||||
trainSituations: values.trainSituations ? {
|
||||
upsert: values.trainSituations.map((situation) => ({
|
||||
where: { id: situation.id || "" },
|
||||
update: {
|
||||
mustTrainTime: situation.mustTrainTime,
|
||||
trainContent: { connect: { id: situation.trainContentId } },
|
||||
// 其他字段...
|
||||
department: {
|
||||
connect: { id: values.deptId }
|
||||
},
|
||||
}))
|
||||
} : undefined,
|
||||
rank: values.rank,
|
||||
type: values.type,
|
||||
showname: values.username,
|
||||
updatedAt: new Date()
|
||||
} as any
|
||||
}
|
||||
);
|
||||
} ,
|
||||
});
|
||||
// console.log(result);
|
||||
} else {
|
||||
await create.mutateAsync(
|
||||
const result = await create.mutateAsync(
|
||||
{
|
||||
data: {
|
||||
username: values.username,
|
||||
deptId: values.deptId,
|
||||
department: {
|
||||
connect: { id: values.deptId }
|
||||
},
|
||||
createdAt: new Date(),
|
||||
showname: values.username,
|
||||
absent: values.absent,
|
||||
trainSituations: {
|
||||
create: values.trainSituations.map((situation) => ({
|
||||
trainContent: { connect: { id: situation.trainContentId } },
|
||||
mustTrainTime: situation.mustTrainTime,
|
||||
// 其他必填字段...
|
||||
}))
|
||||
}
|
||||
}
|
||||
rank: values.rank,
|
||||
type: values.type,
|
||||
} as any,
|
||||
}
|
||||
) ;
|
||||
}
|
||||
toast.success("保存成功");
|
||||
setVisible(false);
|
||||
setEditingRecord(null);
|
||||
} catch (error) {
|
||||
toast.error("保存失败");
|
||||
throw error;
|
||||
|
@ -87,10 +75,17 @@ export default function StaffModal() {
|
|||
|
||||
const handleCancel = () => {
|
||||
setVisible(false);
|
||||
setEditingRecord(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible&&editingRecord) {
|
||||
form.setFieldsValue(editingRecord);
|
||||
}
|
||||
}, [visible,editingRecord]);
|
||||
return (
|
||||
<>
|
||||
{/* 模态框样式更新 */}
|
||||
<Modal
|
||||
title="编辑员工信息"
|
||||
visible={visible}
|
||||
|
@ -104,65 +99,31 @@ export default function StaffModal() {
|
|||
<Form.Item
|
||||
name={"username"}
|
||||
label="姓名"
|
||||
// labelClassName="text-gray-300"
|
||||
>
|
||||
<Input className="rounded-lg" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"deptId"}
|
||||
label="部门"
|
||||
// labelClassName="text-gray-300"
|
||||
>
|
||||
<DepartmentSelect></DepartmentSelect>
|
||||
<DepartmentChildrenSelect></DepartmentChildrenSelect>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"positionId"}
|
||||
label="职务"
|
||||
// labelClassName="text-gray-300"
|
||||
name={["type"]}
|
||||
label="人员类别"
|
||||
>
|
||||
<Input className="rounded-lg" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"absent"}
|
||||
label="在位"
|
||||
>
|
||||
<Select className="rounded-lg" >
|
||||
<Select.Option value={true}>否</Select.Option>
|
||||
<Select.Option value={false}>是</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name="trainSituations">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="flex space-x-2">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'trainContent']}
|
||||
label="培训内容"
|
||||
className="flex-1"
|
||||
name={["rank"]}
|
||||
label="衔职"
|
||||
>
|
||||
<TrainContentTreeSelect></TrainContentTreeSelect>
|
||||
<Input className="rounded-lg" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'mustTrainTime']}
|
||||
label="需训时长"
|
||||
className="flex-1"
|
||||
>
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Button onClick={() => remove(name)}>删除</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button onClick={() => add()}>添加培训</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Button onClick={() =>{console.log(traincontents);}}>TEST</Button>
|
||||
{/* <Button onClick={() =>{console.log(traincontents);}}>TEST</Button> */}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,158 +1,314 @@
|
|||
import { Button, Select, Table } from "antd"
|
||||
import { StaffDto } from "@nice/common";
|
||||
import { useStaff, api } from "@nice/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import React from "react";
|
||||
import { useMainContext } from "../../layout/MainProvider";
|
||||
'use client';
|
||||
import { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { ColDef, ColGroupDef } from 'ag-grid-community';
|
||||
import { api } from '@nice/client';
|
||||
import 'ag-grid-community/styles/ag-grid.css';
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import { areaOptions } from '@web/src/app/main/staffinformation/area-options';
|
||||
import type { CascaderProps } from 'antd/es/cascader';
|
||||
import { SetFilterModule } from '@ag-grid-enterprise/set-filter';
|
||||
import { zhCN } from 'ag-grid-community/dist/ag-grid-community';
|
||||
|
||||
// 修改函数类型定义
|
||||
|
||||
function getAreaName(codes: string[], level?: number): string {
|
||||
const result: string[] = [];
|
||||
let currentLevel: CascaderProps['options'] = areaOptions;
|
||||
|
||||
for (const code of codes) {
|
||||
const found = currentLevel?.find(opt => opt.value === code);
|
||||
if (!found) break;
|
||||
|
||||
result.push(String(found.label));
|
||||
currentLevel = found.children || [];
|
||||
if (level && result.length >= level) break; // 添加层级控制
|
||||
}
|
||||
|
||||
|
||||
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
|
||||
}
|
||||
// 自定义下拉过滤器组件
|
||||
// interface ICustomFilterProps {
|
||||
// column: {
|
||||
// getColId: () => string;
|
||||
// };
|
||||
// api: {
|
||||
// forEachNode: (callback: (node: any) => void) => void;
|
||||
// };
|
||||
// filterChangedCallback?: () => void; // 设置为可选属性
|
||||
// }
|
||||
|
||||
// const CustomDropdownFilter = forwardRef((props: ICustomFilterProps, ref) => {
|
||||
// const [selectedValue, setSelectedValue] = useState<string>('');
|
||||
// const [uniqueValues, setUniqueValues] = useState<string[]>([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const colId = props.column.getColId();
|
||||
// const values = new Set<string>();
|
||||
|
||||
// props.api.forEachNode(node => {
|
||||
// const value = node.data[colId];
|
||||
// if (value != null) {
|
||||
// values.add(String(value));
|
||||
// }
|
||||
// });
|
||||
|
||||
// setUniqueValues(Array.from(values).sort());
|
||||
// }, [props.api, props.column]);
|
||||
|
||||
// useImperativeHandle(ref, () => ({
|
||||
// isFilterActive: () => !!selectedValue,
|
||||
// doesFilterPass: (params: any) => {
|
||||
// const value = String(params.data[props.column.getColId()] || '');
|
||||
// // 精确匹配选中的值
|
||||
// return value === selectedValue;
|
||||
// },
|
||||
// getModel: () => {
|
||||
// return selectedValue ? { value: selectedValue } : null;
|
||||
// },
|
||||
// setModel: (model: any) => {
|
||||
// setSelectedValue(model?.value || '');
|
||||
// }
|
||||
// }));
|
||||
|
||||
// const handleChange = (value: string) => {
|
||||
// setSelectedValue(value);
|
||||
// // 立即触发过滤器更新
|
||||
// if (props.filterChangedCallback) {
|
||||
// props.filterChangedCallback();
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div style={{ padding: '4px', width: '100%' }}>
|
||||
// <Select
|
||||
// value={selectedValue}
|
||||
// onChange={handleChange}
|
||||
// style={{ width: '100%' }}
|
||||
// size="small"
|
||||
// placeholder="选择筛选..."
|
||||
// allowClear
|
||||
// options={uniqueValues.map(value => ({
|
||||
// value: value,
|
||||
// label: value
|
||||
// }))}
|
||||
// onClear={() => handleChange('')}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
export default function StaffTable() {
|
||||
const{form, setVisible,searchValue} = useMainContext()
|
||||
|
||||
const { data: staffs, isLoading } = api.staff.findMany.useQuery({
|
||||
where: {
|
||||
username: {
|
||||
contains: searchValue
|
||||
}
|
||||
}
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
// console.log(staffs.map((staff) => staff.absent));
|
||||
const { create, update } = useStaff();
|
||||
const {editingRecord, setEditingRecord} = useMainContext();
|
||||
const colnums = [
|
||||
|
||||
const columnDefs: (ColDef | ColGroupDef)[] = [
|
||||
{
|
||||
title: "姓名",
|
||||
dataIndex: "username",
|
||||
key: "username",
|
||||
field: 'username',
|
||||
headerName: '姓名',
|
||||
minWidth: 120,
|
||||
pinned: 'left',
|
||||
// floatingFilter: true // 确保启用浮动过滤器
|
||||
},
|
||||
{
|
||||
title: "部门",
|
||||
dataIndex: "deptId",
|
||||
key: "deptId",
|
||||
headerName: '个人基本信息',
|
||||
children: [
|
||||
{
|
||||
field: 'idNumber',
|
||||
headerName: '身份证号',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
title: "职务",
|
||||
dataIndex: "positionId",
|
||||
key: "positionId",
|
||||
field: 'type',
|
||||
headerName: '人员类型',
|
||||
minWidth: 120,
|
||||
},
|
||||
{ field: 'officerId', headerName: '警号', minWidth: 120 },
|
||||
{ field: 'phoneNumber', headerName: '手机号', minWidth: 130 },
|
||||
{ field: 'age', headerName: '年龄', minWidth: 80 },
|
||||
{ field: 'sex', headerName: '性别', minWidth: 80,
|
||||
cellRenderer: (params: any) => params.value ? '男' : '女' },
|
||||
{ field: 'bloodType', headerName: '血型', minWidth: 80 },
|
||||
{
|
||||
field: 'birthplace',
|
||||
headerName: '籍贯',
|
||||
minWidth: 200,
|
||||
valueFormatter: (params) => params.value ? getAreaName(params.value.split('/')) : '',
|
||||
},
|
||||
{ field: 'source', headerName: '来源', minWidth: 120 },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "在位",
|
||||
dataIndex: "absent",
|
||||
key: "absent",
|
||||
render: (_, record) => (
|
||||
<Select
|
||||
// defaultValue={record.absent ? "是" : "否"}
|
||||
style={{ width: 120 }}
|
||||
onChange={async (value) => {
|
||||
try {
|
||||
await update.mutateAsync({
|
||||
where: { id: record.id },
|
||||
data: { absent: value }
|
||||
});
|
||||
toast.success("状态更新成功");
|
||||
} catch (error) {
|
||||
toast.error("状态更新失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Option value="是">是</Select.Option>
|
||||
<Select.Option value="否">否</Select.Option>
|
||||
</Select>
|
||||
)
|
||||
headerName: '政治信息',
|
||||
children: [
|
||||
{ field: 'politicalStatus', headerName: '政治面貌', minWidth: 150 },
|
||||
{ field: 'partyPosition', headerName: '党内职务', minWidth: 120 }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "应时",
|
||||
dataIndex: "trainSituation",
|
||||
key: "trainSituation",
|
||||
headerName: '职务信息',
|
||||
children: [
|
||||
{field: 'deptId', headerName: '所属部门', minWidth: 200 },
|
||||
{ field: 'rank', headerName: '衔职级别', minWidth: 120 },
|
||||
{ field: 'rankDate', headerName: '衔职时间', minWidth: 120,
|
||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' },
|
||||
{ field: 'proxyPosition', headerName: '代理职务', minWidth: 120 }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="primary"
|
||||
key={record.id}
|
||||
onClick={() => handleEdit(record)}>编辑</Button>
|
||||
headerName: '入职信息',
|
||||
children: [
|
||||
{ field: 'hireDate', headerName: '入职时间', minWidth: 120,
|
||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' },
|
||||
{ field: 'seniority', headerName: '工龄认定时间', minWidth: 140,
|
||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' },
|
||||
{ field: 'sourceType', headerName: '来源类型', minWidth: 120 },
|
||||
{ field: 'isReentry', headerName: '是否二次入职', minWidth: 120,
|
||||
cellRenderer: (params: any) => params.value ? '是' : '否' },
|
||||
{ field: 'isExtended', headerName: '是否延期服役', minWidth: 120,
|
||||
cellRenderer: (params: any) => params.value ? '是' : '否' },
|
||||
{ field: 'currentPositionDate', headerName: '现岗位开始时间', minWidth: 140,
|
||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' }
|
||||
]
|
||||
},
|
||||
{
|
||||
headerName: '教育背景',
|
||||
children: [
|
||||
{ field: 'education', headerName: '学历', minWidth: 100 },
|
||||
{ field: 'educationType', headerName: '学历形式', minWidth: 120 },
|
||||
{ field: 'isGraduated', headerName: '是否毕业', minWidth: 100,
|
||||
cellRenderer: (params: any) => params.value ? '是' : '否' },
|
||||
{ field: 'major', headerName: '专业', minWidth: 150 },
|
||||
{ field: 'foreignLang', headerName: '外语能力', minWidth: 120 }
|
||||
]
|
||||
},
|
||||
{
|
||||
headerName: '培训信息',
|
||||
children: [
|
||||
{ field: 'trainType', headerName: '培训类型', minWidth: 120 },
|
||||
{ field: 'trainInstitute', headerName: '培训机构', minWidth: 150 },
|
||||
{ field: 'trainMajor', headerName: '培训专业', minWidth: 150 },
|
||||
{ field: 'hasTrain', headerName: '是否参加培训', minWidth: 120,
|
||||
cellRenderer: (params: any) => params.value ? '是' : '否' }
|
||||
]
|
||||
},
|
||||
{
|
||||
headerName: '鉴定信息',
|
||||
children: [
|
||||
{ field: 'certRank', headerName: '鉴定等级', minWidth: 120 },
|
||||
{ field: 'certWork', headerName: '鉴定工种', minWidth: 120 },
|
||||
{ field: 'hasCert', headerName: '是否参加鉴定', minWidth: 120,
|
||||
cellRenderer: (params: any) => params.value ? '是' : '否' }
|
||||
]
|
||||
},
|
||||
{
|
||||
headerName: '工作信息',
|
||||
children: [
|
||||
{
|
||||
field: 'equipment',
|
||||
headerName: '操作维护装备',
|
||||
minWidth: 150,
|
||||
cellRenderer: (params: any) => (
|
||||
<div
|
||||
style={{ lineHeight: '24px' }}
|
||||
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
|
||||
/>
|
||||
),
|
||||
autoHeight: true
|
||||
},
|
||||
{
|
||||
field: 'projects',
|
||||
headerName: '演训任务经历',
|
||||
minWidth: 150,
|
||||
cellRenderer: (params: any) => (
|
||||
<div
|
||||
style={{ lineHeight: '24px' }}
|
||||
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
|
||||
/>
|
||||
),
|
||||
autoHeight: true
|
||||
},
|
||||
// 修改剩余两个字段的cellRenderer为相同结构
|
||||
{
|
||||
field: 'awards',
|
||||
headerName: '奖励信息',
|
||||
minWidth: 150,
|
||||
cellRenderer: (params: any) => (
|
||||
<div
|
||||
style={{ lineHeight: '24px' }}
|
||||
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
|
||||
/>
|
||||
),
|
||||
autoHeight: true
|
||||
},
|
||||
{
|
||||
field: 'punishments',
|
||||
headerName: '处分信息',
|
||||
minWidth: 150,
|
||||
cellRenderer: (params: any) => (
|
||||
<div
|
||||
style={{ lineHeight: '24px' }}
|
||||
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
|
||||
/>
|
||||
),
|
||||
autoHeight: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
useEffect(() => {
|
||||
if (editingRecord) {
|
||||
form.setFieldsValue(editingRecord);
|
||||
console.log(editingRecord);
|
||||
}
|
||||
}, [editingRecord]);
|
||||
const handleEdit = (record) => {
|
||||
setEditingRecord(record);
|
||||
form.setFieldsValue(editingRecord);
|
||||
setVisible(true);
|
||||
|
||||
|
||||
const defaultColDef: ColDef = {
|
||||
sortable: true,
|
||||
filter: 'agSetColumnFilter',
|
||||
// floatingFilter: true,
|
||||
resizable: false,
|
||||
flex: 1,
|
||||
minWidth: 150,
|
||||
maxWidth: 600,
|
||||
suppressMovable: true,
|
||||
cellStyle: {
|
||||
whiteSpace: 'normal',
|
||||
overflowWrap: 'break-word'
|
||||
},
|
||||
tooltipValueGetter: (params: any) => params.value,
|
||||
wrapText: true,
|
||||
autoHeight: true
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
isLoading ? (<div>加载中...</div>) :
|
||||
(
|
||||
<Table
|
||||
key={"username"}
|
||||
columns={colnums}
|
||||
dataSource={staffs}
|
||||
className="table-auto w-full border-collapse border border-gray-300"
|
||||
tableLayout="fixed"
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
className: "flex justify-center mt-4",
|
||||
pageSize: 12,
|
||||
|
||||
<div className="ag-theme-alpine w-full h-[calc(100vh-100px)]"
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onMouseEnter: () => {
|
||||
const row = document.querySelector(`tr[data-row-key="${record.id}"]`);
|
||||
if (row) {
|
||||
row.classList.add("bg-gray-100");
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-gray-600 text-xl">加载中...</div>
|
||||
</div>
|
||||
) : (
|
||||
<AgGridReact
|
||||
modules={[SetFilterModule]}
|
||||
rowData={staffs}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={{
|
||||
...defaultColDef,
|
||||
filterParams: {
|
||||
textCustomComparator: (_, value) => value !== '',
|
||||
}
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
const row = document.querySelector(`tr[data-row-key="${record.id}"]`);
|
||||
if (row) {
|
||||
row.classList.remove("bg-gray-100");
|
||||
}
|
||||
},
|
||||
})}
|
||||
>
|
||||
<thead className="bg-gray-200">
|
||||
<tr>
|
||||
{colnums.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
className="px-4 py-2 border border-gray-300 text-center font-bold"
|
||||
>
|
||||
{column.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{staffs?.map((record) => (
|
||||
<tr
|
||||
key={record.id}
|
||||
className="border border-gray-300"
|
||||
>
|
||||
{colnums.map((column) => (
|
||||
<td
|
||||
key={column.key}
|
||||
className="px-4 py-2 border border-gray-300 text-center"
|
||||
>
|
||||
{column.render?.(record[column.dataIndex], record) || record[column.dataIndex]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}}
|
||||
localeText={zhCN} // 注入中文语言包
|
||||
enableCellTextSelection={true}
|
||||
pagination={true}
|
||||
paginationAutoPageSize={true}
|
||||
cacheQuickFilter={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
|
@ -2,18 +2,25 @@ import { useAuth } from "@web/src/providers/auth-provider"
|
|||
import { api } from "@nice/client"
|
||||
import { Select } from "antd"
|
||||
import { Department } from "packages/common/dist"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function DepartmentChildrenSelect() {
|
||||
interface Props {
|
||||
value?: string | null
|
||||
onChange?: (value: string | null) => void
|
||||
}
|
||||
export default function DepartmentChildrenSelect({value, onChange}: Props) {
|
||||
const { user, isAuthenticated } = useAuth()
|
||||
|
||||
// const { data: depts, isLoading: deptsLoading }
|
||||
// const { user, isAuthenticated } = useAuth()
|
||||
|
||||
const { data: depts, isLoading: deptsLoading }
|
||||
: { data: Department[], isLoading: boolean }
|
||||
= isAuthenticated ? api.department.getChildSimpleTree.useQuery({
|
||||
rootId: user.deptId
|
||||
}) : { data: null, isLoading: false }
|
||||
|
||||
const deptSelectOptions = depts?.map((dept) => ({
|
||||
label: dept.name,
|
||||
label: dept.title,
|
||||
value: dept.id
|
||||
}))
|
||||
return (
|
||||
|
@ -21,6 +28,8 @@ export default function DepartmentChildrenSelect() {
|
|||
placeholder="请选择单位"
|
||||
optionFilterProp="label"
|
||||
options={deptSelectOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -4,7 +4,11 @@ import { useAuth } from "@web/src/providers/auth-provider"
|
|||
import { TreeSelect } from "antd"
|
||||
import { useMemo } from "react"
|
||||
|
||||
export default function TrainContentTreeSelect() {
|
||||
interface Props {
|
||||
value?: string | null
|
||||
onChange?: (value: string | null) => void
|
||||
}
|
||||
export default function TrainContentTreeSelect({ value, onChange }: Props) {
|
||||
const { user, isAuthenticated } = useAuth()
|
||||
const { data: trainContents, isLoading: trainContentsLoading }
|
||||
: { data: TrainContent[], isLoading: boolean }
|
||||
|
@ -48,6 +52,8 @@ export default function TrainContentTreeSelect() {
|
|||
treeData={treeData}
|
||||
placeholder="请选择学科或课程"
|
||||
treeDefaultExpandAll={false}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
//onChange={onChange}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import TrainPlanCreateForm from "./TrainPlanCreateForm";
|
||||
|
||||
import TrainPlanWrite from "./TrainPlanWrite";
|
||||
|
||||
export default function DailyLayout(){
|
||||
return (
|
||||
<div className="w-full h-[calc(100vh-100px)]">
|
||||
<TrainPlanCreateForm></TrainPlanCreateForm>
|
||||
<TrainPlanWrite></TrainPlanWrite>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function TrainPlanWrite(){
|
||||
return <div>TrainPlanWrite</div>
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
export const areaOptions = [
|
||||
{
|
||||
value: '11',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{
|
||||
value: '1101',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{ value: '110101', label: '东城区' },
|
||||
{ value: '110102', label: '西城区' },
|
||||
{ value: '110105', label: '朝阳区' },
|
||||
{ value: '110106', label: '丰台区' },
|
||||
{ value: '110107', label: '石景山区' },
|
||||
{ value: '110108', label: '海淀区' },
|
||||
{ value: '110109', label: '门头沟区' },
|
||||
{ value: '110111', label: '房山区' },
|
||||
{ value: '110112', label: '通州区' },
|
||||
{ value: '110113', label: '顺义区' },
|
||||
{ value: '110114', label: '昌平区' },
|
||||
{ value: '110115', label: '大兴区' },
|
||||
{ value: '110116', label: '怀柔区' },
|
||||
{ value: '110117', label: '平谷区' },
|
||||
{ value: '110118', label: '密云区' },
|
||||
{ value: '110119', label: '延庆区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
// ... 其他省市区数据
|
||||
];
|
|
@ -10,7 +10,10 @@ import LoginPage from "../app/login";
|
|||
import HomePage from "../app/main/home/page";
|
||||
import StaffMessage from "../app/main/staffpage/page";
|
||||
import MainLayout from "../app/main/layout/MainLayout";
|
||||
import DailyPage from "../app/main/dailyPage/page";
|
||||
import DailyPage from "../app/main/daily/page";
|
||||
import Dashboard from "../app/main/home/page";
|
||||
import WeekPlanPage from "../app/main/plan/weekplan/page";
|
||||
import StaffInformation from "../app/main/staffinformation/page";
|
||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||
name?: string;
|
||||
breadcrumb?: string;
|
||||
|
@ -46,7 +49,11 @@ export const routes: CustomRouteObject[] = [
|
|||
children: [
|
||||
{
|
||||
index: true,
|
||||
element:<StaffMessage></StaffMessage>,
|
||||
element:<Dashboard></Dashboard>,
|
||||
},
|
||||
{
|
||||
path: "/staffinformation",
|
||||
element: <StaffInformation></StaffInformation>,
|
||||
},
|
||||
{
|
||||
path: "/staff",
|
||||
|
@ -54,8 +61,39 @@ export const routes: CustomRouteObject[] = [
|
|||
},
|
||||
{
|
||||
path:"/plan",
|
||||
children:[
|
||||
{
|
||||
path:"weekplan",
|
||||
element:<WeekPlanPage></WeekPlanPage>
|
||||
},
|
||||
{
|
||||
path:"monthplan",
|
||||
element:<DailyPage></DailyPage>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path:"/daily",
|
||||
element:<DailyPage></DailyPage>
|
||||
},
|
||||
{
|
||||
path:"/assessment",
|
||||
children:[
|
||||
{
|
||||
path:"positionassessment",
|
||||
element:<DailyPage></DailyPage>
|
||||
},
|
||||
{
|
||||
path:"commonassessment",
|
||||
element:<DailyPage></DailyPage>
|
||||
},
|
||||
{
|
||||
path:"sportsassessment",
|
||||
element:<DailyPage></DailyPage>
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ server {
|
|||
# 监听80端口
|
||||
listen 80;
|
||||
# 服务器域名/IP地址,使用环境变量
|
||||
server_name 192.168.43.206;
|
||||
server_name 192.168.252.77;
|
||||
|
||||
# 基础性能优化配置
|
||||
# 启用tcp_nopush以优化数据发送
|
||||
|
@ -100,7 +100,7 @@ server {
|
|||
# 仅供内部使用
|
||||
internal;
|
||||
# 代理到认证服务
|
||||
proxy_pass http://192.168.43.206:3001/auth/file;
|
||||
proxy_pass http://192.168.252.77:3000/auth/file;
|
||||
|
||||
# 请求优化:不传递请求体
|
||||
proxy_pass_request_body off;
|
||||
|
|
|
@ -421,45 +421,94 @@ model Department {
|
|||
}
|
||||
|
||||
model Staff {
|
||||
// 基础信息
|
||||
id String @id @default(cuid())
|
||||
showname String? @map("showname")
|
||||
username String @unique @map("username")
|
||||
avatar String? @map("avatar")
|
||||
password String? @map("password")
|
||||
phoneNumber String? @unique @map("phone_number")
|
||||
age Int? @map("age")
|
||||
sex Boolean? @map("sex")
|
||||
absent Boolean? @map("absent")@default(false)
|
||||
showname String? @map("showname")
|
||||
avatar String? @map("avatar")
|
||||
enabled Boolean? @default(true)
|
||||
|
||||
trainSituations TrainSituation[]
|
||||
position Position? @relation("StaffPosition", fields: [positionId], references: [id])
|
||||
|
||||
// 个人基本信息
|
||||
idNumber String? @unique @map("id_number") // 身份证号
|
||||
type String? @map("type") // 人员类型
|
||||
officerId String? @unique @map("officer_id") // 警号
|
||||
phoneNumber String? @unique @map("phone_number") // 手机号
|
||||
age Int? @map("age") // 年龄
|
||||
sex Boolean? @map("sex") // 性别
|
||||
bloodType String? @map("blood_type") // 血型
|
||||
birthplace String? @map("birthplace") // 籍贯
|
||||
source String? @map("source") // 来源
|
||||
|
||||
// 政治信息
|
||||
politicalStatus String? @map("political_status") // 政治面貌
|
||||
partyPosition String? @map("party_position") // 党内职务
|
||||
|
||||
// 职务信息
|
||||
rank String? @map("rank") // 衔职级别
|
||||
rankDate DateTime? @map("rank_date") // 衔职时间
|
||||
proxyPosition String? @map("proxy_position") // 代理职务
|
||||
position Position? @relation("StaffPosition", fields: [positionId], references: [id]) // 岗位
|
||||
positionId String? @map("position_id")
|
||||
|
||||
// 入职相关信息
|
||||
hireDate DateTime? @map("hire_date") // 入职时间
|
||||
seniority DateTime? @map("seniority_date") // 工龄认定时间
|
||||
sourceType String? @map("source_type") // 来源类型
|
||||
isReentry Boolean? @default(false) @map("is_reentry") // 是否二次入职
|
||||
isExtended Boolean? @default(false) @map("is_extended") // 是否延期服役
|
||||
currentPositionDate DateTime? @map("current_position_date") // 现岗位开始时间
|
||||
|
||||
// 教育背景
|
||||
education String? @map("education") // 学历
|
||||
educationType String? @map("education_type") // 学历形式
|
||||
isGraduated Boolean? @default(true) @map("is_graduated") // 是否毕业
|
||||
major String? @map("major") // 专业
|
||||
foreignLang String? @map("foreign_lang") // 外语能力
|
||||
|
||||
// 培训
|
||||
trainType String? @map("train_type") // 培训类型
|
||||
trainInstitute String? @map("train_institute") // 培训机构
|
||||
trainMajor String? @map("train_major") // 培训专业
|
||||
hasTrain Boolean? @default(false) @map("has_train") // 是否参加培训
|
||||
|
||||
//鉴定
|
||||
certRank String? @map("cert_rank") // 鉴定等级
|
||||
certWork String? @map("cert_work") // 鉴定工种
|
||||
hasCert Boolean? @default(false) @map("has_cert") // 是否参加鉴定
|
||||
|
||||
// 工作信息
|
||||
equipment String? @map("equipment") // 操作维护装备
|
||||
projects String? @map("projects") // 演训任务经历
|
||||
awards String? @map("awards") // 奖励信息
|
||||
punishments String? @map("staff_punishments") // 处分信息
|
||||
|
||||
// 部门关系
|
||||
domainId String? @map("domain_id")
|
||||
deptId String? @map("dept_id")
|
||||
|
||||
domain Department? @relation("DomainStaff", fields: [domainId], references: [id])
|
||||
department Department? @relation("DeptStaff", fields: [deptId], references: [id])
|
||||
order Float?
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
enabled Boolean? @default(true)
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
officerId String? @map("officer_id")
|
||||
|
||||
// watchedPost Post[] @relation("post_watch_staff")
|
||||
// 关联关系
|
||||
trainSituations TrainSituation[]
|
||||
visits Visit[]
|
||||
posts Post[]
|
||||
|
||||
learningPosts Post[] @relation("post_student")
|
||||
sentMsgs Message[] @relation("message_sender")
|
||||
receivedMsgs Message[] @relation("message_receiver")
|
||||
registerToken String?
|
||||
enrollments Enrollment[]
|
||||
teachedPosts PostInstructor[]
|
||||
ownedResources Resource[]
|
||||
|
||||
// 系统信息
|
||||
registerToken String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
absent Boolean? @default(false) @map("absent")
|
||||
|
||||
@@index([officerId])
|
||||
@@index([deptId])
|
||||
@@index([domainId])
|
||||
|
|
Loading…
Reference in New Issue