feat: 钉钉学生同步支持仅绑定手机号及学生列表修复

This commit is contained in:
2026-08-04 14:41:48 +08:00
parent 50c44e4410
commit 216a20ffaf
9 changed files with 334 additions and 36 deletions

View File

@@ -28,6 +28,7 @@ import {
InboxOutlined,
PlusOutlined,
SwapOutlined,
SyncOutlined,
UndoOutlined,
UploadOutlined,
} from '@ant-design/icons';
@@ -39,6 +40,7 @@ import JinshujuMatchModal from '../../components/JinshujuMatchModal';
import { maskIdNumber, maskPhone } from '../../utils/sensitive';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
import { useUserStore } from '../../store/user/userStore';
import { selectArchiveRecords } from '../archive-view';
const statusMap: Record<string, { text: string; color: string }> = {
@@ -79,6 +81,17 @@ interface StudentUpdateImportResult {
skipped?: number;
}
interface DingTalkSyncLog {
status: string;
recordsCount: number;
errorMessage?: string | null;
}
interface DingTalkSyncResult {
synced: number;
logs: DingTalkSyncLog[];
}
interface StudentFilterLookups {
classes: Array<{ id: number; name: string; code?: string }>;
teachers: Array<{ id: number; name: string; username: string }>;
@@ -98,6 +111,7 @@ const StudentsPage: React.FC = () => {
const canEditStudent = hasPermission('student:edit');
const canDeleteStudent = hasPermission('student:delete');
const canSyncJinshuju = hasAllPermissions('sync:read', 'sync:trigger');
const canSyncDingTalk = hasAllPermissions('sync:read', 'sync:trigger');
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
@@ -114,7 +128,9 @@ const StudentsPage: React.FC = () => {
const [showArchived, setShowArchived] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const [batchLoading, setBatchLoading] = useState(false);
const [dingSyncLoading, setDingSyncLoading] = useState(false);
const [enrollmentData, setEnrollmentData] = useState<Record<number, EnrollmentInfo[]>>({});
const [pageInfo, setPageInfo] = useState({ current: 1, pageSize: 15 });
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerStudentId, setDrawerStudentId] = useState<number | undefined>(undefined);
const [form] = Form.useForm();
@@ -336,7 +352,7 @@ const StudentsPage: React.FC = () => {
const baseURL = import.meta.env.PROD
? '/api'
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
const token = localStorage.getItem('token');
const token = useUserStore.getState().token;
fetch(`${baseURL}/students/template`, { headers: { Authorization: `Bearer ${token}` } })
.then((res) => res.blob())
.then((blob) => {
@@ -441,11 +457,33 @@ const StudentsPage: React.FC = () => {
}
};
const handleDingTalkSync = async () => {
setDingSyncLoading(true);
try {
const res = await api.post<DingTalkSyncResult>('/sync/trigger', null, {
params: { platform: 'dingtalk_students', createMissing: false, updateProfile: false },
timeout: 120000,
});
const log = res.logs?.[0];
if (log?.status === 'partial') {
message.warning(log.errorMessage || '钉钉同步完成,但有数据需要人工处理');
} else {
message.success(log?.errorMessage || `钉钉同步完成,共处理 ${res.synced}`);
}
await fetchData();
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '钉钉同步失败');
} finally {
setDingSyncLoading(false);
}
};
const handleExport = () => {
const baseURL = import.meta.env.PROD
? '/api'
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
const token = localStorage.getItem('token');
const token = useUserStore.getState().token;
const params = new URLSearchParams();
if (searchName) params.set('name', searchName);
if (filterStatus) params.set('status', filterStatus);
@@ -469,7 +507,13 @@ const StudentsPage: React.FC = () => {
const columns = useMemo(
() => [
{ title: 'ID', dataIndex: 'id', width: 70 },
{
title: '序号',
key: 'index',
width: 70,
render: (_: unknown, __: unknown, index: number) =>
(pageInfo.current - 1) * pageInfo.pageSize + index + 1,
},
{
title: '姓名',
dataIndex: 'name',
@@ -741,6 +785,7 @@ const StudentsPage: React.FC = () => {
saveCell,
hasPermission,
canChooseOrganization,
pageInfo,
],
);
@@ -904,6 +949,11 @@ const StudentsPage: React.FC = () => {
</Button>
) : null}
{!showArchived && canSyncDingTalk ? (
<Button icon={<SyncOutlined />} loading={dingSyncLoading} onClick={handleDingTalkSync}>
</Button>
) : null}
<PermissionButton
permission="student:view"
icon={<DownloadOutlined />}
@@ -920,6 +970,23 @@ const StudentsPage: React.FC = () => {
</PermissionButton>
</Space>
</div>
{selectedRowKeys.length > 0 ? (
<Alert
type="info"
showIcon
style={{ marginBottom: 12 }}
title={
<span>
<strong style={{ color: '#1677ff' }}>{selectedRowKeys.length}</strong>
</span>
}
action={
<Button size="small" type="link" onClick={() => setSelectedRowKeys([])}>
</Button>
}
/>
) : null}
<Alert
showIcon
type="warning"
@@ -940,9 +1007,12 @@ const StudentsPage: React.FC = () => {
scroll={{ x: 1410 }}
pagination={{
defaultPageSize: 15,
current: pageInfo.current,
pageSize: pageInfo.pageSize,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
onChange: (current, pageSize) => setPageInfo({ current, pageSize }),
}}
rowClassName={(record: any) => (record.status === 'archived' ? 'archived-row' : '')}
rowSelection={{