This commit is contained in:
linfeng 2025-03-19 15:57:48 +08:00
parent e98f1dcfa0
commit 66fc63c682
20 changed files with 1666 additions and 388 deletions

View File

@ -1,9 +1,9 @@
import React from "react"
export default function HomePage() {
export default function Dashboard() {
return (
<div >
</div>
)
}

View File

@ -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>

View File

@ -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>
// </>
// );
// }

View File

View File

@ -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>
)
}

View File

@ -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();

View File

@ -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;

View File

@ -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 />
</>
);
}

View File

@ -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> */}
</>
)
}

View File

@ -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>
);
}

View File

@ -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}
/>
)
}

View File

@ -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}
/>
)

View File

@ -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>
)
}

View File

@ -0,0 +1,3 @@
export default function TrainPlanWrite(){
return <div>TrainPlanWrite</div>
}

View File

@ -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: '延庆区' }
]
}
]
},
// ... 其他省市区数据
];

View File

@ -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>
}
]
},
],
},

View File

@ -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;

View File

@ -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])