This commit is contained in:
Li1304553726 2025-04-06 19:44:22 +08:00
parent e223a02f95
commit 1fb33c2408
34 changed files with 2085 additions and 315 deletions

View File

@ -1,3 +1,6 @@
{
"marscode.chatLanguage": "cn"
"marscode.chatLanguage": "cn",
"marscode.codeCompletionPro": {
"enableCodeCompletionPro": true
}
}

View File

@ -14,6 +14,7 @@ import {
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthSchema, JwtPayload } from '@nice/common';
import { AuthGuard } from './auth.guard';

View File

@ -27,11 +27,14 @@ export class StaffController {
}
@Get('find-by-dept')
async findByDept(
@Query('dept-id') deptId: string,
@Query('dept-id') deptId: string | null,
@Query('domain-id') domainId: string,
) {
try {
const result = await this.staffService.findByDept({ deptId, domainId });
const result = await this.staffService.findByDept({
deptId: deptId || null,
domainId: domainId,
});
return {
data: result,
errmsg: 'success',

View File

@ -72,6 +72,8 @@ const StaffSelect = {
@Injectable()
export class StaffService extends BaseService<Prisma.StaffDelegate> {
//索引
[x: string]: any;
constructor(private readonly departmentService: DepartmentService) {
super(db, ObjectType.STAFF, true);
}
@ -83,7 +85,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
async findByDept(data: z.infer<typeof StaffMethodSchema.findByDept>) {
const { deptId, domainId } = data;
const childDepts = await this.departmentService.getDescendantIds(
deptId,
deptId || null,
true,
);
const result = await db.staff.findMany({

View File

@ -39,6 +39,7 @@ export class TrainSituationRouter {
deptId: z.string().optional(),
domainId: z.string().optional(),
trainContentId: z.string().optional(),
date: z.string().optional(),
}),
)
.query(async ({ input }) => {

View File

@ -18,13 +18,13 @@ export class TrainSituationService extends BaseService<Prisma.TrainSituationDele
// 创建培训情况
async create(args: Prisma.TrainSituationCreateArgs) {
console.log(args);
const result = await super.create(args);
const result = await db.trainSituation.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
// 更新培训情况
async update(args: Prisma.TrainSituationUpdateArgs) {
const result = await super.update(args);
const result = await db.trainSituation.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
@ -35,10 +35,11 @@ export class TrainSituationService extends BaseService<Prisma.TrainSituationDele
return result;
}
// 查找某一单位所有人员的培训情况
async findManyByDeptId(args: {
async findManyByDeptId(params: {
deptId?: string;
domainId?: string;
trainContentId?: string;
date?: string;
}): Promise<
{
id: string;
@ -49,16 +50,56 @@ export class TrainSituationService extends BaseService<Prisma.TrainSituationDele
score: number;
}[]
> {
const staffs = await this.staffService.findByDept({
deptId: args.deptId,
domainId: args.domainId,
});
const { deptId, domainId, trainContentId, date } = params;
// 构建查询条件
const where: Prisma.TrainSituationWhereInput = {};
if (deptId) {
const staffs = await this.staffService.findManyByDeptId(deptId);
where.staffId = { in: staffs.map((s) => s.id) };
}
if (trainContentId) {
where.trainContentId = trainContentId;
}
// 添加日期过滤条件
if (date) {
// 创建日期的开始时间当天的00:00:00
const startDate = new Date(date);
startDate.setHours(0, 0, 0, 0);
// 创建日期的结束时间当天的23:59:59.999
const endDate = new Date(date);
endDate.setHours(23, 59, 59, 999);
// 设置createdAt字段在指定的日期范围内
where.createdAt = {
gte: startDate,
lte: endDate,
};
// 日志输出,方便调试
console.log(
`Filtering train situations between ${startDate.toISOString()} and ${endDate.toISOString()}`,
);
}
// 查询结果并包含关联数据
const result = await super.findMany({
where: {
staffId: {
in: staffs.map((staff) => staff.id),
where,
include: {
staff: {
include: {
department: true,
position: true,
},
},
...(args.trainContentId ? { trainContentId: args.trainContentId } : {}),
trainContent: true,
},
orderBy: {
createdAt: 'desc', // 按创建时间倒序排列,最新的记录在前
},
});
return result;

View File

@ -1,42 +1,77 @@
.ag-theme-alpine {
--ag-primary-color: var(--color-primary);
--ag-alpine-active-color: var(--color-primary);
--ag-background-color: var(--color-bg-container);
--ag-foreground-color: var(--colorText);
--ag-borders-critical: solid 1px;
--ag-critical-border-color: var(--color-border-secondary);
--ag-borders: 1px solid;
--ag-borders-input: solid 1px;
--ag-border-color: var(--color-border-secondary);
--ag-secondary-border-color: var(--color-border-secondary);
--ag-secondary-foreground-color: var(--color-text-tertiary);
/* --ag-border-radius: 2px; */
--ag-header-column-separator-display: block;
--ag-header-column-separator-height: 30%;
--ag-header-column-separator-width: 2px;
--ag-header-column-separator-color: var(--color-fill-secondary);
--ag-font-size: var(--fontSize);
--ag-header-background-color: white;
--ag-selected-row-background-color: var(--color-border-primary);
--ag-range-selection-border-color: var(--color-border-primary);
--ag-header-font-size: var(--fontSize);
--ag-header-font-weight: 600;
--ag-header-foreground-color: var(--color-primary);
--ag-row-border-style: solid;
--ag-row-border-width: 1px;
--ag-row-border-color: var(--color-border-secondary);
--ag-row-hover-color: var(--color-bg-text-hover);
--ag-padding-horizontal: 0.7rem;
--ag-padding-vertical: 0.9rem;
--ag-side-panel-border-width: 1px;
--ag-side-panel-border-color: var(--color-border-secondary);
--ag-spacing: 6px;
--ag-odd-row-background-color: var(--color-fill-quaternary);
--ag-wrapper-border-width: 0px;
/* --ag-wrapper-border-color: var(--color-border-secondary); */
/* --ag-wrapper-border-radius: 10px; */
--ag-primary-color: var(--color-primary);
--ag-alpine-active-color: var(--color-primary);
--ag-background-color: var(--color-bg-container);
--ag-foreground-color: var(--colorText);
--ag-borders-critical: solid 1px;
--ag-critical-border-color: var(--color-border-secondary);
--ag-borders: 1px solid;
--ag-borders-input: solid 1px;
--ag-border-color: var(--color-border-secondary);
--ag-secondary-border-color: var(--color-border-secondary);
--ag-secondary-foreground-color: var(--color-text-tertiary);
/* --ag-border-radius: 2px; */
--ag-header-column-separator-display: block;
--ag-header-column-separator-height: 30%;
--ag-header-column-separator-width: 2px;
--ag-header-column-separator-color: var(--color-fill-secondary);
--ag-font-size: var(--fontSize);
--ag-header-background-color: white;
--ag-selected-row-background-color: var(--color-border-primary);
--ag-range-selection-border-color: var(--color-border-primary);
--ag-header-font-size: var(--fontSize);
--ag-header-font-weight: 600;
--ag-header-foreground-color: var(--color-primary);
--ag-row-border-style: solid;
--ag-row-border-width: 1px;
--ag-row-border-color: var(--color-border-secondary);
--ag-row-hover-color: var(--color-bg-text-hover);
--ag-padding-horizontal: 0.7rem;
--ag-padding-vertical: 0.9rem;
--ag-side-panel-border-width: 1px;
--ag-side-panel-border-color: var(--color-border-secondary);
--ag-spacing: 6px;
--ag-odd-row-background-color: var(--color-fill-quaternary);
--ag-wrapper-border-width: 0px;
/* --ag-wrapper-border-color: var(--color-border-secondary); */
/* --ag-wrapper-border-radius: 10px; */
}
.ag-root-wrapper {
border: 0px;
}
border: 0px;
}
.assessment-standard-table .ant-table {
border: 1px solid #e5e7eb;
}
.assessment-standard-table .ant-table-thead > tr > th {
background-color: #f9fafb;
font-weight: 500;
color: #374151;
text-align: center;
padding: 12px 8px;
border: 1px solid #e5e7eb;
}
.assessment-standard-table .ant-table-tbody > tr > td {
text-align: center;
padding: 12px 8px;
border: 1px solid #e5e7eb;
}
.assessment-standard-table .ant-table-tbody > tr:hover > td {
background-color: #f9fafb;
}
.assessment-standard-table .ant-input {
text-align: center;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 4px 8px;
width: 80px;
}
.assessment-standard-table .ant-input:focus {
border-color: #9ca3af;
box-shadow: none;
}

View File

@ -6,7 +6,7 @@ export default function AssessmentModal() {
<div>
<Modal
title="添加年龄范围"
visible={isAgeModalVisible}
open={isAgeModalVisible}
onOk={ageForm.submit}
onCancel={handleAgeCancel}
>
@ -19,10 +19,9 @@ export default function AssessmentModal() {
</Form.Item>
</Form>
</Modal>
<Modal
title="添加分数与对应标准"
visible={isScoreModalVisible}
open={isScoreModalVisible}
onOk={scoreForm.submit}
onCancel={handleScoreCancel}
>

View File

@ -57,7 +57,6 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
const showAgeModal = () => {
setIsAgeModalVisible(true);
};
// 处理年龄范围模态框确定
const handleAgeOk = (values: any) => {
console.log('values',values)
@ -68,10 +67,8 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
label: end ? `${start}-${end}` : `${start}岁以上`,
};
setAgeRanges([...ageRanges, newRange]);
setIsAgeModalVisible(false);
};
// 处理年龄范围模态框取消
const handleAgeCancel = () => {
setIsAgeModalVisible(false);
@ -81,7 +78,6 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
const showScoreModal = () => {
setIsScoreModalVisible(true);
};
// 处理分数标准模态框确定
const handleScoreOk = async (values: any) => {
const { score, standards } = values;

View File

@ -1,9 +1,9 @@
import { Button, Form, Input, Select, Skeleton } from "antd";
import { Button, Form, Input, Select, Skeleton, Pagination } from "antd";
import { useAssessmentStandardContext } from "./assessment-standard-provider";
import { api, useSport } from "@nice/client";
import toast from "react-hot-toast";
import create from "@ant-design/icons/lib/components/IconFont";
import { useState } from 'react';
const { Search } = Input;
export default function SportCreateContent() {
const { form, sportProjectList, sportProjectLoading } = useAssessmentStandardContext();
const { createSportProject, softDeleteByIds } = useSport();
@ -13,6 +13,9 @@ export default function SportCreateContent() {
title: "体能考核"
}
})
const [searchValue, setSearchValue] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const handleCreateProject = async () => {
if (form.getFieldsValue().createProjectName && form.getFieldsValue().unit) {
await createSportProject.mutateAsync({
@ -37,16 +40,20 @@ export default function SportCreateContent() {
}
}
const handleDeleteProject = async (id: string) => {
console.log(id)
// console.log(id)
await softDeleteByIds.mutateAsync({
ids: [id]
} as any)
toast.success("删除项目成功")
}
const handleSearch = (value) => {
setSearchValue(value);
setCurrentPage(1);
};
return (
<>
<Form form={form} layout="vertical">
<div className="flex items-center space-x-4 w-1/2">
<div className="flex items-center space-x-4 w-1/3">
<Form.Item label="创建项目" name="createProjectName">
<Input placeholder="请输入创建的项目名称" className="mr-2" />
</Form.Item>
@ -64,16 +71,48 @@ export default function SportCreateContent() {
</div>
{sportProjectLoading ?
<Skeleton></Skeleton> :
<div className='w-1/3 my-3 max-h-48 overflow-y-auto'>
<div className='flex my-3 max-h-48 overflow-y-auto'>
{sportProjectList?.filter(item=>item.deletedAt === null)?.map((item) => (
<div key={item.id} className='w-full flex justify-between p-4 mt-2 bg-white rounded-md'>
<div className='font-bold'>{item.name}{item.unit}</div>
<span className='text-red-500 cursor-pointer' onClick={() => handleDeleteProject(item.id)}></span>
<div
key={item.id}
className='w-full flex justify-between items-center p-4 my-3 bg-white rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition-shadow duration-200'
>
<div className='flex items-center'>
<span className='h-4 w-1 bg-blue-500 rounded-full mr-3'></span>
<div className='font-medium text-gray-800'>
{item.name}
<span className='text-gray-500 text-sm ml-2'>{item.unit}</span>
</div>
</div>
<button
className='text-gray-400 hover:text-red-500 transition-colors duration-200 flex items-center gap-1 px-2 py-1 rounded-md hover:bg-red-50'
onClick={() => handleDeleteProject(item.id)}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span></span>
</button>
</div>
))}
</div>
}
</Form>
{/* <Search
placeholder="搜索..."
allowClear
value={searchValue}
onChange={(e) => {
if (!e.target.value) {
setSearchValue('');
setCurrentPage(1);
} else {
setSearchValue(e.target.value);
}
}}
onSearch={handleSearch}
style={{ width: 200 }}
/> */}
</>
)
}

View File

@ -3,6 +3,7 @@ import { useAssessmentStandardContext } from "./assessment-standard-provider";
import toast from "react-hot-toast";
import { api, useSport } from "@nice/client";
import { useEffect, useMemo } from "react";
import '../../../App.css'
export default function StandardCreateContent() {
const {form,sportProjectList,ageRanges,records,showAgeModal,showScoreModal,setRecords,setAgeRanges,isStandardCreate,setIsStandardCreate} = useAssessmentStandardContext();
const { createSportStandard,updateSportStandard } = useSport();
@ -28,6 +29,9 @@ export default function StandardCreateContent() {
dataIndex: 'score',
key: 'score',
width: 100,
render: (score: number) => (
<span className="font-medium text-gray-700">{score}</span>
)
},
...ageRanges.map((range, index) => ({
title: range.label,
@ -35,7 +39,6 @@ export default function StandardCreateContent() {
key: `standard[${index}]`,
render: (_: any, record: any) => (
<Input
style={{ width: '80px' }}
value={record.standards[index]}
onChange={(e) => {
const inputValue = e.target.value;
@ -113,10 +116,8 @@ export default function StandardCreateContent() {
setRecords(records)
}
}, [data])
return (
<Form form={form} layout="vertical">
<Space size="large" className="my-6">
<Form.Item label="项目" name="projectId">
<Select
@ -125,7 +126,6 @@ export default function StandardCreateContent() {
options={sportProjectList?.filter(item=>item.deletedAt === null)?.map((item) => ({ value: item.id, label: `${item.name}${item.unit}` })) || []}
/>
</Form.Item>
<Form.Item label="性别" name="gender">
<Select
style={{ width: 120 }}
@ -136,7 +136,6 @@ export default function StandardCreateContent() {
]}
/>
</Form.Item>
<Form.Item label="人员类型" name="personType">
<Select
style={{ width: 160 }}
@ -151,14 +150,19 @@ export default function StandardCreateContent() {
<Button onClick={showScoreModal} className="mt-9 ml-2"></Button>
<Button type="primary" onClick={handleSave} className='mt-9 ml-2'></Button>
</Space>
<Table
columns={columns}
dataSource={records}
bordered
pagination={false}
scroll={{ x: 'max-content' }}
/>
<div className="mt-4">
<Table
columns={columns}
dataSource={records}
pagination={false}
bordered
rowKey="score"
className="assessment-standard-table"
size="middle"
scroll={{ x: 'max-content' }}
rowClassName={() => "bg-white"}
/>
</div>
</Form>
)
}

View File

@ -0,0 +1,7 @@
export default function CommonAssessmentPage() {
return (
<div>
<h1>CommonAssessmentPage</h1>
</div>
)
}

View File

@ -0,0 +1,141 @@
import { api } from "@nice/client";
import { useMainContext } from "../layout/MainProvider";
import toast from "react-hot-toast";
import { Form, Modal, Select, Radio } from "antd";
import { useState } from "react";
export default function DailyModal() {
const { form, visible, setVisible, editingRecord, setEditingRecord } = useMainContext();
const [submitting, setSubmitting] = useState(false);
// 获取更新和创建的mutation
const updateMutation = api.trainSituation.update.useMutation();
const createMutation = api.trainSituation.create.useMutation();
// 获取员工列表
const { data: staffs } = api.staff.findMany.useQuery({
where: {
deletedAt: null
},
include: {
position: true,
},
});
// console.log('wd', staffs);
// 获取训练内容列表
const { data: trainContents } = api.trainContent.findMany.useQuery({});
const handleOk = async () => {
try {
console.log('handleOk 开始执行');
const values = await form.validateFields();
console.log('表单验证通过,值:', values);
setSubmitting(true);
// 根据 TrainSituation 模型构建数据
const trainData = {
staffId: values.staffId,
trainContentId: values.trainContentId,
mustTrainTime: 1, // 预期训练时间
alreadyTrainTime: values.isPresent ? 1 : 0, // 实际训练时间
value: values.content, // 训练内容描述
score: values.isPresent ? 1 : 0,
};
if (editingRecord?.id) {
await updateMutation.mutateAsync({
where: { id: editingRecord.id },
data: trainData
});
} else {
await createMutation.mutateAsync({
data: trainData
});
}
console.log('操作成功');
toast.success("保存成功");
setVisible(false);
setEditingRecord(null);
form.resetFields();
} catch (error) {
console.error("保存失败:", error);
toast.error("保存失败");
} finally {
setSubmitting(false);
}
};
const handleCancel = () => {
setVisible(false);
setEditingRecord(null);
form.resetFields();
};
// useEffect(() => {
// if (visible && editingRecord) {
// form.setFieldsValue(editingRecord);
// }
// }, [visible, editingRecord]);
return (
<Modal
title="每日训练填报"
open={visible}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={submitting}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="staffId"
label="人员"
rules={[{ required: true, message: '请选择人员' }]}
>
<Select
placeholder="请选择人员"
options={staffs?.map(staff => ({
label: `${staff.showname || staff.username || '未知'} ${staff.positionId || '无职务'})}`,
value: staff.id
}))}
/>
</Form.Item>
{/* <Form.Item
name="position"
label="职务"
>
<Input disabled />
</Form.Item> */}
<Form.Item
name="trainContentId"
label="训练科目"
rules={[{ required: true, message: '请选择训练科目' }]}
>
<Select
placeholder="请选择训练科目"
options={trainContents?.map(item => ({
label: item.title,
value: item.id
}))}
/>
</Form.Item>
{/* <Form.Item
name="content"
label="训练内容"
rules={[{ required: true, message: '请填写训练内容' }]}
>
<Input.TextArea rows={4} placeholder="请输入今日训练内容" />
</Form.Item> */}
<Form.Item
name="isPresent"
label="在位情况"
rules={[{ required: true, message: '请选择在位情况' }]}
initialValue={true}
>
<Radio.Group>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
}

View File

@ -0,0 +1,76 @@
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Button, Form, Input, DatePicker } from "antd";
import { useCallback, useEffect } from "react";
import _ from "lodash";
import { useMainContext } from "../layout/MainProvider";
import DailyTable from "./DailyTable";
import DailyModal from "./DailyModal";
import dayjs from "dayjs";
export default function DailyReport() {
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
useEffect(() => {
setFormValue({
staffId: "",
trainContentId: "",
content: "",
isPresent: true,
date: dayjs().format('YYYY-MM-DD')
});
},);
const handleNew = () => {
form.setFieldsValue(formValue);
setVisible(true);
};
const handleSearch = useCallback(
_.debounce((value: string) => {
setSearchValue(value);
}, 500),
[]
);
const handleDateChange = (date) => {
if (!date) return;
// 更新当前选择的日期
const newFormValue = { ...formValue, date: date.format('YYYY-MM-DD') };
setFormValue(newFormValue);
};
return (
<div className="p-2 min-h-screen bg-gradient-to-br">
<Form>
<div className="p-4 h-full flex flex-col">
<div className="max-w-full mx-auto flex-1 flex flex-col">
<div className="flex justify-between mb-4 space-x-4 items-center">
<div className="text-2xl"></div>
<DatePicker
defaultValue={dayjs()}
onChange={handleDateChange}
allowClear={false}
className="w-40"
/>
<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>
<DailyTable></DailyTable>
<DailyModal></DailyModal>
</div>
</div>
</Form>
</div>
);
}

View File

@ -0,0 +1,143 @@
import { Button, Table, Modal } from "antd";
import { api } from "@nice/client";
import { useMainContext } from "../layout/MainProvider";
import dayjs from "dayjs";
import { useState } from "react";
// 定义 editRecord 的类型接口
interface EditRecord {
id: string;
staffId?: string;
staffName?: string;
position?: string;
trainContentId?: string;
content?: string;
isPresent: boolean;
}
export default function DailyTable() {
const { form, setVisible, searchValue, formValue } = useMainContext();
const [editingRecord, setEditingRecord] = useState<EditRecord | null>(null);
const { data: trainSituations, isLoading, refetch } =
api.trainSituation.findMany.useQuery({
where: {
...(searchValue ? {
staff: {
showname: {
contains: searchValue
}
}
} : {})
},
include: {
staff: {
include: {
position: true,
department: true
}
},
trainContent: true,
}
});
// console.log('data', trainSituations);
// 基于日期过滤数据(如果后端不支持日期过滤)
const filteredTrainSituations = trainSituations?.filter(situation => {
// 检查是否有日期条件
if (!formValue.date) return true;
// 将记录日期转换为YYYY-MM-DD格式
const situationDate = dayjs(situation.createdAt).format('YYYY-MM-DD');
return situationDate === formValue.date;
}) || [];
const columns = [
{
title: "人员姓名",
dataIndex: ["staff", "showname"],
key: "showname",
render: (name, record) => name || record.staff?.showname || "未知"
},
{
title: "单位",
dataIndex: ["staff", "department", "name"],
key: "dept",
render: (name) => name || "未知单位"
},
{
title: "职务",
dataIndex: ["staff", "position", "type"],
key: "position",
render: (type, record) => type || "无职务"
},
{
title: "训练内容",
dataIndex: ["trainContent", "title"],
key: "trainContent",
render: (title, record) => title || record.value || "未知内容"
},
{
title: "在位情况",
dataIndex: "alreadyTrainTime",
key: "isPresent",
render: (alreadyTrainTime) => alreadyTrainTime > 0 ? "在位" : "不在位"
},
{
title: "操作",
key: "action",
render: (_, record) => (
<div className="flex space-x-2">
<Button
type="primary"
key={record.id}
onClick={() => handleEdit(record)}
>
</Button>
</div>
),
}
];
const handleEdit = (record: any) => {
console.log(record);
const editRecord: EditRecord = {
id: record.id,
staffId: record.staff?.id,
staffName: record.staff?.showname,
position: record.staff?.position?.type,
trainContentId: record.trainContent?.id,
content: record.value,
isPresent: record.alreadyTrainTime > 0,
};
setEditingRecord(editRecord);
form.setFieldsValue(editRecord);
setVisible(true);
refetch();
};
return (
<>
{isLoading ? (
<div>...</div>
) : (
<Table
columns={columns}
dataSource={filteredTrainSituations}
rowKey="id"
tableLayout="fixed"
pagination={{
position: ["bottomCenter"],
className: "flex justify-center mt-4",
pageSize: 10,
}}
/>
)}
</>
);
}

View File

@ -0,0 +1,6 @@
import React from 'react';
import { Button, Table, Modal } from "antd";
import { api } from "@nice/client";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import dayjs from "dayjs";

View File

@ -1,10 +1,78 @@
import DailyContext from "@web/src/components/models/trainPlan/TrainPlanContext";
import DailyLayout from "@web/src/components/models/trainPlan/TrainPlanLayout";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Button, Form, Input, DatePicker } from "antd";
import { useCallback, useEffect } from "react";
import _ from "lodash";
import { useMainContext } from "../layout/MainProvider";
import DailyTable from "./DailyTable";
import DailyModal from "./DailyModal";
import dayjs from "dayjs";
export default function DailyPage(){
return <>
<DailyContext>
<DailyLayout></DailyLayout>
</DailyContext>
</>
export default function DailyReport() {
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
useEffect(() => {
setFormValue({
staffId: "",
trainContentId: "",
content: "",
isPresent: true,
date: dayjs().format('YYYY-MM-DD')
});
}, []);
const handleNew = () => {
form.setFieldsValue(formValue);
setVisible(true);
};
const handleSearch = useCallback(
_.debounce((value: string) => {
setSearchValue(value);
}, 500),
[]
);
const handleDateChange = (date) => {
if (!date) return;
// 更新当前选择的日期
const newFormValue = { ...formValue, date: date.format('YYYY-MM-DD') };
setFormValue(newFormValue);
};
return (
<div className="p-2 min-h-screen bg-gradient-to-br">
<Form>
<div className="p-4 h-full flex flex-col">
<div className="max-w-full mx-auto flex-1 flex flex-col">
<div className="flex justify-between mb-4 space-x-4 items-center">
<div className="text-2xl"></div>
<DatePicker
defaultValue={dayjs()}
onChange={handleDateChange}
allowClear={false}
className="w-40"
/>
<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>
<DailyTable></DailyTable>
<DailyModal></DailyModal>
</div>
</div>
</Form>
</div>
);
}

View File

@ -1,34 +1,642 @@
import { Button } from "antd"
import { api } from "@nice/client"
import React from "react"
import { useSport } from "@nice/client"
export default function Dashboard() {
const { createSportStandard } = useSport()
const handleCreateSportStandard = async () => {
const res = await createSportStandard.mutateAsync({
data: {
projectId: "cm8o6jzp908bp846bv513aqvo",
gender: true,
personType: "STAFF",
ageRanges: [
{ start: null, end: 24, label: "24岁以下" },
{ start: 24, end: 27, label: "25-27岁" },
{ start: 27, end: 30, label: "28-30岁" },
{ start: 30, end: null, label: "30岁以上" }
],
scoreTable: {
"100": [85, 81, 79, 77],
"95": [74, 70, 68, 66],
"90": [65, 61, 59, 57]
import React, { useState } from 'react';
import { Card, Row, Col, Divider } from 'antd';
import ReactEcharts from 'echarts-for-react';
const Dashboard = () => {
// 在这里获取你的项目数据
const [data, setData] = useState({
projectInfo: {
name: '训练管理系统',
type: '软件开发',
status: '进行中',
department: '技术研发中心',
client: '软件小组',
supervisor: '项目监理公司',
contractor: '开发团队',
planStartDate: '2023-10-01',
planEndDate: '2024-03-31',
actualStartDate: '2023-10-05',
actualEndDate: '待定'
},
attendance: {
personnel: { present: 15, absent: 5, leave: 2 }
},
training: {
// 参训内容数据
contents: [
{ value: 8, name: '岗位训练' },
{ value: 6, name: '共同科目' },
{ value: 4, name: '通用四项' },
{ value: 2, name: '其他' }
],
// 参训时长数据
hours: [
{ value: 25, name: '已完成' },
{ value: 10, name: '进行中' },
{ value: 5, name: '未开始' }
]
},
materials: {
planned: [10, 15, 20],
actual: [5, 8, 12]
}
});
// 人员出勤率图表配置 - 优化样式
const personnelAttendanceOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
formatter: '{a} <br/>{b}: {c}人 ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle',
itemWidth: 8,
itemHeight: 8,
textStyle: { color: '#fff', fontSize: 14 }
},
series: [{
name: '人员出勤率',
type: 'pie',
radius: ['50%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderWidth: 1,
borderColor: 'rgba(32,120,160,0.8)',
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 10,
color: '#fff',
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{
value: data.attendance.personnel.present,
name: '在位',
itemStyle: { color: '#3DD598' }
},
{
value: data.attendance.personnel.absent,
name: '不在位',
itemStyle: { color: '#FFB076' }
},
{
value: data.attendance.personnel.leave,
name: '请假',
itemStyle: { color: '#FC5A5A' }
}
]
}]
};
// 参训内容饼图配置
const trainingContentsOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
formatter: '{a} <br/>{b}: {c}项 ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
textStyle: { color: '#fff', fontSize: 14 }
},
series: [{
name: '参训内容',
type: 'pie',
radius: ['50%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderWidth: 1,
borderColor: 'rgba(32,120,160,0.8)',
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 10,
color: '#fff'
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{
value: data.training.contents[0].value,
name: data.training.contents[0].name,
itemStyle: { color: '#4FDFFF' }
},
{
value: data.training.contents[1].value,
name: data.training.contents[1].name,
itemStyle: { color: '#FFCA28' }
},
{
value: data.training.contents[2].value,
name: data.training.contents[2].name,
itemStyle: { color: '#3DD598' }
},
{
value: data.training.contents[3].value,
name: data.training.contents[3].name,
itemStyle: { color: '#FC5A5A' }
}
]
}]
};
// 参训时长饼图配置
const trainingHoursOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
formatter: '{a} <br/>{b}: {c}小时 ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
textStyle: { color: '#fff', fontSize: 14 }
},
series: [{
name: '参训时长',
type: 'pie',
radius: ['50%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderWidth: 1,
borderColor: 'rgba(32,120,160,0.8)',
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 10,
color: '#fff'
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{
value: data.training.hours[0].value,
name: data.training.hours[0].name,
itemStyle: { color: '#4FDFFF' }
},
{
value: data.training.hours[1].value,
name: data.training.hours[1].name,
itemStyle: { color: '#FFB076' }
},
{
value: data.training.hours[2].value,
name: data.training.hours[2].name,
itemStyle: { color: '#FC5A5A' }
}
]
}]
};
// 任务完成情况柱状图配置
const materialsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['上半年', '下半年'],
right: 10,
top: 10,
textStyle: {
color: '#fff',
fontSize: 10
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '22%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['任务A', '任务B', '任务C'],
axisLine: {
lineStyle: {
color: 'rgba(255,255,255,0.3)'
}
},
axisLabel: {
color: '#fff',
fontSize: 10
}
},
yAxis: {
type: 'value',
axisLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,0.3)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(255,255,255,0.1)',
type: 'dashed'
}
},
axisLabel: {
color: '#fff',
fontSize: 10
}
},
series: [
{
name: '上半年',
type: 'bar',
barWidth: '25%',
itemStyle: {
color: '#4FDFFF'
},
emphasis: {
itemStyle: {
color: '#6ab0f0'
}
},
data: data.materials.planned
},
{
name: '下半年',
type: 'bar',
barWidth: '25%',
itemStyle: {
color: '#FFCA28'
},
emphasis: {
itemStyle: {
color: '#FFB75C'
}
},
data: data.materials.actual
}
]
};
// 项目进度甘特图配置
const ganttChartOption = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: function (params) {
if (params.seriesName === '实际完成' || params.seriesName === '计划完成') {
return `${params.name}<br/>${params.seriesName}: ${params.value[0]} ~ ${params.value[1]}`;
}
return params.name;
},
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff'}
},
legend: {
data: ['计划完成', '实际完成', '超时完成'],
top: 0,
right: 10,
textStyle: { color: '#fff', fontSize: 12 },
itemWidth: 15,
itemHeight: 10
},
grid: {
top: 30,
bottom: 30,
left: 100,
right: 20
},
xAxis: {
type: 'time',
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' } },
axisLabel: {
color: '#fff',
fontSize: 10,
formatter: '{yyyy}.{MM}.{dd}',
rotate: 45,
margin: 8,
align: 'center',
interval: 'auto',
hideOverlap: true
},
splitLine: {
show: true,
lineStyle: { color: 'rgba(255,255,255,0.1)', type: 'dashed' }
},
min: '2019-02-01',
max: '2019-04-30'
},
yAxis: {
type: 'category',
data: ['岗位训练', '共同科目', '通用四项'],
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' } },
axisLabel: {
color: '#fff',
fontSize: 12,
margin: 20
},
inverse: true
},
series: [
{
name: '计划完成',
type: 'bar',
stack: 'same',
itemStyle: { color: '#FFCA28' },
barWidth: 20,
data: [
{
name: '岗位训练',
value: ['2019-03-08', '2019-04-17']
},
{
name: '共同科目',
value: ['2019-02-20', '2019-03-07']
},
{
name: '通用四项',
value: ['2019-02-05', '2019-02-26']
}
]
},
{
name: '实际完成',
type: 'bar',
stack: 'same',
itemStyle: { color: '#4FDFFF' },
barWidth: 20,
data: [
{
},
{
name: '共同科目',
value: ['2019-02-20', '2019-03-05']
},
{}
]
},
{
name: '超时完成',
type: 'bar',
stack: 'same',
itemStyle: { color: '#FC5A5A' },
barWidth: 20,
data: [
// 空数据
{},
{},
{
name: '通用四项',
value: ['2019-02-26', '2019-02-27']
}
]
},
// 添加当前日期标记
{
name: '当前日期',
type: 'line',
markLine: {
symbol: ['none', 'none'],
label: {
show: false
},
lineStyle: {
color: '#fff',
type: 'dashed',
width: 1
},
data: [
{
xAxis: '2019-02-27'
}
]
}
}
} as any)
console.log(res)
}
]
};
// 六边形背景SVG
const hexagonBg = `
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="hexagons" width="50" height="43.4" patternUnits="userSpaceOnUse" patternTransform="scale(5)">
<path d="M 0,25 12.5,0 37.5,0 50,25 37.5,50 12.5,50 Z"
stroke="rgba(32,120,160,0.2)" stroke-width="0.5" fill="none"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#hexagons)" />
</svg>
`;
// 卡片通用样式
const cardStyle = {
backgroundColor: 'rgba(0,21,41,0.7)',
color: 'white',
border: '1px solid rgba(32,120,160,0.8)',
boxShadow: '0 0 15px rgba(0,100,200,0.3)'
};
const headerStyle = {
color: '#4FDFFF',
borderBottom: '1px solid rgba(32,120,160,0.8)',
fontSize: '16px',
fontWeight: 'bold'
};
return (
<div >
<Button type="primary" onClick={() => handleCreateSportStandard()}></Button>
<div style={{
padding: '20px',
minHeight: '100vh',
background: 'linear-gradient(135deg, #001529 0%, #003366 100%)',
position: 'relative',
overflow: 'hidden'
}}>
{/* 六边形背景 */}
<div dangerouslySetInnerHTML={{ __html: hexagonBg }} style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 0
}} />
<div style={{ position: 'relative', zIndex: 1 }}>
<h1 style={{
textAlign: 'center',
color: '#4FDFFF',
marginBottom: '20px',
textShadow: '0 0 10px rgba(79,223,255,0.5)'
}}></h1>
<div style={{
position: 'absolute',
top: '20px',
left: '20px',
fontSize: '14px',
color: '#fff'
}}>
{new Date().toLocaleString()} {['日', '一', '二', '三', '四', '五', '六'][new Date().getDay()]}
</div>
<Row gutter={[16, 16]}>
<Col span={8}>
<Card
title="项目概况"
bordered={false}
style={{...cardStyle,height:'100%'}}
headStyle={headerStyle}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* <div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div> */}
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>: </span>
<span>{data.projectInfo.name}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>: </span>
<span>{data.projectInfo.client}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>:</span>
<span>{data.projectInfo.planStartDate}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>:</span>
<span>{data.projectInfo.planEndDate}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div>
</div>
</Card>
</Col>
<Col span={16}>
<Card
title="项目进度甘特图"
bordered={false}
style={{...cardStyle,height:'100%'}}
headStyle={headerStyle}
>
<ReactEcharts option={ganttChartOption} style={{ height: '220px' }} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
<Col span={8}>
<Card
title="人员出勤率"
bordered={false}
style={cardStyle}
headStyle={headerStyle}
>
<ReactEcharts option={personnelAttendanceOption} style={{ height: '220px' }} />
</Card>
</Col>
<Col span={8}>
<Card
title="参训内容"
bordered={false}
style={cardStyle}
headStyle={headerStyle}
>
<ReactEcharts option={trainingContentsOption} style={{ height: '220px' }} />
</Card>
</Col>
<Col span={8}>
<Card
title="参训时长"
bordered={false}
style={cardStyle}
headStyle={headerStyle}
>
<ReactEcharts option={trainingHoursOption} style={{ height: '220px' }} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
<Col span={24}>
<Card
title="任务完成情况"
bordered={false}
style={{ height: '300px', backgroundColor: '#0c2135', color: 'white' }}
headStyle={{ color: 'white', borderBottom: '1px solid #1890ff' }}
>
<ReactEcharts option={materialsOption} style={{ height: '220px' }} />
</Card>
</Col>
</Row>
</div>
</div>
)
}
);
};
export default Dashboard;

View File

@ -39,7 +39,7 @@ const items = [
),
getItem(
"训练计划",
"/plan",
"/plan",
<i className="iconfont icon-icon-user" />,
[
getItem("周训练计划", "/plan/weekplan", null, null, null),
@ -88,7 +88,7 @@ const NavigationMenu: React.FC = () => {
"^/plan/monthplan": ["/plan"],
// 添加考核成绩子路径的匹配规则
"^/assessment/positionassessment": ["/assessment"],
"^/assessment/commonassessment": ["/assessment"],
"^/assessment/commonassessment": ["/assessment"],
"^/assessment/sportsassessment": ["/assessment"],
"^/admin/base-setting": ["/admin"],
"^/admin/department": ["/admin"],
@ -107,8 +107,7 @@ const NavigationMenu: React.FC = () => {
location.pathname.startsWith("/assessment/") ||
location.pathname === "/plan/weekplan" ||
location.pathname === "/plan/monthplan"
)
{
) {
setSelectedKeys([location.pathname]);
setOpenKeys([location.pathname.split('/').slice(0, 2).join('/')]);
} else if (
@ -121,9 +120,9 @@ const NavigationMenu: React.FC = () => {
setOpenKeys(openKeyMerge(location.pathname));
}
}, [location.pathname]);
const hit = (pathname: string): string[] => {
for (let p in children2Parent) {
for (const p in children2Parent) {
if (pathname.search(p) >= 0) {
return children2Parent[p];
}
@ -132,7 +131,7 @@ const NavigationMenu: React.FC = () => {
};
const openKeyMerge = (pathname: string): string[] => {
let newOpenKeys = hit(pathname);
const newOpenKeys = hit(pathname);
for (let i = 0; i < openKeys.length; i++) {
let isIn = false;
for (let j = 0; j < newOpenKeys.length; j++) {
@ -167,20 +166,20 @@ const NavigationMenu: React.FC = () => {
useEffect(() => {
setActiveMenus(items);
// console.log(activeMenus)
},[items])
}, [items])
// useEffect(() => {
// checkMenuPermissions(items, permissions);
// }, [items, permissions]);
// const checkMenuPermissions = (items: any, permissions: any) => {
// let menus: any = [];
// const menus: any = [];
// if (permissions.length === 0) {
// setActiveMenus(menus);
// return;
// }
// for (let i in items) {
// let menuItem = items[i];
// for (const i in items) {
// const menuItem = items[i];
// // 一级菜单=>没有子菜单&配置了权限
// if (menuItem.children === null) {
// if (
@ -192,10 +191,10 @@ const NavigationMenu: React.FC = () => {
// menus.push(menuItem);
// continue;
// }
// let children = [];
// const children = [];
// for (let j in menuItem.children) {
// let childrenItem = menuItem.children[j];
// for (const j in menuItem.children) {
// const childrenItem = menuItem.children[j];
// if (
// typeof permissions[childrenItem.permission] !== "undefined" ||

View File

@ -0,0 +1,7 @@
export default function MonthPlanPage() {
return (
<div>
<h1>MonthPlanPage</h1>
</div>
)
}

View File

@ -3,7 +3,7 @@ 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'
import React from 'react';
interface TableData {
key: string
[key: string]: any
@ -25,7 +25,7 @@ export default function WeekPlanPage() {
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 => {
@ -45,7 +45,7 @@ export default function WeekPlanPage() {
})
}
const jsonData: any[] = XLSX.utils.sheet_to_json(firstSheet, {
const jsonData: any[] = XLSX.utils.sheet_to_json(firstSheet, {
header: 1,
defval: '',
blankrows: false,
@ -79,7 +79,7 @@ export default function WeekPlanPage() {
children: text,
props: {
rowSpan: calculateRowSpan(tableData, index, header),
style: {
style: {
border: '1px solid #f0f0f0',
borderBottom: '1px solid #f0f0f0'
}
@ -158,7 +158,7 @@ export default function WeekPlanPage() {
// 获取当前页应该显示的值
const currentValue = uniqueValues[(currentPage - 1)]
// 返回第一列等于当前值的所有行
return data.filter(item => item[firstColumnName] === currentValue)
}
@ -188,7 +188,7 @@ export default function WeekPlanPage() {
<p className="text-sm text-gray-500"> .xlsx, .xls Excel文件</p>
</div>
</div>
{data.length > 0 && (
<>
<Table
@ -212,7 +212,7 @@ export default function WeekPlanPage() {
<Pagination
className="mt-4"
current={currentPage}
total={Array.from(new Set(data.map(item =>
total={Array.from(new Set(data.map(item =>
item[(columns[0] as ColumnType<TableData>)?.dataIndex as string]
))).filter(Boolean).length}
pageSize={1}

View File

@ -0,0 +1,7 @@
export default function PositionAssessmentPage() {
return (
<div>
<h1>PositionAssessmentPage</h1>
</div>
)
}

View File

@ -1,7 +1,6 @@
import { Button, Table, Modal, Form, Upload, Skeleton } from "antd";
import { useEffect, useState,} from "react";
import { useEffect, useState, } from "react";
import toast from "react-hot-toast";
import _ from "lodash";
import * as XLSX from 'xlsx';
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import { useSportPageContext } from "./sportPageProvider";
@ -11,14 +10,14 @@ import { api, useTrainSituation } from "@nice/client";
export default function SportPage() {
const [sportsData, setSportsData] = useState([]);
const { sportProjectList, handlNewScoreOpen, staffsWithScore, staffsWithScoreLoading } = useSportPageContext()
const {deleteSameGroupTrainSituation} = useTrainSituation()
const { deleteSameGroupTrainSituation } = useTrainSituation()
useEffect(() => {
if (!staffsWithScore) return;
const newSportsData = []
staffsWithScore.forEach(item => {
const groupedTrainSituations = {};
item.trainSituations.forEach(train => {
if(train.groupId === null) return
if (train.groupId === null) return
const groupId = train.groupId;
if (!groupedTrainSituations[groupId]) {
groupedTrainSituations[groupId] = {
@ -39,12 +38,12 @@ export default function SportPage() {
age: item.age,
unit: item.department?.name,
situationIds: groupedTrainSituations[groupId].situations.map(train => train.id),
totalScore:groupedTrainSituations[groupId].totalScore,
totalScore: groupedTrainSituations[groupId].totalScore,
...groupedTrainSituations[groupId].trainScore,
groupId
})
});
});
console.log(newSportsData)
setSportsData(newSportsData);
@ -70,12 +69,6 @@ export default function SportPage() {
dataIndex: "unit",
key: "unit",
},
...sportProjectList?.filter(item=>item.deletedAt === null).map((item) => ({
title: item.name,
dataIndex: item.name,
key: item.name,
editable: true,
})),
{
title: "BMI",
dataIndex: "bodyType",
@ -115,7 +108,7 @@ export default function SportPage() {
console.log(record)
await deleteSameGroupTrainSituation.mutateAsync(
{
groupId:record.groupId
groupId: record.groupId
}
)
toast.success("删除成功");
@ -148,37 +141,35 @@ export default function SportPage() {
// 导入成绩
const handleImport = (file) => {
const reader = new FileReader();
// const reader = new FileReader();
// reader.onload = (e) => {
// try {
// const data = new Uint8Array(e.target.result);
// const workbook = XLSX.read(data, { type: 'array' });
// // 获取第一个工作表
// const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// // 转换为JSON
// const jsonData = XLSX.utils.sheet_to_json(worksheet);
// // 添加id字段
// const importedData = jsonData.map((item, index) => ({
// id: sportsData.length + index + 1,
// ...item,
// // 确保totalScore是计算所有项目成绩的和
// totalScore: calculateTotalScore(item)
// }));
// // 更新数据
// setSportsData([...sportsData, ...importedData]);
// toast.success(`成功导入${importedData.length}条记录`);
// } catch (error) {
// console.error('导入失败:', error);
// toast.error("导入失败,请检查文件格式");
// }
// };
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
// 获取第一个工作表
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 转换为JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet);
// 添加id字段
const importedData = jsonData.map((item, index) => ({
id: sportsData.length + index + 1,
...item,
// 确保totalScore是计算所有项目成绩的和
totalScore: calculateTotalScore(item)
}));
// reader.readAsArrayBuffer(file);
// 更新数据
setSportsData([...sportsData, ...importedData]);
toast.success(`成功导入${importedData.length}条记录`);
} catch (error) {
console.error('导入失败:', error);
toast.error("导入失败,请检查文件格式");
}
};
reader.readAsArrayBuffer(file);
// 防止自动上传
return false;
// // 防止自动上传
// return false;
};
return (

View File

@ -51,7 +51,7 @@ export default function SportPageModal() {
<Modal
title="添加考核成绩"
onOk={newScoreForm.submit}
visible={isNewScoreModalVisible}
open={isNewScoreModalVisible}
onCancel={handleNewScoreCancel}
>
<Form

View File

@ -1,13 +1,13 @@
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Button, Form, Input } from "antd";
import { useCallback, useEffect} from "react";
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, editingRecord} = useMainContext();
useEffect(()=>{
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
useEffect(() => {
setFormValue(
{
username: "",
@ -15,9 +15,9 @@ export default function StaffMessage() {
absent: false,
positionId: "",
trainSituations: "",
}
}
)
},[])
}, [])
const handleNew = () => {
form.setFieldsValue(formValue);
console.log(editingRecord);
@ -54,7 +54,7 @@ export default function StaffMessage() {
</Button>
</div>
<StaffTable></StaffTable>
<StaffModal></StaffModal>
<StaffModal></StaffModal>
</div>
</div>
</Form>

View File

@ -7,18 +7,17 @@ import TrainContentTreeSelect from "@web/src/components/models/trainContent/trai
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
export default function StaffModal() {
const { data: traincontents} = api.trainSituation.findMany.useQuery({
select:{
const { data: traincontents } = api.trainSituation.findMany.useQuery({
select: {
id: true,
trainContent:{
select:{
trainContent: {
select: {
id: true,
title: true,
type: true,
}
},
}
});
// useEffect(() => {
// traincontents?.forEach((situation)=>{
@ -76,7 +75,7 @@ export default function StaffModal() {
data: staffData
});
}
toast.success("保存成功");
setVisible(false);
setEditingRecord(null);
@ -94,15 +93,15 @@ export default function StaffModal() {
};
useEffect(() => {
if (visible&&editingRecord) {
if (visible && editingRecord) {
form.setFieldsValue(editingRecord);
}
}, [visible,editingRecord]);
}, [visible, editingRecord]);
return (
<>
<Modal
title="编辑员工信息"
visible={visible}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
>
@ -137,7 +136,7 @@ export default function StaffModal() {
<Select.Option value={false}></Select.Option>
</Select>
</Form.Item>
<Form.List name="trainSituations">
{(fields, { add, remove }) => (
<>
@ -149,7 +148,7 @@ export default function StaffModal() {
label="培训内容"
className="flex-1"
>
<TrainContentTreeSelect/>
<TrainContentTreeSelect />
</Form.Item>
<Form.Item
{...restField}

View File

@ -9,15 +9,15 @@ import { render } from "react-dom";
// 提取处理嵌套字段的函数
const getNestedValue = (record: any, dataIndex: string | string[]) => {
if (Array.isArray(dataIndex)) {
return dataIndex.reduce((obj, key) => obj?.[key], record);
}
return record[dataIndex];
if (Array.isArray(dataIndex)) {
return dataIndex.reduce((obj, key) => obj?.[key], record);
}
return record[dataIndex];
};
export default function StaffTable() {
const{form, setVisible,searchValue} = useMainContext()
const { form, setVisible, searchValue } = useMainContext()
const { data: staffs, isLoading } = api.staff.findMany.useQuery({
where: {
deletedAt: null,
@ -44,7 +44,7 @@ export default function StaffTable() {
console.log(staffs);
}, [staffs]);
const { softDeleteByIds } = useStaff();
const {editingRecord, setEditingRecord} = useMainContext();
const { editingRecord, setEditingRecord } = useMainContext();
const [isTrainingModalVisible, setIsTrainingModalVisible] = useState(false);
const [selectedTrainings, setSelectedTrainings] = useState([]);
@ -70,7 +70,7 @@ export default function StaffTable() {
title: "职务",
dataIndex: ["position", "type"],
key: "position",
render: (_, record) => record.position?.type || "无职务"
render: (_, record) => record.position?.type || "无职务"
},
{
title: "在位",
@ -84,7 +84,7 @@ export default function StaffTable() {
key: "trainSituations",
render: (situations) => {
if (!situations?.length) return "无培训记录";
return (
<div>
{situations.slice(0, 2).map((s) => (
@ -94,7 +94,7 @@ export default function StaffTable() {
</div>
))}
{situations.length > 2 && (
<span
<span
className="text-blue-500 cursor-pointer hover:underline"
onClick={() => showTrainingDetails(situations)}
>
@ -142,15 +142,15 @@ export default function StaffTable() {
];
useEffect(() => {
if (editingRecord) {
form.setFieldsValue(editingRecord);
console.log(editingRecord);
form.setFieldsValue(editingRecord);
console.log(editingRecord);
}
}, [editingRecord]);
}, [editingRecord]);
const handleEdit = (record) => {
setEditingRecord(record);
form.setFieldsValue(record); // 修正为设置当前记录的值
setVisible(true);
};
};
return (
<>
{
@ -188,7 +188,7 @@ export default function StaffTable() {
// 使用提取的函数处理嵌套字段
<td key={column.key}>
{column.render?.(
getNestedValue(record, column.dataIndex),
getNestedValue(record, column.dataIndex),
record
) || getNestedValue(record, column.dataIndex)}
</td>
@ -199,7 +199,7 @@ export default function StaffTable() {
</Table>
)
}
<Modal
title="培训详情"
open={isTrainingModalVisible}

View File

@ -1,6 +1,6 @@
import TrainPlanWrite from "./TrainPlanWrite";
export default function DailyLayout(){
return (
<div className="w-full h-[calc(100vh-100px)]">

View File

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

View File

@ -17,6 +17,9 @@ import AdminLayout from "../components/layout/admin/AdminLayout";
import { adminRoute } from "./admin-route";
import SportPage from "../app/main/sport/page";
import { SportPageProvider } from "../app/main/sport/sportPageProvider";
import PositionAssessmentPage from "../app/main/positionassessment/page";
import CommonAssessmentPage from "../app/main/commonassessment/page";
import MonthPlanPage from "../app/main/plan/monthplan/page";
interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
@ -25,7 +28,6 @@ interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
}
export interface CustomNonIndexRouteObject extends NonIndexRouteObject {
name?: string;
children?: CustomRouteObject[];
@ -53,45 +55,45 @@ export const routes: CustomRouteObject[] = [
children: [
{
index: true,
element:<Dashboard></Dashboard>,
element: <Dashboard></Dashboard>,
},
{
path: "/staff",
element: <StaffMessage></StaffMessage>,
},
{
path:"/plan",
children:[
path: "/plan",
children: [
{
path:"weekplan",
element:<WeekPlanPage></WeekPlanPage>
path: "weekplan",
element: <WeekPlanPage></WeekPlanPage>
},
{
path:"monthplan",
element:<DailyPage></DailyPage>
path: "monthplan",
element: <MonthPlanPage></MonthPlanPage>
}
]
},
{
path:"/daily",
element:<DailyPage></DailyPage>
path: "/daily",
element: <DailyPage></DailyPage>
},
{
path:"/assessment",
children:[
path: "/assessment",
children: [
{
path:"positionassessment",
element:<DailyPage></DailyPage>
path: "positionassessment",
element: <PositionAssessmentPage></PositionAssessmentPage>
},
{
path:"commonassessment",
element:<DailyPage></DailyPage>
path: "commonassessment",
element: <CommonAssessmentPage></CommonAssessmentPage>
},
{
path:"sportsassessment",
element:<SportPageProvider>
<SportPage></SportPage>
</SportPageProvider>
path: "sportsassessment",
element: <SportPageProvider>
<SportPage></SportPage>
</SportPageProvider>
}
]
},
@ -100,13 +102,11 @@ export const routes: CustomRouteObject[] = [
element: <AdminLayout></AdminLayout>,
children: adminRoute.children,
}
],
},
],
},
{
path: "/login",
breadcrumb: "登录",

View File

@ -100,7 +100,7 @@ server {
# 仅供内部使用
internal;
# 代理到认证服务
proxy_pass http://192.168.252.77:3000/auth/file;
proxy_pass http://192.168.252.77:3001/auth/file;
# 请求优化:不传递请求体
proxy_pass_request_body off;

View File

@ -11,5 +11,11 @@
},
"keywords": [],
"author": "insiinc",
"license": "ISC"
"license": "ISC",
"dependencies": {
"@antv/g6": "^5.0.44",
"antd": "^5.23.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2"
}
}

View File

@ -437,7 +437,6 @@ model Staff {
age Int? @default(22) @map("age")
sex Boolean? @default(true) @map("sex")
absent Boolean? @default(false) @map("absent")
trainSituations TrainSituation[]
position Position? @relation("StaffPosition", fields: [positionId], references: [id])
positionId String? @map("position_id")

File diff suppressed because it is too large Load Diff