初始化模版工程
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/utils/cn'
|
||||
import type { SlideQuestion } from '../../../types'
|
||||
|
||||
interface SlideFormInteractionProps {
|
||||
questions?: SlideQuestion[]
|
||||
disabled?: boolean
|
||||
messageTime?: string
|
||||
onSendMessage?: (content: string) => void
|
||||
}
|
||||
|
||||
const COUNTDOWN_SECONDS = 30
|
||||
|
||||
export const SlideFormInteraction = memo(
|
||||
({
|
||||
questions,
|
||||
disabled,
|
||||
messageTime,
|
||||
onSendMessage,
|
||||
}: SlideFormInteractionProps) => {
|
||||
const [answers, setAnswers] = useState<Record<number, string>>({})
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [seconds, setSeconds] = useState(messageTime ? COUNTDOWN_SECONDS : 0)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
// 初始化已有答案
|
||||
useEffect(() => {
|
||||
if (!questions) return
|
||||
const initial: Record<number, string> = {}
|
||||
questions.forEach((q, i) => {
|
||||
if (q.answer != null) initial[i] = q.answer
|
||||
})
|
||||
setAnswers(initial)
|
||||
}, [questions])
|
||||
|
||||
// 倒计时
|
||||
useEffect(() => {
|
||||
if (!messageTime || seconds <= 0) return
|
||||
timerRef.current = setInterval(() => {
|
||||
setSeconds(s => {
|
||||
if (s <= 1) {
|
||||
clearInterval(timerRef.current!)
|
||||
handleSubmit()
|
||||
return 0
|
||||
}
|
||||
return s - 1
|
||||
})
|
||||
}, 1000)
|
||||
return () => { if (timerRef.current) clearInterval(timerRef.current) }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messageTime])
|
||||
|
||||
function handleSubmit() {
|
||||
if (submitted) return
|
||||
setSubmitted(true)
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
|
||||
const content = {
|
||||
questions: questions?.map((q, idx) => ({
|
||||
...q,
|
||||
answer: answers[idx] ?? '',
|
||||
})),
|
||||
expected_user_action: 'slide_message_reply',
|
||||
}
|
||||
onSendMessage?.(JSON.stringify(content))
|
||||
}
|
||||
|
||||
if (!questions?.length) return null
|
||||
|
||||
const isDisabled = disabled || submitted || seconds === 0
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('mt-2 w-full overflow-hidden rounded-xl border border-border')}
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(58% 12% at 97% 1%, var(--page-glow-2) 0%, rgba(0,0,0,0) 100%), radial-gradient(108% 26% at 10% -4%, var(--page-glow-1) 0%, rgba(0,0,0,0) 100%), var(--card)',
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center gap-1 px-3 py-2.5 font-medium text-primary">
|
||||
<span className="text-sm">✦ 补充信息</span>
|
||||
</div>
|
||||
|
||||
{/* 问题列表 */}
|
||||
<div className="px-3 pb-3 flex flex-col gap-3">
|
||||
{questions.map((q, idx) => (
|
||||
<div key={idx} className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-foreground">{q.question}</label>
|
||||
|
||||
{q.type === 'input' || !q.options?.length ? (
|
||||
<Input
|
||||
value={answers[idx] ?? ''}
|
||||
onChange={e =>
|
||||
setAnswers(prev => ({ ...prev, [idx]: e.target.value }))
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{q.options.map(opt => {
|
||||
const selected =
|
||||
q.type === 'multiple'
|
||||
? (answers[idx] ?? '').split(',').includes(opt)
|
||||
: answers[idx] === opt
|
||||
return (
|
||||
<button
|
||||
key={opt}
|
||||
type="button"
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
if (q.type === 'multiple') {
|
||||
const current = (answers[idx] ?? '').split(',').filter(Boolean)
|
||||
const next = selected
|
||||
? current.filter(v => v !== opt)
|
||||
: [...current, opt]
|
||||
setAnswers(prev => ({ ...prev, [idx]: next.join(',') }))
|
||||
} else {
|
||||
setAnswers(prev => ({ ...prev, [idx]: opt }))
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-lg border px-3 py-1.5 text-sm transition-colors',
|
||||
selected
|
||||
? 'border-primary/40 bg-primary/10 text-primary'
|
||||
: 'border-border/60 bg-secondary/60 text-foreground hover:bg-accent',
|
||||
isDisabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 底部:倒计时 + 提交 */}
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{submitted || seconds === 0 ? (
|
||||
'已自动运行'
|
||||
) : (
|
||||
<>
|
||||
<span className="text-primary">{seconds}s</span> 后自动运行
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={isDisabled}
|
||||
className="text-sm h-7"
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user