ux: 热点问题管理改为列表式编辑,支持逐条增删改+批量粘贴
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

This commit is contained in:
selfrelease
2026-09-01 10:00:03 +08:00
parent 06f046de1e
commit 944ba177bb
+66 -25
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react'
import { toast } from 'sonner'
import { Sparkles, Loader2, RefreshCw, Pencil, Calendar } from 'lucide-react'
import { Sparkles, Loader2, RefreshCw, Pencil, Calendar, Plus, X } from 'lucide-react'
import { PageLayout } from '@/components/common/PageLayout'
import { EmptyState } from '@/components/common/EmptyState'
import { LoadingState } from '@/components/common/LoadingState'
@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Input } from '@/components/ui/input'
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog'
@@ -160,7 +160,7 @@ export function AdminDailyTopics() {
)
}
/** 编辑/创建热点问题弹窗 */
/** 编辑/创建热点问题弹窗:列表式编辑,每条可单独修改/删除 */
function EditTopicsDialog({
topic,
onClose,
@@ -172,23 +172,43 @@ function EditTopicsDialog({
}) {
const isCreate = !topic
const [scene, setScene] = useState(topic?.scene || 'legal')
const [questionsText, setQuestionsText] = useState(
topic?.questions.join('\n') || '',
)
const [questions, setQuestions] = useState<string[]>(topic?.questions || [''])
const [saving, setSaving] = useState(false)
/** 更新某行问题 */
const handleUpdate = (index: number, value: string) => {
setQuestions(prev => prev.map((q, i) => i === index ? value : q))
}
/** 删除某行问题 */
const handleDelete = (index: number) => {
setQuestions(prev => prev.filter((_, i) => i !== index))
}
/** 添加空行 */
const handleAdd = () => {
setQuestions(prev => [...prev, ''])
}
/** 批量粘贴:从剪贴板按行解析 */
const handlePaste = (e: React.ClipboardEvent) => {
const text = e.clipboardData.getData('text')
if (text.includes('\n') && text.trim().length > 50) {
e.preventDefault()
const lines = text.split('\n').map(s => s.trim()).filter(Boolean)
setQuestions(prev => [...prev.filter(s => s.trim()), ...lines])
}
}
const handleSave = async () => {
const questions = questionsText
.split('\n')
.map((s) => s.trim())
.filter(Boolean)
if (questions.length === 0) {
const valid = questions.map(s => s.trim()).filter(Boolean)
if (valid.length === 0) {
toast.error('至少输入一个问题')
return
}
setSaving(true)
try {
await api.adminUpdateDailyTopics({ scene, questions })
await api.adminUpdateDailyTopics({ scene, questions: valid })
toast.success(isCreate ? '热点问题已添加' : '热点问题已更新')
onSuccess()
} catch (e) {
@@ -200,17 +220,17 @@ function EditTopicsDialog({
return (
<Dialog open onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{isCreate ? '添加热点问题' : `编辑 - ${SCENE_LABELS[topic!.scene]}`}</DialogTitle>
<DialogDescription>每行一个问题,保存后用户端实时生效</DialogDescription>
<DialogDescription>保存后用户端实时生效。可逐条编辑,支持批量粘贴。</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{isCreate && (
<div className="space-y-1.5">
<Label>场景</Label>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
{Object.entries(SCENE_LABELS).map(([value, label]) => (
<Button
key={value}
@@ -226,17 +246,38 @@ function EditTopicsDialog({
)}
<div className="space-y-1.5">
<Label htmlFor="questions">问题列表(每行一个)</Label>
<Textarea
id="questions"
value={questionsText}
onChange={(e) => setQuestionsText(e.target.value)}
placeholder={'每行一个问题,例如:\n新公司法下股东出资加速到期,我要补缴吗?\n配偶要求离婚,公司股权怎么分割损失最小?'}
className="min-h-[300px] scrollbar-none"
<div className="flex items-center justify-between">
<Label>问题列表({questions.filter(s => s.trim()).length} 条)</Label>
<Button size="sm" variant="ghost" onClick={handleAdd} className="h-7 text-xs">
<Plus className="mr-1 h-3.5 w-3.5" /> 添加
</Button>
</div>
<div
className="max-h-[50vh] space-y-2 overflow-y-auto scrollbar-none rounded-md border p-2"
onPaste={handlePaste}
>
{questions.map((q, i) => (
<div key={i} className="flex items-start gap-2">
<span className="mt-2 shrink-0 text-xs tabular-nums text-muted-foreground">{i + 1}</span>
<Input
value={q}
onChange={(e) => handleUpdate(i, e.target.value)}
placeholder="输入问题..."
className="h-8 text-sm"
/>
<p className="text-xs text-muted-foreground">
{questionsText.split('\n').filter(s => s.trim()).length} 个问题
</p>
<button
onClick={() => handleDelete(i)}
className="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-red-50 hover:text-red-500"
aria-label="删除"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
))}
{questions.length === 0 && (
<p className="py-4 text-center text-xs text-muted-foreground">暂无问题,点击"添加"或粘贴</p>
)}
</div>
</div>
</div>