54 lines
6.0 KiB
TypeScript
54 lines
6.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Download, RotateCcw, Save, Upload } from 'lucide-react'
|
|
import type { DetailExportFormat, DetailScreen } from '@/lib/detail-generator/types'
|
|
import DetailCanvas from './DetailCanvas'
|
|
import { assetUrl } from './client'
|
|
import { detailButtonClass, detailFieldClass, detailPrimaryClass } from './DetailProductForm'
|
|
|
|
export default function DetailScreenEditor({ screens, busy, dirty, complete, configured, onChange, onUnsavedEdit, onPendingTextChange, onSave, onReplaceImage, onRegenerate, onExport }: {
|
|
screens: DetailScreen[]
|
|
busy: boolean
|
|
dirty: boolean
|
|
complete: boolean
|
|
configured: boolean
|
|
onChange: (screen: DetailScreen) => void
|
|
onUnsavedEdit: () => void
|
|
onPendingTextChange: (screenId: string, layerId: string, text: string) => void
|
|
onSave: (screen: DetailScreen) => void
|
|
onReplaceImage: (screen: DetailScreen, file: File) => void
|
|
onRegenerate: (screenId: string) => void
|
|
onExport: (format: DetailExportFormat) => void
|
|
}) {
|
|
const [selectedId, setSelectedId] = useState('')
|
|
const [format, setFormat] = useState<DetailExportFormat>('png')
|
|
const screen = screens.find((item) => item.id === selectedId) || screens[0]
|
|
if (!screen) return <p role="status" className="py-10 text-center text-sm text-[#66758b]">等待生成图片…</p>
|
|
const changeText = (id: string, value: string) => {
|
|
const index = screen.texts.findIndex((layer) => layer.id === id)
|
|
if (index < 0 || screen.texts[index].text === value) return
|
|
onChange({ ...screen, ...(index === 0 ? { title: value } : index === 1 ? { body: value } : {}), texts: screen.texts.map((layer) => layer.id === id ? { ...layer, text: value } : layer) })
|
|
}
|
|
return <div>
|
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2"><h2 className="text-base font-semibold">详情页</h2><span className="text-sm tabular-nums text-[#66758b]">{screens.length} 屏</span></div>
|
|
<div className="flex min-w-0 flex-1 items-center justify-end gap-2 sm:flex-none"><select aria-label="导出格式" className={`${detailFieldClass} !w-auto min-w-0 max-w-[148px] flex-1`} disabled={busy} value={format} onChange={(event) => setFormat(event.target.value as DetailExportFormat)}><option value="png">分屏 PNG</option><option value="jpg">分屏 JPG</option><option value="long-png">完整长图</option><option value="psd">分层 PSD</option></select><button type="button" className={detailPrimaryClass} disabled={busy || dirty || !complete} onClick={() => onExport(format)}><Download className="h-4 w-4" aria-hidden="true" />导出</button></div>
|
|
</div>
|
|
<div className="flex flex-col gap-4 xl:flex-row">
|
|
<div aria-label="页面列表" className="flex gap-2 overflow-x-auto pb-2 xl:w-24 xl:shrink-0 xl:flex-col xl:overflow-y-auto xl:pb-0">
|
|
{screens.map((item, index) => <button type="button" key={item.id} aria-current={screen.id === item.id ? 'true' : undefined} aria-label={`编辑第 ${index + 1} 屏:${item.title}`} disabled={busy || (dirty && item.id !== screen.id)} onClick={() => setSelectedId(item.id)} className={`flex min-h-11 w-20 shrink-0 flex-col gap-1 rounded-lg border p-1.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] disabled:opacity-50 ${screen.id === item.id ? 'border-[#1677ff] bg-[#edf5ff]' : 'border-[#e2e8f0] bg-white hover:bg-[#f8fafc]'}`}>
|
|
{item.image ? <img src={assetUrl(item.image.assetId)} alt="" className="h-16 w-full rounded object-cover" /> : <span className="flex h-16 items-center justify-center text-xs text-[#66758b]">暂无图片</span>}<span className="w-full truncate px-1 text-xs text-[#344054]">{index + 1}. {item.title}</span>
|
|
</button>)}
|
|
</div>
|
|
<div className="min-w-0 flex-1 bg-[#eef2f6] p-3 sm:p-5"><DetailCanvas screen={screen} disabled={busy} onTextChange={changeText} onUnsavedEdit={onUnsavedEdit} onPendingTextChange={(layerId, text) => onPendingTextChange(screen.id, layerId, text)} /></div>
|
|
<div className="w-full space-y-4 xl:w-64 xl:shrink-0">
|
|
<div className="flex flex-wrap items-center gap-2"><button type="button" className={`${detailButtonClass} flex-1`} disabled={busy || !dirty} onClick={() => onSave(screen)}><Save className="h-4 w-4" aria-hidden="true" />保存修改</button><span role="status" className="shrink-0 whitespace-nowrap text-xs text-[#66758b]">{dirty ? '有未保存修改' : '已保存'}</span></div>
|
|
{screen.texts.map((layer, index) => <label key={layer.id} className="block space-y-1.5 text-sm font-medium">{index === 0 ? '标题' : index === 1 ? '正文' : `文字 ${index + 1}`}<textarea className={`${detailFieldClass} ${index === 0 ? 'min-h-20' : 'min-h-28'} resize-y font-normal`} disabled={busy} maxLength={index === 0 ? 120 : 1800} value={layer.text} onChange={(event) => changeText(layer.id, event.target.value)} /></label>)}
|
|
<details><summary className="cursor-pointer text-sm text-[#4f6077]">画面要求</summary><textarea aria-label="当前屏画面要求" className={`${detailFieldClass} mt-2 min-h-24 resize-y`} disabled={busy} value={screen.imagePrompt} maxLength={4000} onChange={(event) => onChange({ ...screen, imagePrompt: event.target.value })} /></details>
|
|
<div className="flex flex-wrap gap-2 border-t border-[#e2e8f0] pt-4"><label className={`${detailButtonClass} cursor-pointer focus-within:ring-2 focus-within:ring-[#1677ff] ${busy ? 'pointer-events-none opacity-50' : ''}`}><Upload className="h-4 w-4" aria-hidden="true" />替换图片<input type="file" className="sr-only" aria-label="替换当前屏图片" accept="image/png,image/jpeg,image/webp" disabled={busy} onChange={(event) => { const file = event.target.files?.[0]; event.target.value = ''; if (file) onReplaceImage(screen, file) }} /></label><button type="button" className={detailButtonClass} disabled={busy || dirty || !configured} onClick={() => onRegenerate(screen.id)}><RotateCcw className="h-4 w-4" aria-hidden="true" />重新生成此屏</button></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|