"use client"; import { useState } from "react"; import { AlertCircle, CheckCircle2, GripVerticalIcon, Loader2Icon, PencilIcon, PlusIcon, TagIcon, Trash2Icon, } from "lucide-react"; 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, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { useAdminRecommendedQuestions, useCreateRecommendedQuestion, useDeleteRecommendedQuestion, useReorderRecommendedQuestions, useUpdateRecommendedQuestion, } from "@/core/recommended-questions/hooks"; import type { RecommendedQuestion } from "@/core/recommended-questions"; type DialogState = | { mode: "add" } | { mode: "edit"; question: RecommendedQuestion } | null; /** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */ const successToastDarkOptions = { classNames: { toast: "!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md", title: "!text-[#FFFFFF]", description: "!text-[#FFFFFF]", }, icon: , } as const; /** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */ const errorToastDarkOptions = { classNames: { toast: "!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md", title: "!text-[#FFFFFF]", description: "!text-[#FFFFFF]", }, icon: , } as const; export default function RecommendedQuestionsPage({ embedded = false, }: { embedded?: boolean; } = {}) { const { theme } = useTheme(); const { questions, isLoading, error } = useAdminRecommendedQuestions(); const createMutation = useCreateRecommendedQuestion(); const updateMutation = useUpdateRecommendedQuestion(); const deleteMutation = useDeleteRecommendedQuestion(); const reorderMutation = useReorderRecommendedQuestions(); const [dialog, setDialog] = useState(null); const [formTitle, setFormTitle] = useState(""); const [formContent, setFormContent] = useState(""); const [deleteTarget, setDeleteTarget] = useState(null); const [tagTarget, setTagTarget] = useState(null); // drag state 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(""); setDialog({ mode: "add" }); } function openEdit(q: RecommendedQuestion) { setFormTitle(q.title); setFormContent(q.content); setDialog({ mode: "edit", question: q }); } function closeDialog() { setDialog(null); } async function handleSave() { const title = formTitle.trim(); const content = formContent.trim(); if (!title || !content) { toast.error("标题和内容不能为空", darkToastOpts.error); return; } try { if (dialog?.mode === "add") { await createMutation.mutateAsync({ title, content, sort_order: questions.length, }); toast.success("问题已添加", darkToastOpts.success); } else if (dialog?.mode === "edit") { await updateMutation.mutateAsync({ id: dialog.question.id, body: { title, content }, }); toast.success("问题已更新", darkToastOpts.success); } closeDialog(); } catch (e) { toast.error( e instanceof Error ? e.message : "操作失败", darkToastOpts.error, ); } } async function handleDelete(q: RecommendedQuestion) { try { await deleteMutation.mutateAsync(q.id); setDeleteTarget(null); toast.success("问题已删除", darkToastOpts.success); } catch (e) { toast.error( e instanceof Error ? e.message : "删除失败", darkToastOpts.error, ); } } function handleDragStart(index: number) { setDragIndex(index); } function handleDragOver(e: React.DragEvent, index: number) { e.preventDefault(); setDragOverIndex(index); } async function handleDrop(toIndex: number) { if (dragIndex === null || dragIndex === toIndex) { setDragIndex(null); setDragOverIndex(null); return; } const next = [...questions]; const [moved] = next.splice(dragIndex, 1); next.splice(toIndex, 0, moved!); setDragIndex(null); setDragOverIndex(null); try { await reorderMutation.mutateAsync(next.map((q) => q.id)); } catch { toast.error("排序保存失败", darkToastOpts.error); } } const isSaving = createMutation.isPending || updateMutation.isPending || deleteMutation.isPending || reorderMutation.isPending; const darkToastOpts = theme === "dark" ? { success: successToastDarkOptions, error: errorToastDarkOptions } : { success: undefined, error: undefined }; return (

推荐问题配置

管理首页展示的推荐问题,点击后直接发送到对话。拖拽可调整顺序。

{!isLoading && !error && questions.length > 0 && ( )} {isLoading ? (
) : error ? (

加载失败:{error.message}

) : questions.length === 0 ? (

暂无推荐问题

) : visibleQuestions.length === 0 ? (

没有匹配的推荐问题

) : (
{visibleQuestions.map((q, index) => (
!isFiltering && handleDragStart(index)} onDragOver={(e) => !isFiltering && handleDragOver(e, index)} onDrop={() => !isFiltering && handleDrop(index)} onDragEnd={() => { setDragIndex(null); setDragOverIndex(null); }} className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors dark:bg-[#022248] dark:!border-[#213D5E] ${ dragOverIndex === index && dragIndex !== index ? "border-blue-400 bg-blue-50 dark:border-[#2580FF] dark:bg-[#0D2D54]" : "border-[#e4e4e7]" }`} >
{q.title}
{q.content}
{(q.tags?.length ?? 0) > 0 && ( )}
))}
)} {/* 添加 / 编辑弹窗 */} !o && closeDialog()}> {dialog?.mode === "add" ? "添加推荐问题" : "编辑推荐问题"}

卡片标题

setFormTitle(e.target.value)} />

发送内容