初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -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>
)
},
)