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:
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user