feat: 钉钉学生同步支持仅绑定手机号及学生列表修复
This commit is contained in:
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user