forked from wangziqi/gongxue-base
feat: DingTalk attendance import + integration config + expense types + UI polish
Server: - Add DingTalk attendance import service with SSE progress streaming - Add IntegrationConfig entity & module for multi-tenant DingTalk setup - Add ExpenseType entity & ExpenseTypesModule - Add SeedModule for DB initialization - Add UserDingMapping entity for DingTalk user linkage - Attendance service: import flow with dedup & student auto-mapping - Rooms service: time-range overlap queries - Sync controller/service: DingTalk integration wiring - Permission guard: refactor to pure re-export - Campus scope middleware: tenant-aware filtering Admin UI: - Attendance page: import UI with progress & result summary - All pages: tableStyle/tablePagination standardization - Login page: responsive styling - Sensitive data: useViewSensitive hook for masked viewing - Vite config: path aliases, build optimization - Test infra: vitest config, test utilities Docs: PRD DingTalk batch 1 & 2 design docs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Tabs,
|
||||
Card,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Spin,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
@@ -33,6 +34,8 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import api from '../../api';
|
||||
import { maskPhone, maskIdNumber } from '../../utils/sensitive';
|
||||
import { useViewSensitive } from '../../hooks/useViewSensitive';
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
@@ -752,16 +755,87 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
}
|
||||
}, [studentId]);
|
||||
|
||||
if (!aggregateData) return null;
|
||||
const handleViewSensitive = useViewSensitive(studentId, '学生档案');
|
||||
|
||||
const { student, profile, enrollments, examScores, learningRecords, result, attachments } = aggregateData;
|
||||
const tabItems = useMemo(() => {
|
||||
if (!aggregateData) return [];
|
||||
const { profile, enrollments, examScores, learningRecords, result, attachments } = aggregateData;
|
||||
return [
|
||||
{
|
||||
key: 'profile',
|
||||
label: '扩展档案',
|
||||
children: <ProfileTab data={profile} studentId={studentId} onRefresh={fetchData} />,
|
||||
},
|
||||
{
|
||||
key: 'enrollments',
|
||||
label: `报读班型 (${enrollments.length})`,
|
||||
children: (
|
||||
<EnrollmentsTab data={enrollments} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'exams',
|
||||
label: `考试成绩 (${examScores.length})`,
|
||||
children: (
|
||||
<ExamScoresTab
|
||||
data={examScores}
|
||||
studentId={studentId}
|
||||
enrollments={enrollments}
|
||||
onRefresh={fetchData}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'attendance',
|
||||
label: '出勤记录',
|
||||
children: <Empty description="暂无出勤记录" />,
|
||||
},
|
||||
{
|
||||
key: 'learning',
|
||||
label: `课堂回访 (${learningRecords.length})`,
|
||||
children: (
|
||||
<LearningTab data={learningRecords} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'result',
|
||||
label: '录取归档',
|
||||
children: <ResultTab data={result} studentId={studentId} onRefresh={fetchData} />,
|
||||
},
|
||||
{
|
||||
key: 'attachments',
|
||||
label: `附件 (${attachments.length})`,
|
||||
children: (
|
||||
<AttachmentsTab data={attachments} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'reports',
|
||||
label: '报告版本',
|
||||
children: <Empty description="暂无报告版本" />,
|
||||
},
|
||||
];
|
||||
}, [aggregateData, studentId, fetchData]);
|
||||
|
||||
if (!aggregateData) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 80 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const { student, profile } = aggregateData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inDrawer && (
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: 24 }}>
|
||||
<Space>
|
||||
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
||||
<Button type="text" icon={<CloseOutlined />} onClick={onClose} aria-label="关闭档案" />
|
||||
<span style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
学员档案 - {student.name} ({student.studentNo})
|
||||
</span>
|
||||
@@ -794,8 +868,26 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
|
||||
<Descriptions bordered column={3} size="small" style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="学号">{student.studentNo || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="电话">{student.phone || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="身份证号">{student.idNumber || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="电话">
|
||||
{student.phone ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskPhone(student.phone)}</span>
|
||||
<a onClick={() => handleViewSensitive('电话', student.phone)}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="身份证号">
|
||||
{student.idNumber ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskIdNumber(student.idNumber)}</span>
|
||||
<a onClick={() => handleViewSensitive('身份证号', student.idNumber)}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag>{student.status || '-'}</Tag>
|
||||
</Descriptions.Item>
|
||||
@@ -818,61 +910,7 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey="profile"
|
||||
items={[
|
||||
{
|
||||
key: 'profile',
|
||||
label: '扩展档案',
|
||||
children: <ProfileTab data={profile} studentId={studentId} onRefresh={fetchData} />,
|
||||
},
|
||||
{
|
||||
key: 'enrollments',
|
||||
label: `报读班型 (${enrollments.length})`,
|
||||
children: (
|
||||
<EnrollmentsTab data={enrollments} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'exams',
|
||||
label: `考试成绩 (${examScores.length})`,
|
||||
children: (
|
||||
<ExamScoresTab
|
||||
data={examScores}
|
||||
studentId={studentId}
|
||||
enrollments={enrollments}
|
||||
onRefresh={fetchData}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'attendance',
|
||||
label: '出勤记录',
|
||||
children: <Empty description="暂无出勤记录" />,
|
||||
},
|
||||
{
|
||||
key: 'learning',
|
||||
label: `课堂回访 (${learningRecords.length})`,
|
||||
children: (
|
||||
<LearningTab data={learningRecords} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'result',
|
||||
label: '录取归档',
|
||||
children: <ResultTab data={result} studentId={studentId} onRefresh={fetchData} />,
|
||||
},
|
||||
{
|
||||
key: 'attachments',
|
||||
label: `附件 (${attachments.length})`,
|
||||
children: (
|
||||
<AttachmentsTab data={attachments} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'reports',
|
||||
label: '报告版本',
|
||||
children: <Empty description="暂无报告版本" />,
|
||||
},
|
||||
]}
|
||||
items={tabItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user