Files
clawshow-admin/src/components/EmojiForm.tsx
2026-03-19 18:57:15 +08:00

260 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { useEmojis } from '@/hooks/useEmojis'
import {
AVATAR_EMOJI_RESULTS,
AVATAR_EMOJI_TYPES,
supportsAvatarEmojiResult,
type AvatarEmojiResult,
type AvatarEmojiType,
} from '@/lib/protocol'
import { EmojiMedia } from './EmojiMedia'
interface EmojiFormProps {
avatarId: string
onSuccess: () => void
onCancel: () => void
emojiId?: string
initialLabel?: string
initialUrl?: string
initialDescription?: string
initialTriggerType?: AvatarEmojiType
initialTriggerResult?: AvatarEmojiResult | ''
}
export function EmojiForm({
avatarId,
onSuccess,
onCancel,
emojiId,
initialLabel = '',
initialUrl = '',
initialDescription = '',
initialTriggerType = 'message',
initialTriggerResult = '',
}: EmojiFormProps) {
const { createEmoji, updateEmoji } = useEmojis()
const [label, setLabel] = useState(initialLabel)
const [url, setUrl] = useState(initialUrl)
const [description, setDescription] = useState(initialDescription)
const [triggerType, setTriggerType] = useState<AvatarEmojiType>(initialTriggerType)
const [triggerResult, setTriggerResult] = useState<AvatarEmojiResult | ''>(initialTriggerResult)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const resultEnabled = supportsAvatarEmojiResult(triggerType)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!label.trim()) {
setError('请输入表情标签')
return
}
if (!url.trim()) {
setError('请输入表情URL')
return
}
// 简单的URL验证
try {
new URL(url)
} catch {
setError('请输入有效的URL')
return
}
try {
setSubmitting(true)
setError(null)
if (emojiId) {
await updateEmoji(emojiId, {
label: label.trim(),
url: url.trim(),
description: description.trim() || undefined,
trigger_type: triggerType,
trigger_result: resultEnabled
? (triggerResult || null)
: null,
})
} else {
await createEmoji(
avatarId,
label.trim(),
url.trim(),
description.trim() || undefined,
undefined,
triggerType,
resultEnabled ? (triggerResult || undefined) : undefined,
)
}
onSuccess()
} catch (err) {
setError(err instanceof Error ? err.message : '添加失败')
} finally {
setSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
)}
<div>
<label
htmlFor="label"
className="block text-sm font-medium text-slate-700"
>
*
</label>
<input
id="label"
type="text"
value={label}
onChange={e => setLabel(e.target.value)}
placeholder="例如:开心、难过、生气"
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
disabled={submitting}
/>
</div>
<div>
<label
htmlFor="url"
className="block text-sm font-medium text-slate-700"
>
URL *
</label>
<input
id="url"
type="url"
value={url}
onChange={e => setUrl(e.target.value)}
placeholder="https://example.com/emoji.png"
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
disabled={submitting}
/>
<p className="mt-1 text-xs text-slate-500">
URL地址
</p>
</div>
<div>
<label
htmlFor="triggerType"
className="block text-sm font-medium text-slate-700"
>
*
</label>
<select
id="triggerType"
value={triggerType}
onChange={e => {
const nextType = e.target.value as AvatarEmojiType
setTriggerType(nextType)
if (!supportsAvatarEmojiResult(nextType)) {
setTriggerResult('')
}
}}
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
disabled={submitting}
>
{AVATAR_EMOJI_TYPES.map(type => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
{resultEnabled && (
<div>
<label
htmlFor="triggerResult"
className="block text-sm font-medium text-slate-700"
>
</label>
<select
id="triggerResult"
value={triggerResult}
onChange={e => setTriggerResult(e.target.value as AvatarEmojiResult | '')}
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
disabled={submitting}
>
<option value=""></option>
{AVATAR_EMOJI_RESULTS.map(result => (
<option key={result} value={result}>
{result}
</option>
))}
</select>
<p className="mt-1 text-xs text-slate-500">
</p>
</div>
)}
<div>
<label
htmlFor="description"
className="block text-sm font-medium text-slate-700"
>
</label>
<textarea
id="description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="简单描述这个表情..."
rows={2}
className="mt-1 w-full resize-none rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
disabled={submitting}
/>
</div>
{url && (
<div>
<label className="block text-sm font-medium text-slate-700">
</label>
<div className="mt-2 inline-block rounded-lg border border-slate-200 bg-slate-50 p-4">
<EmojiMedia
url={url}
alt="预览"
className="h-24 w-24 object-contain"
fallbackClassName="h-24 w-24 object-contain"
/>
</div>
</div>
)}
<div className="flex gap-2">
<button
type="submit"
disabled={submitting}
className="flex-1 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{submitting ? (emojiId ? '保存中...' : '添加中...') : (emojiId ? '保存' : '添加')}
</button>
<button
type="button"
onClick={onCancel}
disabled={submitting}
className="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:opacity-50"
>
</button>
</div>
</form>
)
}