Files
gongxue-base/apps/admin/src/pages/Attendance/teacher.tsx
wangziqi ff3b6cdfde style(admin): 清理 AI slop 痕迹
- 移除教师端 hero 的英文眉标(TEACHING DAY),日期并入正文描述
- 删除不再使用的 attendance-eyebrow 样式
- Dashboard 的 ══ 装饰注释改为普通注释

kill-ai-slop 扫描: 18 处命中 → 7 处(剩余均为合理用法:
链接 hover 下划线、头像圆形角)
2026-08-07 17:41:01 +08:00

222 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button, Card, Col, Empty, Row, Spin, Tag, Tooltip } from 'antd';
import {
ArrowRightOutlined,
CheckCircleFilled,
ClockCircleOutlined,
ReloadOutlined,
ScheduleOutlined,
TeamOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import api from '../../api';
import { QueryErrorState } from '../../components/QueryState';
import {
canPullAttendance,
getSchedulePhase,
type SchedulePhase,
} from './attendance-workspace';
import LessonAttendanceDetail from './LessonAttendanceDetail';
import type { LessonAttendanceSchedule } from './types';
type TodaySchedule = LessonAttendanceSchedule;
interface AssignedClass {
classId: number;
className: string;
classCode: string;
roleType: string;
subject: string;
}
interface TeacherWorkspaceData {
assignedClasses: AssignedClass[];
todaySchedules: TodaySchedule[];
}
export const TeacherAttendanceWorkspace: React.FC<{ canCreate: boolean }> = ({ canCreate }) => {
const [selectedSchedule, setSelectedSchedule] = useState<TodaySchedule | null>(null);
const {
data: workspace,
isFetching,
isPending,
isError,
refetch,
} = useQuery<TeacherWorkspaceData | null>({
queryKey: ['attendance', 'workspace'],
queryFn: async () => {
return await api.get<TeacherWorkspaceData>('/rbac/teacher-workspace');
},
});
// isPending 覆盖自动重试的退避窗口,避免「加载失败/重试中」短暂闪现为空态
const loading = isPending || isFetching;
const loadWorkspace = useCallback(() => refetch(), [refetch]);
const classNameById = useMemo(
() => new Map(workspace?.assignedClasses.map((item) => [item.classId, item.className]) ?? []),
[workspace],
);
const openAttendance = useCallback((schedule: TodaySchedule) => {
setSelectedSchedule(schedule);
}, []);
const now = new Date();
const schedules = workspace?.todaySchedules ?? [];
const startedCount = schedules.filter((item) =>
canPullAttendance(getSchedulePhase(item.startTime, item.endTime, now)),
).length;
const nextSchedule = schedules.find(
(item) => getSchedulePhase(item.startTime, item.endTime, now) !== 'ended',
);
return (
<div className="attendance-page teacher-attendance">
<section className="attendance-hero attendance-hero--teacher">
<div>
<h1></h1>
<p>
{dayjs().format('MM月DD日 dddd')} ·
</p>
</div>
<Button icon={<ReloadOutlined />} onClick={() => void loadWorkspace()}>
</Button>
</section>
<Row gutter={[16, 16]} className="teacher-overview">
<Col xs={24} md={8}>
<div className="teacher-kpi">
<span></span>
<strong>{schedules.length}</strong>
<small></small>
</div>
</Col>
<Col xs={24} md={8}>
<div className="teacher-kpi">
<span></span>
<strong>{startedCount}</strong>
<small></small>
</div>
</Col>
<Col xs={24} md={8}>
<div className="teacher-kpi teacher-kpi--next">
<span></span>
<strong>{nextSchedule ? nextSchedule.startTime : '—'}</strong>
<small>{nextSchedule?.subject || '今天没有更多课程'}</small>
</div>
</Col>
</Row>
<div className="attendance-section-heading">
<div>
<span></span>
<h2></h2>
</div>
<span className="attendance-section-note"></span>
</div>
<Spin spinning={loading}>
{isError ? (
<Card className="attendance-empty-card">
<QueryErrorState
title="课程数据加载失败"
description="请检查网络后点击重试;若持续失败请联系管理员。"
onRetry={() => void loadWorkspace()}
/>
</Card>
) : schedules.length === 0 ? (
<Card className="attendance-empty-card">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<div>
<strong></strong>
<p></p>
</div>
}
/>
</Card>
) : (
<div className="lesson-timeline">
{schedules.map((schedule, index) => {
const phase = getSchedulePhase(schedule.startTime, schedule.endTime, now);
return (
<LessonCard
key={schedule.id}
schedule={schedule}
phase={phase}
className={classNameById.get(schedule.classId) || `班级 ${schedule.classId}`}
index={index + 1}
onOpen={canCreate ? () => openAttendance(schedule) : undefined}
/>
);
})}
</div>
)}
</Spin>
{canCreate ? (
<LessonAttendanceDetail
key={selectedSchedule?.id ?? 'closed'}
schedule={selectedSchedule}
className={
selectedSchedule
? classNameById.get(selectedSchedule.classId) || `班级 ${selectedSchedule.classId}`
: ''
}
onClose={() => setSelectedSchedule(null)}
/>
) : null}
</div>
);
};
export const LessonCard: React.FC<{
schedule: TodaySchedule;
phase: SchedulePhase;
className: string;
index: number;
onOpen?: () => void;
}> = ({ schedule, phase, className, index, onOpen }) => {
const phaseMeta = {
upcoming: { label: '待上课', icon: <ClockCircleOutlined />, tone: 'upcoming' },
ongoing: { label: '进行中', icon: <ScheduleOutlined />, tone: 'ongoing' },
ended: { label: '已结束', icon: <CheckCircleFilled />, tone: 'ended' },
}[phase];
return (
<article className={`lesson-card lesson-card--${phaseMeta.tone}`}>
<div className="lesson-sequence">{String(index).padStart(2, '0')}</div>
<div className="lesson-time">
<strong>{schedule.startTime}</strong>
<span />
<strong>{schedule.endTime}</strong>
</div>
<div className="lesson-main">
<div className="lesson-title-row">
<h3>{schedule.subject}</h3>
<Tag icon={phaseMeta.icon}>{phaseMeta.label}</Tag>
</div>
<p>
<TeamOutlined /> {className}
<span> {schedule.classroomId}</span>
</p>
</div>
<div className="lesson-action">
{phase === 'upcoming' ? (
<Tooltip title="课程尚未开始">
<Button disabled></Button>
</Tooltip>
) : onOpen ? (
<Button type="primary" onClick={onOpen}>
{phase === 'ongoing' ? '查看当前考勤' : '拉取 / 查看考勤'} <ArrowRightOutlined />
</Button>
) : null}
</div>
</article>
);
};