Files
waooplus/src/app/[locale]/workspace/asset-hub/components/AddLocationModal.tsx

229 lines
11 KiB
TypeScript

'use client'
import { logError as _ulogError } from '@/lib/logging/core'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { ART_STYLES } from '@/lib/constants'
import { useAiDesignLocation, useCreateAssetHubLocation } from '@/lib/query/hooks'
import { useImageGenerationCount } from '@/lib/image-generation/use-image-generation-count'
import TaskStatusInline from '@/components/task/TaskStatusInline'
import { resolveTaskPresentationState } from '@/lib/task/presentation'
import { AppIcon } from '@/components/ui/icons'
import type { LocationAvailableSlot } from '@/lib/location-available-slots'
interface AddLocationModalProps {
folderId: string | null
onClose: () => void
onSuccess: () => void
}
// 内联 SVG 图标
const XMarkIcon = ({ className }: { className?: string }) => (
<AppIcon name="close" className={className} />
)
const SparklesIcon = ({ className }: { className?: string }) => (
<AppIcon name="sparklesAlt" className={className} />
)
export function AddLocationModal({ folderId, onClose, onSuccess }: AddLocationModalProps) {
const t = useTranslations('assetHub')
// 表单字段
const [name, setName] = useState('')
const [summary, setSummary] = useState('')
const [aiInstruction, setAiInstruction] = useState('')
const [artStyle, setArtStyle] = useState('american-comic')
const [availableSlots, setAvailableSlots] = useState<LocationAvailableSlot[]>([])
const aiDesignMutation = useAiDesignLocation()
const createLocationMutation = useCreateAssetHubLocation()
const { count: locationGenerationCount } = useImageGenerationCount('location')
const isSubmitting = createLocationMutation.isPending
const isAiDesigning = aiDesignMutation.isPending
const aiDesigningState = isAiDesigning
? resolveTaskPresentationState({
phase: 'processing',
intent: 'generate',
resource: 'image',
hasOutput: false,
})
: null
const submittingState = isSubmitting
? resolveTaskPresentationState({
phase: 'processing',
intent: 'generate',
resource: 'image',
hasOutput: false,
})
: null
// AI 设计描述
const handleAiDesign = async () => {
if (!aiInstruction.trim()) return
try {
const data = await aiDesignMutation.mutateAsync(aiInstruction.trim())
setSummary(data.prompt || '')
setAvailableSlots(Array.isArray(data.availableSlots) ? data.availableSlots : [])
setAiInstruction('')
} catch (error) {
_ulogError('AI设计失败:', error)
}
}
// 提交
const handleSubmit = async () => {
if (!name.trim() || !summary.trim()) return
try {
await createLocationMutation.mutateAsync({
name: name.trim(),
summary: summary.trim(),
folderId,
artStyle,
count: locationGenerationCount,
availableSlots,
})
onSuccess()
} catch (error) {
_ulogError('创建场景失败:', error)
}
}
return (
<div className="fixed inset-0 glass-overlay flex items-center justify-center z-50 p-4">
<div className="glass-surface-modal max-w-lg w-full max-h-[85vh] overflow-hidden flex flex-col">
<div className="p-6 overflow-y-auto app-scrollbar flex-1 min-h-0">
{/* 标题 */}
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-[var(--glass-text-primary)]">
{t('modal.newLocation')}
</h3>
<button
onClick={onClose}
className="glass-btn-base glass-btn-soft h-8 w-8 rounded-full flex items-center justify-center text-[var(--glass-text-tertiary)] hover:text-[var(--glass-text-secondary)]"
>
<XMarkIcon className="w-5 h-5" />
</button>
</div>
<div className="space-y-5">
{/* AI 设计区域 */}
<div className="glass-surface-soft border border-[var(--glass-stroke-base)] rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--glass-text-primary)]">
<SparklesIcon className="w-4 h-4" />
<span>{t('modal.aiDesign')}</span>
</div>
<div className="flex gap-2">
<input
type="text"
value={aiInstruction}
onChange={(e) => setAiInstruction(e.target.value)}
placeholder={t('modal.aiDesignLocationPlaceholder')}
className="glass-input-base flex-1 px-3 py-2 text-sm"
disabled={isAiDesigning}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleAiDesign()
}
}}
/>
<button
onClick={handleAiDesign}
disabled={isAiDesigning || !aiInstruction.trim()}
className="glass-btn-base glass-btn-tone-info px-4 py-2 rounded-lg text-sm"
>
{isAiDesigning ? (
<TaskStatusInline state={aiDesigningState} className="text-white [&>span]:text-white [&_svg]:text-white" />
) : (
<>
<SparklesIcon className="w-4 h-4" />
<span>{t('modal.generate')}</span>
</>
)}
</button>
</div>
<p className="glass-field-hint">
{t('modal.aiDesignLocationTip')}
</p>
</div>
{/* 场景名称 */}
<div className="space-y-2">
<label className="glass-field-label block">
{t('modal.locationNameLabel')}
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t('modal.locationNamePlaceholder')}
className="glass-input-base w-full px-3 py-2 text-sm"
/>
</div>
{/* 风格选择 */}
<div className="space-y-2">
<label className="glass-field-label block">
</label>
<div className="grid grid-cols-2 gap-2">
{ART_STYLES.map((style) => (
<button
key={style.value}
type="button"
onClick={() => setArtStyle(style.value)}
className={`glass-btn-base px-3 py-2 rounded-lg text-sm border flex items-center justify-start transition-all ${artStyle === style.value
? 'glass-btn-tone-info border-[var(--glass-stroke-focus)]'
: 'glass-btn-soft border-[var(--glass-stroke-base)] text-[var(--glass-text-secondary)] hover:border-[var(--glass-stroke-strong)]'
}`}
>
<span>{style.label}</span>
</button>
))}
</div>
</div>
{/* 场景描述 */}
<div className="space-y-2">
<label className="glass-field-label block">
{t('modal.locationSummaryLabel')}
</label>
<textarea
value={summary}
onChange={(e) => setSummary(e.target.value)}
placeholder={t('modal.locationSummaryPlaceholder')}
className="glass-textarea-base w-full h-40 px-3 py-2 text-sm resize-none"
/>
</div>
</div>
{/* 按钮区 */}
<div className="flex gap-3 justify-end mt-6 pt-4 border-t border-[var(--glass-stroke-base)]">
<button
onClick={onClose}
className="glass-btn-base glass-btn-secondary px-4 py-2 rounded-lg text-sm"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
onClick={handleSubmit}
disabled={isSubmitting || !name.trim() || !summary.trim()}
className="glass-btn-base glass-btn-primary px-4 py-2 rounded-lg text-sm"
>
{isSubmitting ? (
<TaskStatusInline state={submittingState} className="text-white [&>span]:text-white [&_svg]:text-white" />
) : (
<span>{t('modal.addLocation')}</span>
)}
</button>
</div>
</div>
</div>
</div>
)
}