diff --git a/backend/api/v1/history.py b/backend/api/v1/history.py index 2547c83..e3dd0e6 100644 --- a/backend/api/v1/history.py +++ b/backend/api/v1/history.py @@ -102,11 +102,12 @@ async def post_failure_process( @router.get("/inherit-batch-options", response_model=InheritBatchOptionsResponse) async def get_inherit_batch_options_endpoint( exclude_batch: Optional[str] = Query(None, description="排除的批次"), + q: Optional[str] = Query(None, max_length=200, description="批次子串搜索;为空时返回最近 100 个"), db: AsyncSession = Depends(get_db), payload: dict = Depends(get_current_user), ): - """获取继承弹窗的批次选项,排除当前批次,按时间倒序。""" - return await get_inherit_batch_options(db, exclude_batch) + """获取继承弹窗的批次选项,排除当前批次,按时间倒序;支持 q 全库子串搜索。""" + return await get_inherit_batch_options(db, exclude_batch, q) @router.get("/inherit-source-options", response_model=InheritSourceOptionsResponse) diff --git a/backend/services/inherit_failure_reason_service.py b/backend/services/inherit_failure_reason_service.py index 81dc518..d306f99 100644 --- a/backend/services/inherit_failure_reason_service.py +++ b/backend/services/inherit_failure_reason_service.py @@ -60,10 +60,27 @@ async def _mysql_release_lock(db: AsyncSession, lock_name: str) -> None: await db.execute(text("SELECT RELEASE_LOCK(:n)"), {"n": lock_name}) +_LIKE_ESCAPE_CHAR = "\\" +_DEFAULT_BATCH_OPTIONS_LIMIT = 100 + + +def _like_escape_literal(value: str) -> str: + return ( + value.replace(_LIKE_ESCAPE_CHAR, _LIKE_ESCAPE_CHAR + _LIKE_ESCAPE_CHAR) + .replace("%", _LIKE_ESCAPE_CHAR + "%") + .replace("_", _LIKE_ESCAPE_CHAR + "_") + ) + + async def get_inherit_batch_options( - db: AsyncSession, exclude_batch: Optional[str] = None + db: AsyncSession, + exclude_batch: Optional[str] = None, + q: Optional[str] = None, ) -> InheritBatchOptionsResponse: - """获取继承弹窗的批次选项,排除当前批次,按时间倒序。仅返回 20 开头的批次(与历史列表默认逻辑一致)。""" + """获取继承弹窗的批次选项,排除当前批次,按时间倒序。仅返回 20 开头的批次。 + + 无关键词时返回最近 100 个去重批次;有关键词时按 start_time 子串在全库检索(仍最多 100 条)。 + """ stmt = ( select(ph.start_time) .where(ph.start_time.is_not(None)) @@ -71,10 +88,14 @@ async def get_inherit_batch_options( .where(ph.start_time.like("20%")) .distinct() .order_by(ph.start_time.desc()) - .limit(100) + .limit(_DEFAULT_BATCH_OPTIONS_LIMIT) ) if exclude_batch and str(exclude_batch).strip(): stmt = stmt.where(ph.start_time != exclude_batch.strip()) + q_stripped = str(q).strip() if q is not None else "" + if q_stripped: + pattern = f"%{_like_escape_literal(q_stripped)}%" + stmt = stmt.where(ph.start_time.like(pattern, escape=_LIKE_ESCAPE_CHAR)) result = await db.execute(stmt) batches = [r[0] for r in result.all() if r[0]] return InheritBatchOptionsResponse(batches=batches) diff --git a/frontend/src/pages/history/HistoryPage.tsx b/frontend/src/pages/history/HistoryPage.tsx index 87f120c..16dad4f 100644 --- a/frontend/src/pages/history/HistoryPage.tsx +++ b/frontend/src/pages/history/HistoryPage.tsx @@ -315,6 +315,8 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { const [inheritSubmitLoading, setInheritSubmitLoading] = useState(false); /** 继承提交进行中时用于中止 HTTP,避免点取消后仍占服务端 GET_LOCK */ const inheritAbortRef = useRef(null); + /** 源批次远程搜索防抖 */ + const inheritBatchSearchTimerRef = useRef | null>(null); const [oneClickLoading, setOneClickLoading] = useState(false); const [bugNotifyLoading, setBugNotifyLoading] = useState(false); const [reportModalVisible, setReportModalVisible] = useState(false); @@ -955,6 +957,32 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { processForm.resetFields(); }; + const fetchInheritBatchOptions = async (q?: string, showError = false) => { + setInheritBatchOptionsLoading(true); + try { + const res = await historyApi.inheritBatchOptions(currentBatch, q); + setInheritBatchOptions(res.batches ?? []); + } catch (e: unknown) { + setInheritBatchOptions([]); + if (showError) { + const err = e as { response?: { data?: { detail?: string } }; message?: string }; + message.error(err?.response?.data?.detail || err?.message || "获取批次选项失败"); + } + } finally { + setInheritBatchOptionsLoading(false); + } + }; + + const handleInheritBatchSearch = (input: string) => { + if (inheritBatchSearchTimerRef.current) { + clearTimeout(inheritBatchSearchTimerRef.current); + } + inheritBatchSearchTimerRef.current = setTimeout(() => { + const trimmed = input.trim(); + void fetchInheritBatchOptions(trimmed || undefined); + }, 300); + }; + const openInheritModal = async () => { if (!processBtnEnabled) return; inheritAbortRef.current?.abort(); @@ -969,16 +997,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { source_pfr_id: undefined, }); if (showBatchDimension) { - setInheritBatchOptionsLoading(true); - try { - const res = await historyApi.inheritBatchOptions(currentBatch); - setInheritBatchOptions(res.batches ?? []); - } catch (e: unknown) { - const err = e as { response?: { data?: { detail?: string } }; message?: string }; - message.error(err?.response?.data?.detail || err?.message || "获取批次选项失败"); - } finally { - setInheritBatchOptionsLoading(false); - } + await fetchInheritBatchOptions(undefined, true); } else { setInheritSourceOptionsLoading(true); try { @@ -1051,6 +1070,10 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { const handleInheritModalCancel = () => { inheritAbortRef.current?.abort(); inheritAbortRef.current = null; + if (inheritBatchSearchTimerRef.current) { + clearTimeout(inheritBatchSearchTimerRef.current); + inheritBatchSearchTimerRef.current = null; + } setInheritSubmitLoading(false); setInheritModalVisible(false); inheritForm.resetFields(); @@ -2316,15 +2339,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { inheritForm.setFieldValue("source_pfr_id", undefined); setInheritSourceRecords([]); if (all.inherit_mode === "batch" && showBatchDimension) { - setInheritBatchOptionsLoading(true); - try { - const res = await historyApi.inheritBatchOptions(currentBatch); - setInheritBatchOptions(res.batches ?? []); - } catch { - setInheritBatchOptions([]); - } finally { - setInheritBatchOptionsLoading(false); - } + await fetchInheritBatchOptions(); } else { fetchInheritSourceOptions(); } @@ -2367,13 +2382,17 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { rules={[{ required: true, message: "请选择源批次" }]} >