feat: 推荐问题配置支持按标签搜索并在列表展示标签

- 后端列表接口为每个推荐问题附带 tags,并支持 tag_ids OR 过滤
- 前端配置页新增 TagSearchBar(标题/内容搜索 + 标签筛选),卡片展示标签
- 打标签后失效 recommended-questions 查询,列表即时刷新
- 过滤时禁用拖拽排序,避免破坏 sort_order

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-16 16:52:30 +08:00
1 parent 5182ea5d11
commit 97da7dc87f
6 files changed
+161 -7

No files matched your search

@@ -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;
}
+1
View File
@@ -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"] });
},
});
}
@@ -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<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
// 名称 / 标签搜索
const [search, setSearch] = useState("");
const [tagIds, setTagIds] = useState<string[]>([]);
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({
</Button>
</div>
{!isLoading && !error && questions.length > 0 && (
<TagSearchBar
targetType="recommended_question"
searchPlaceholder="搜索标题 / 内容"
onSearchChange={setSearch}
tagIds={tagIds}
onTagIdsChange={setTagIds}
/>
)}
{isLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2Icon className="text-muted-foreground size-6 animate-spin" />
@@ -219,15 +250,19 @@ export default function RecommendedQuestionsPage({
添加第一个问题
</Button>
</div>
) : visibleQuestions.length === 0 ? (
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm dark:text-[#7FACF2] dark:!border-[#213F62]">
<p>没有匹配的推荐问题</p>
</div>
) : (
<div className="flex flex-col gap-2 ">
{questions.map((q, index) => (
{visibleQuestions.map((q, index) => (
<div
key={q.id}
draggable
onDragStart={() => 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]"
}`}
>
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing dark:text-[#566B7F]" />
<GripVerticalIcon
className={`text-muted-foreground/40 mt-0.5 size-4 shrink-0 dark:text-[#566B7F] ${
isFiltering
? "cursor-not-allowed opacity-40"
: "cursor-grab active:cursor-grabbing"
}`}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-[#18181b] dark:text-white">
{q.title}
@@ -246,6 +287,9 @@ export default function RecommendedQuestionsPage({
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs dark:text-[#7FACF2]">
{q.content}
</div>
{(q.tags?.length ?? 0) > 0 && (
<TagChipList tags={q.tags!} className="mt-2" />
)}
</div>
<div className="flex shrink-0 items-center gap-1">
<Button