From 97da7dc87f6b2ebf804752441a5d9773b75a12f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Tue, 16 Jun 2026 16:52:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A8=E8=8D=90=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=94=AF=E6=8C=81=E6=8C=89=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=B9=B6=E5=9C=A8=E5=88=97=E8=A1=A8=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端列表接口为每个推荐问题附带 tags,并支持 tag_ids OR 过滤 - 前端配置页新增 TagSearchBar(标题/内容搜索 + 标签筛选),卡片展示标签 - 打标签后失效 recommended-questions 查询,列表即时刷新 - 过滤时禁用拖拽排序,避免破坏 sort_order Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/core/recommended-questions/index.ts | 4 + frontend-web/src/core/tags/hooks.ts | 1 + .../src/pages/RecommendedQuestionsPage.tsx | 56 +++++++++++-- offline-backend-20260512/backend/CLAUDE.md | 5 ++ .../gateway/routers/recommended_questions.py | 23 +++++- .../tests/test_recommended_questions_tags.py | 79 +++++++++++++++++++ 6 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 offline-backend-20260512/backend/tests/test_recommended_questions_tags.py diff --git a/frontend-web/src/core/recommended-questions/index.ts b/frontend-web/src/core/recommended-questions/index.ts index a7a5b286..40c36723 100644 --- a/frontend-web/src/core/recommended-questions/index.ts +++ b/frontend-web/src/core/recommended-questions/index.ts @@ -1,8 +1,12 @@ +import type { Tag } from "@/core/tags"; + export interface RecommendedQuestion { id: string; title: string; content: string; sort_order: number; + /** Tags assigned to this question (按岗位分发标签). */ + tags?: Tag[]; created_at?: string; updated_at?: string; } diff --git a/frontend-web/src/core/tags/hooks.ts b/frontend-web/src/core/tags/hooks.ts index 42861feb..950eb082 100644 --- a/frontend-web/src/core/tags/hooks.ts +++ b/frontend-web/src/core/tags/hooks.ts @@ -83,6 +83,7 @@ export function useSetResourceTags() { void queryClient.invalidateQueries({ queryKey: ["skills"] }); void queryClient.invalidateQueries({ queryKey: ["agents"] }); void queryClient.invalidateQueries({ queryKey: ["scheduled-tasks"] }); + void queryClient.invalidateQueries({ queryKey: ["recommended-questions"] }); }, }); } diff --git a/frontend-web/src/pages/RecommendedQuestionsPage.tsx b/frontend-web/src/pages/RecommendedQuestionsPage.tsx index 7fa85e0d..c8d76bdf 100644 --- a/frontend-web/src/pages/RecommendedQuestionsPage.tsx +++ b/frontend-web/src/pages/RecommendedQuestionsPage.tsx @@ -15,6 +15,8 @@ import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { ResourceTagDialog } from "@/components/workspace/tags/resource-tag-dialog"; +import { TagChipList } from "@/components/workspace/tags/tag-chip"; +import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar"; import { useTheme } from "@/contexts/ThemeContext"; import { Dialog, @@ -83,6 +85,25 @@ export default function RecommendedQuestionsPage({ const [dragIndex, setDragIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); + // 名称 / 标签搜索 + const [search, setSearch] = useState(""); + const [tagIds, setTagIds] = useState([]); + + const isFiltering = search.trim() !== "" || tagIds.length > 0; + const needle = search.trim().toLowerCase(); + const visibleQuestions = isFiltering + ? questions.filter((q) => { + const matchesText = + needle === "" || + q.title.toLowerCase().includes(needle) || + q.content.toLowerCase().includes(needle); + const matchesTags = + tagIds.length === 0 || + (q.tags ?? []).some((t) => tagIds.includes(t.id)); + return matchesText && matchesTags; + }) + : questions; + function openAdd() { setFormTitle(""); setFormContent(""); @@ -203,6 +224,16 @@ export default function RecommendedQuestionsPage({ + {!isLoading && !error && questions.length > 0 && ( + + )} + {isLoading ? (
@@ -219,15 +250,19 @@ export default function RecommendedQuestionsPage({ 添加第一个问题
+ ) : visibleQuestions.length === 0 ? ( +
+

没有匹配的推荐问题

+
) : (
- {questions.map((q, index) => ( + {visibleQuestions.map((q, index) => (
handleDragStart(index)} - onDragOver={(e) => handleDragOver(e, index)} - onDrop={() => handleDrop(index)} + draggable={!isFiltering} + onDragStart={() => !isFiltering && handleDragStart(index)} + onDragOver={(e) => !isFiltering && handleDragOver(e, index)} + onDrop={() => !isFiltering && handleDrop(index)} onDragEnd={() => { setDragIndex(null); setDragOverIndex(null); @@ -238,7 +273,13 @@ export default function RecommendedQuestionsPage({ : "border-[#e4e4e7]" }`} > - +
{q.title} @@ -246,6 +287,9 @@ export default function RecommendedQuestionsPage({
{q.content}
+ {(q.tags?.length ?? 0) > 0 && ( + + )}