From 8fd1606424c8a504953ab3ccc856840f3d65a9dd Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Thu, 4 Jun 2026 20:09:38 +0800 Subject: [PATCH] add function of username search in admin vocabulary scoreaudit --- server/routes/admin.ts | 14 ++++++++++++++ src/api/admin.ts | 2 +- src/components/AdminVocabularyScoreAudit.tsx | 14 ++++++++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/server/routes/admin.ts b/server/routes/admin.ts index 307450f..cd6c4d3 100644 --- a/server/routes/admin.ts +++ b/server/routes/admin.ts @@ -70,6 +70,7 @@ const INVALIDATING_VOCABULARY_RISK_FLAGS = [ ]; const getObjectIdString = (value: any): string => value?._id?.toString?.() || value?.toString?.() || ''; +const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const parseAdminDateRange = (source: Record) => { const start = new Date(String(source.start || source.startTime || '')); @@ -544,6 +545,17 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => { } const { start, end } = range as { start: Date; end: Date }; + const studentNo = String(req.query.studentNo || '').trim(); + const userMatchStages = studentNo + ? [{ + $match: { + 'user.username': { + $regex: escapeRegex(studentNo), + $options: 'i' + } + } + }] + : []; const validPassItems = await VocabularyTestRecord.aggregate([ { $match: { @@ -575,6 +587,7 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => { } }, { $unwind: { path: '$user', preserveNullAndEmptyArrays: true } }, + ...userMatchStages, { $project: { _id: 0, @@ -630,6 +643,7 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => { } }, { $unwind: { path: '$user', preserveNullAndEmptyArrays: true } }, + ...userMatchStages, { $project: { _id: 0, diff --git a/src/api/admin.ts b/src/api/admin.ts index 3baaeae..b7c8c81 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -336,7 +336,7 @@ export const adminApi = { return api.put('/api/vocabulary/words', { words }); }, - getVocabularyPassSummary: async (params: { start: string; end: string }): Promise => { + getVocabularyPassSummary: async (params: { start: string; end: string; studentNo?: string }): Promise => { return api.get('/admin/vocabulary/test-pass-summary', { params }); }, diff --git a/src/components/AdminVocabularyScoreAudit.tsx b/src/components/AdminVocabularyScoreAudit.tsx index 1401e3c..ff8398a 100644 --- a/src/components/AdminVocabularyScoreAudit.tsx +++ b/src/components/AdminVocabularyScoreAudit.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Button, Card, Collapse, DatePicker, Descriptions, message, Modal, Popconfirm, Space, Statistic, Table, Tag, Typography } from 'antd'; +import { Button, Card, Collapse, DatePicker, Descriptions, Input, message, Modal, Popconfirm, Space, Statistic, Table, Tag, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { CheckCircleOutlined, DeleteOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons'; import { @@ -93,6 +93,7 @@ const getAnswerRows = (record: VocabularyAuditRecordDetail): VocabularyAuditAnsw const AdminVocabularyScoreAudit: React.FC = () => { const [range, setRange] = useState<[string, string] | null>(null); + const [studentNo, setStudentNo] = useState(''); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(false); const [clearingUserId, setClearingUserId] = useState(null); @@ -112,7 +113,8 @@ const AdminVocabularyScoreAudit: React.FC = () => { setLoading(true); const response = await adminApi.getVocabularyPassSummary({ start: range[0], - end: range[1] + end: range[1], + studentNo: studentNo.trim() || undefined }); setSummary(response); } catch (error) { @@ -550,6 +552,14 @@ const AdminVocabularyScoreAudit: React.FC = () => { setRange([toIsoString(dates[0]), toIsoString(dates[1])]); }} /> + setStudentNo(event.target.value)} + onPressEnter={fetchSummary} + style={{ width: 220 }} + />