ux: 热点问题管理改为列表式编辑,支持逐条增删改+批量粘贴
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { toast } from 'sonner'
|
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 { PageLayout } from '@/components/common/PageLayout'
|
||||||
import { EmptyState } from '@/components/common/EmptyState'
|
import { EmptyState } from '@/components/common/EmptyState'
|
||||||
import { LoadingState } from '@/components/common/LoadingState'
|
import { LoadingState } from '@/components/common/LoadingState'
|
||||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
@@ -160,7 +160,7 @@ export function AdminDailyTopics() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑/创建热点问题弹窗 */
|
/** 编辑/创建热点问题弹窗:列表式编辑,每条可单独修改/删除 */
|
||||||
function EditTopicsDialog({
|
function EditTopicsDialog({
|
||||||
topic,
|
topic,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -172,23 +172,43 @@ function EditTopicsDialog({
|
|||||||
}) {
|
}) {
|
||||||
const isCreate = !topic
|
const isCreate = !topic
|
||||||
const [scene, setScene] = useState(topic?.scene || 'legal')
|
const [scene, setScene] = useState(topic?.scene || 'legal')
|
||||||
const [questionsText, setQuestionsText] = useState(
|
const [questions, setQuestions] = useState<string[]>(topic?.questions || [''])
|
||||||
topic?.questions.join('\n') || '',
|
|
||||||
)
|
|
||||||
const [saving, setSaving] = useState(false)
|
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 handleSave = async () => {
|
||||||
const questions = questionsText
|
const valid = questions.map(s => s.trim()).filter(Boolean)
|
||||||
.split('\n')
|
if (valid.length === 0) {
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
if (questions.length === 0) {
|
|
||||||
toast.error('至少输入一个问题')
|
toast.error('至少输入一个问题')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await api.adminUpdateDailyTopics({ scene, questions })
|
await api.adminUpdateDailyTopics({ scene, questions: valid })
|
||||||
toast.success(isCreate ? '热点问题已添加' : '热点问题已更新')
|
toast.success(isCreate ? '热点问题已添加' : '热点问题已更新')
|
||||||
onSuccess()
|
onSuccess()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -200,17 +220,17 @@ function EditTopicsDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(o) => !o && onClose()}>
|
<Dialog open onOpenChange={(o) => !o && onClose()}>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{isCreate ? '添加热点问题' : `编辑 - ${SCENE_LABELS[topic!.scene]}`}</DialogTitle>
|
<DialogTitle>{isCreate ? '添加热点问题' : `编辑 - ${SCENE_LABELS[topic!.scene]}`}</DialogTitle>
|
||||||
<DialogDescription>每行一个问题,保存后用户端实时生效</DialogDescription>
|
<DialogDescription>保存后用户端实时生效。可逐条编辑,支持批量粘贴。</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{isCreate && (
|
{isCreate && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>场景</Label>
|
<Label>场景</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{Object.entries(SCENE_LABELS).map(([value, label]) => (
|
{Object.entries(SCENE_LABELS).map(([value, label]) => (
|
||||||
<Button
|
<Button
|
||||||
key={value}
|
key={value}
|
||||||
@@ -226,17 +246,38 @@ function EditTopicsDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="questions">问题列表(每行一个)</Label>
|
<div className="flex items-center justify-between">
|
||||||
<Textarea
|
<Label>问题列表({questions.filter(s => s.trim()).length} 条)</Label>
|
||||||
id="questions"
|
<Button size="sm" variant="ghost" onClick={handleAdd} className="h-7 text-xs">
|
||||||
value={questionsText}
|
<Plus className="mr-1 h-3.5 w-3.5" /> 添加
|
||||||
onChange={(e) => setQuestionsText(e.target.value)}
|
</Button>
|
||||||
placeholder={'每行一个问题,例如:\n新公司法下股东出资加速到期,我要补缴吗?\n配偶要求离婚,公司股权怎么分割损失最小?'}
|
</div>
|
||||||
className="min-h-[300px] scrollbar-none"
|
<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">
|
<button
|
||||||
{questionsText.split('\n').filter(s => s.trim()).length} 个问题
|
onClick={() => handleDelete(i)}
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user