diff --git a/apps/web/src/app/main/dailyPage/page.tsx b/apps/web/src/app/main/daily/page.tsx similarity index 100% rename from apps/web/src/app/main/dailyPage/page.tsx rename to apps/web/src/app/main/daily/page.tsx diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 729e69d..d6c0312 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -1,9 +1,9 @@ import React from "react" -export default function HomePage() { +export default function Dashboard() { return (
- 首页 + 数据看板(待开发)
) } \ No newline at end of file diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index fee3ad5..737fe8f 100644 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -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 ( - - {/* 左侧导航栏 */} - - {/* 用户头像区域 */} -
-
- } - className=" transition-all duration-300" - /> -
-
- -
- {/* 新增可滚动内容区域 */} - - + + {/* 顶部Header */} +
+ +
+ + {/* 主体布局 */} + + {/* 左侧导航栏 */} + + + + + {/* 内容区域 */} + diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index b8c2ef8..490cea3 100644 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -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( + "首页概览", + "/", + , + null, + null, + ), + getItem( + "人员信息", + "/staffinformation", + , + null, + null, + ), + getItem( + "人员总览", + "/staff", + , + null, + null, + ), + getItem( + "训练计划", + "/plan", + , + [ + getItem("周训练计划", "/plan/weekplan", null, null, null), + getItem("月训练计划", "/plan/monthplan", null, null, null), + ], + null, + ), + getItem( + "每日填报", + "/daily", + , + null, + null, + ), + getItem( + "考核成绩", + "/assessment", + , + [ + getItem("岗位", "/assessment/positionassessment", null, null, null), + getItem("共同", "/assessment/commonassessment", null, null, null), + getItem("体育", "/assessment/sportsassessment", null, null, null), + ], + null, + ) +]; + +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([ + location.pathname, + ]); + // 展开菜单 + const [openKeys, setOpenKeys] = useState([]); + // const permissions = useSelector( + // (state: any) => state.loginUser.value.permissions + // ); + const [activeMenus, setActiveMenus] = useState(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 ( - <> +
+
{ + window.location.href = "/"; + }} + > + {/* 此处为版权标识,严禁删改 + */} +
+
location.pathname.startsWith(item.path))?.key, - ]} - > - {menuItems.map((item) => ( - navigate(item.path)} - > -
- {item.label} -
-
- ))} -
- + items={activeMenus} + onSelect={(data: any) => { + setSelectedKeys(data.selectedKeys); + }} + onOpenChange={(keys: any) => { + setOpenKeys(keys); + }} + /> +
+
); -} \ No newline at end of file +}; + +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 ( +// <> +// location.pathname.startsWith(item.path))?.key, +// ]} +// > +// {menuItems.map((item) => ( +// navigate(item.path)} +// > +//
+// {item.label} +//
+//
+// ))} +//
+// +// ); +// } \ No newline at end of file diff --git a/apps/web/src/app/main/plan/monthplan/page.tsx b/apps/web/src/app/main/plan/monthplan/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/src/app/main/plan/page.tsx b/apps/web/src/app/main/plan/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/src/app/main/plan/weekplan/page.tsx b/apps/web/src/app/main/plan/weekplan/page.tsx new file mode 100644 index 0000000..2d870a5 --- /dev/null +++ b/apps/web/src/app/main/plan/weekplan/page.tsx @@ -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([]) + const [columns, setColumns] = useState>([]) + const [currentPage, setCurrentPage] = useState(1) + const [trainingStatus, setTrainingStatus] = useState>({}) + const pageSize = 1 // 每页显示一个第一列的唯一值 + + const handleFileUpload = (event: React.ChangeEvent) => { + 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 = 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) => ( + +
+ +

点击或拖拽文件到此处上传

+

支持 .xlsx, .xls 格式的Excel文件

+
+ + + {data.length > 0 && ( + <> + +
+ +
+ + item[(columns[0] as ColumnType)?.dataIndex as string] + ))).filter(Boolean).length} + pageSize={1} + onChange={(page) => { + setCurrentPage(page) + }} + /> + + )} + + ) +} \ No newline at end of file diff --git a/apps/web/src/app/main/staffinformation/area-options.ts b/apps/web/src/app/main/staffinformation/area-options.ts new file mode 100644 index 0000000..05355b1 --- /dev/null +++ b/apps/web/src/app/main/staffinformation/area-options.ts @@ -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; + + return Object.entries(provinces).map(([provinceCode, provinceName]): DefaultOptionType => { + const cities = areaData[provinceCode] as Record; + + return { + value: provinceCode, + label: provinceName as string, + children: Object.entries(cities).map(([cityCode, cityName]): DefaultOptionType => { + const districts = areaData[cityCode] as Record; + + 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(); \ No newline at end of file diff --git a/apps/web/src/app/main/staffinformation/page.tsx b/apps/web/src/app/main/staffinformation/page.tsx new file mode 100644 index 0000000..acc41e8 --- /dev/null +++ b/apps/web/src/app/main/staffinformation/page.tsx @@ -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([]); + const [punishmentsList, setPunishmentsList] = useState([]); + const [equipmentList, setEquipmentList] = useState([]); // 新增装备列表 + const [projectsList, setProjectsList] = useState([]); // 新增任务列表 + 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 ( +
+

人员信息填报

+
{ + console.log('表单验证失败:', errorInfo); + }} + className="space-y-6" + > + {/* 个人基本信息 */} +
+

个人基本信息

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + return path.some(option => + typeof option.label === 'string' && + option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + ); + } + }} + changeOnSelect + /> + +
+
+ + {/* 政治信息 */} +
+

政治信息

+
+ + + + + + +
+
+ + {/* 职务信息 */} +
+

职务信息

+
+ + + + + + + + + + + + + +
+
+ + {/* 入职信息 */} +
+

入职信息

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + {/* 教育背景 */} +
+

教育背景

+
+ + + + + + + + + + + + + + + + + + +
+
+ + {/* 培训信息 */} +
+

培训信息

+
+ + + + + + + prevValues.hasTrain !== currentValues.hasTrain} + > + {({ getFieldValue }) => ( +
+ + + + + + + + + +
+ )} +
+
+
+ + {/* 鉴定信息 */} +
+

鉴定信息

+
+ + + + + + + prevValues.hasCert !== currentValues.hasCert} + > + {({ getFieldValue }) => ( +
+ + + + + + +
+ )} +
+
+
+ + {/* 工作信息 */} +
+

工作信息

+
+
+
+ + +
+
+ {equipmentList.map((equipment, index) => ( +
+ {equipment} + +
+ ))} +
+
+
+
+ + +
+
+ {projectsList.map((mission, index) => ( +
+ {mission} + +
+ ))} +
+
+
+
+ + +
+
+ {rewardsList.map((reward, index) => ( +
+ {reward} + +
+ ))} +
+
+
+
+ + +
+
+ {punishmentsList.map((punishment, index) => ( +
+ {punishment} + +
+ ))} +
+
+
+
+ +
+
+ + +
+
+ + + { + setIsModalVisible(false); + modalForm.resetFields(); + }} + > +
+ +