|
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
import { usePageContext, usePreviewUrl } from '../../../shared/pageContext'
|
|
import { usePageContext, usePreviewUrl } from '../../../shared/pageContext'
|
|
|
import { DashboardLayout } from '../../../layouts/DashboardLayout'
|
|
import { DashboardLayout } from '../../../layouts/DashboardLayout'
|
|
|
|
|
+import { hairstyleTemplateAssets } from '../../../config/hairstyleTemplateAssets.generated'
|
|
|
import {
|
|
import {
|
|
|
createTemplateMakeTask,
|
|
createTemplateMakeTask,
|
|
|
getTemplateAssets,
|
|
getTemplateAssets,
|
|
@@ -32,6 +33,34 @@ const EMPTY_ASSETS: TemplateAssets = {
|
|
|
backgrounds: [],
|
|
backgrounds: [],
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const STATIC_PRESET_ASSETS: TemplateAssets = {
|
|
|
|
|
+ models: hairstyleTemplateAssets
|
|
|
|
|
+ .filter((item) => item.category === 'model')
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ name: item.label,
|
|
|
|
|
+ url: item.previewUrl,
|
|
|
|
|
+ configKey: item.id,
|
|
|
|
|
+ })),
|
|
|
|
|
+ clothes: hairstyleTemplateAssets
|
|
|
|
|
+ .filter((item) => item.category === 'outfit')
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ name: item.label,
|
|
|
|
|
+ url: item.previewUrl,
|
|
|
|
|
+ configKey: item.id,
|
|
|
|
|
+ })),
|
|
|
|
|
+ backgrounds: hairstyleTemplateAssets
|
|
|
|
|
+ .filter((item) => item.category === 'background')
|
|
|
|
|
+ .slice(0, 4)
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ name: item.label,
|
|
|
|
|
+ url: item.previewUrl,
|
|
|
|
|
+ configKey: item.id,
|
|
|
|
|
+ })),
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export function TemplateFormPage() {
|
|
export function TemplateFormPage() {
|
|
|
const { locale, navigate, path, copy } = usePageContext()
|
|
const { locale, navigate, path, copy } = usePageContext()
|
|
|
const pageCopy = copy.templateFormPage
|
|
const pageCopy = copy.templateFormPage
|
|
@@ -53,7 +82,10 @@ export function TemplateFormPage() {
|
|
|
const [customModelFile, setCustomModelFile] = useState<File | null>(null)
|
|
const [customModelFile, setCustomModelFile] = useState<File | null>(null)
|
|
|
const [customOutfitFile, setCustomOutfitFile] = useState<File | null>(null)
|
|
const [customOutfitFile, setCustomOutfitFile] = useState<File | null>(null)
|
|
|
const [customBackgroundFile, setCustomBackgroundFile] = useState<File | null>(null)
|
|
const [customBackgroundFile, setCustomBackgroundFile] = useState<File | null>(null)
|
|
|
- const referencePreview = usePreviewUrl(referenceFile, '/images/stylist-profile/hairstyle/long/User3-4.jpg')
|
|
|
|
|
|
|
+ const referencePreview = usePreviewUrl(referenceFile, '')
|
|
|
|
|
+ const customModelPreview = usePreviewUrl(customModelFile, '')
|
|
|
|
|
+ const customOutfitPreview = usePreviewUrl(customOutfitFile, '')
|
|
|
|
|
+ const customBackgroundPreview = usePreviewUrl(customBackgroundFile, '')
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
const [submitPhase, setSubmitPhase] = useState<'idle' | 'uploading' | 'generating'>('idle')
|
|
const [submitPhase, setSubmitPhase] = useState<'idle' | 'uploading' | 'generating'>('idle')
|
|
|
const [error, setError] = useState('')
|
|
const [error, setError] = useState('')
|
|
@@ -69,17 +101,27 @@ export function TemplateFormPage() {
|
|
|
const data = await getTemplateAssets()
|
|
const data = await getTemplateAssets()
|
|
|
if (cancelled) return
|
|
if (cancelled) return
|
|
|
|
|
|
|
|
- setAssets(data)
|
|
|
|
|
- setSelectedModelId((current) => current || data.models[0]?.id || '')
|
|
|
|
|
- setSelectedOutfitId((current) => current || data.clothes[0]?.id || '')
|
|
|
|
|
- setSelectedBackgroundId((current) => current || data.backgrounds[0]?.id || '')
|
|
|
|
|
|
|
+ const nextAssets: TemplateAssets = {
|
|
|
|
|
+ models: STATIC_PRESET_ASSETS.models,
|
|
|
|
|
+ clothes: data.clothes.length ? data.clothes : STATIC_PRESET_ASSETS.clothes,
|
|
|
|
|
+ backgrounds: STATIC_PRESET_ASSETS.backgrounds,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setAssets(nextAssets)
|
|
|
|
|
+ setSelectedModelId((current) => current || nextAssets.models[0]?.id || '')
|
|
|
|
|
+ setSelectedOutfitId((current) => current || nextAssets.clothes[0]?.id || '')
|
|
|
|
|
+ setSelectedBackgroundId((current) => current || nextAssets.backgrounds[0]?.id || '')
|
|
|
|
|
|
|
|
- if (data.models.length === 0 || data.clothes.length === 0 || data.backgrounds.length === 0) {
|
|
|
|
|
|
|
+ if (nextAssets.models.length === 0 || nextAssets.clothes.length === 0 || nextAssets.backgrounds.length === 0) {
|
|
|
setAssetsError(pageCopy.assetEmpty || '素材未配置完成,请检查服务器配置')
|
|
setAssetsError(pageCopy.assetEmpty || '素材未配置完成,请检查服务器配置')
|
|
|
}
|
|
}
|
|
|
} catch {
|
|
} catch {
|
|
|
if (cancelled) return
|
|
if (cancelled) return
|
|
|
- setAssets(EMPTY_ASSETS)
|
|
|
|
|
|
|
+ setAssets({
|
|
|
|
|
+ models: STATIC_PRESET_ASSETS.models,
|
|
|
|
|
+ clothes: [],
|
|
|
|
|
+ backgrounds: STATIC_PRESET_ASSETS.backgrounds,
|
|
|
|
|
+ })
|
|
|
setAssetsError(pageCopy.assetLoadFailed || '素材加载失败,请刷新后重试')
|
|
setAssetsError(pageCopy.assetLoadFailed || '素材加载失败,请刷新后重试')
|
|
|
} finally {
|
|
} finally {
|
|
|
if (!cancelled) {
|
|
if (!cancelled) {
|
|
@@ -123,7 +165,7 @@ export function TemplateFormPage() {
|
|
|
|
|
|
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
|
event.preventDefault()
|
|
event.preventDefault()
|
|
|
- if (!name || !description || submitting) return
|
|
|
|
|
|
|
+ if (!name || submitting) return
|
|
|
setError('')
|
|
setError('')
|
|
|
setSubmitting(true)
|
|
setSubmitting(true)
|
|
|
setSubmitPhase('idle')
|
|
setSubmitPhase('idle')
|
|
@@ -143,21 +185,21 @@ export function TemplateFormPage() {
|
|
|
|
|
|
|
|
if (modelSource === 'upload') {
|
|
if (modelSource === 'upload') {
|
|
|
if (!customModelFile) {
|
|
if (!customModelFile) {
|
|
|
- throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
|
|
+ throw new Error(pageCopy.validation?.customModelRequired || '请先选择自定义模特图片')
|
|
|
}
|
|
}
|
|
|
modelUrl = (await uploadMedia(customModelFile, 'ai-template-model-image')).url
|
|
modelUrl = (await uploadMedia(customModelFile, 'ai-template-model-image')).url
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (outfitSource === 'upload') {
|
|
if (outfitSource === 'upload') {
|
|
|
if (!customOutfitFile) {
|
|
if (!customOutfitFile) {
|
|
|
- throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
|
|
+ throw new Error(pageCopy.validation?.customOutfitRequired || '请先选择自定义服装图片')
|
|
|
}
|
|
}
|
|
|
clothesUrl = (await uploadMedia(customOutfitFile, 'ai-template-clothing-image')).url
|
|
clothesUrl = (await uploadMedia(customOutfitFile, 'ai-template-clothing-image')).url
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (backgroundSource === 'upload') {
|
|
if (backgroundSource === 'upload') {
|
|
|
if (!customBackgroundFile) {
|
|
if (!customBackgroundFile) {
|
|
|
- throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
|
|
+ throw new Error(pageCopy.validation?.customBackgroundRequired || '请先选择自定义背景图片')
|
|
|
}
|
|
}
|
|
|
backgroundUrl = (await uploadMedia(customBackgroundFile, 'ai-template-background-image')).url
|
|
backgroundUrl = (await uploadMedia(customBackgroundFile, 'ai-template-background-image')).url
|
|
|
}
|
|
}
|
|
@@ -178,7 +220,7 @@ export function TemplateFormPage() {
|
|
|
? 'SHORT'
|
|
? 'SHORT'
|
|
|
: 'DYE',
|
|
: 'DYE',
|
|
|
status: status === 'Published' ? 'PUBLIC' : 'DRAFT',
|
|
status: status === 'Published' ? 'PUBLIC' : 'DRAFT',
|
|
|
- description,
|
|
|
|
|
|
|
+ description: description.trim() || undefined,
|
|
|
tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
|
refHairUrl,
|
|
refHairUrl,
|
|
|
modelUrl,
|
|
modelUrl,
|
|
@@ -228,7 +270,14 @@ export function TemplateFormPage() {
|
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.reference.title}</h2>
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.reference.title}</h2>
|
|
|
<label htmlFor="template-reference-upload" className="template-upload-box">
|
|
<label htmlFor="template-reference-upload" className="template-upload-box">
|
|
|
<input id="template-reference-upload" type="file" accept="image/*" onChange={(event) => setReferenceFile(event.target.files?.[0] || null)} />
|
|
<input id="template-reference-upload" type="file" accept="image/*" onChange={(event) => setReferenceFile(event.target.files?.[0] || null)} />
|
|
|
- <img src={referencePreview} alt={pageCopy.reference.previewAlt} />
|
|
|
|
|
|
|
+ {referencePreview ? (
|
|
|
|
|
+ <img src={referencePreview} alt={pageCopy.reference.previewAlt} />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="template-upload-placeholder">
|
|
|
|
|
+ <i className="feather icon-feather-image"></i>
|
|
|
|
|
+ <strong>{pageCopy.reference.upload}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
<span><i className="feather icon-feather-upload"></i>{pageCopy.reference.upload}</span>
|
|
<span><i className="feather icon-feather-upload"></i>{pageCopy.reference.upload}</span>
|
|
|
</label>
|
|
</label>
|
|
|
<p className="fs-14 text-medium-gray lh-24 mt-15px mb-0">{pageCopy.reference.help}</p>
|
|
<p className="fs-14 text-medium-gray lh-24 mt-15px mb-0">{pageCopy.reference.help}</p>
|
|
@@ -244,7 +293,7 @@ export function TemplateFormPage() {
|
|
|
<div className="col-md-5"><label htmlFor="template-visibility" className="customer-edit-label">{pageCopy.info.status}</label><select id="template-visibility" className="form-select customer-edit-control" value={status} onChange={(event) => setStatus(event.target.value)}><option value="Draft">{pageCopy.statusOptions.Draft}</option><option value="Published">{pageCopy.statusOptions.Published}</option></select></div>
|
|
<div className="col-md-5"><label htmlFor="template-visibility" className="customer-edit-label">{pageCopy.info.status}</label><select id="template-visibility" className="form-select customer-edit-control" value={status} onChange={(event) => setStatus(event.target.value)}><option value="Draft">{pageCopy.statusOptions.Draft}</option><option value="Published">{pageCopy.statusOptions.Published}</option></select></div>
|
|
|
<div className="col-md-6"><label htmlFor="template-category" className="customer-edit-label">{pageCopy.info.category}</label><select id="template-category" className="form-select customer-edit-control" value={category} onChange={(event) => setCategory(event.target.value)}><option value="Long">{pageCopy.categoryOptions.Long}</option><option value="Medium">{pageCopy.categoryOptions.Medium}</option><option value="Short">{pageCopy.categoryOptions.Short}</option><option value="Color">{pageCopy.categoryOptions.Color}</option></select></div>
|
|
<div className="col-md-6"><label htmlFor="template-category" className="customer-edit-label">{pageCopy.info.category}</label><select id="template-category" className="form-select customer-edit-control" value={category} onChange={(event) => setCategory(event.target.value)}><option value="Long">{pageCopy.categoryOptions.Long}</option><option value="Medium">{pageCopy.categoryOptions.Medium}</option><option value="Short">{pageCopy.categoryOptions.Short}</option><option value="Color">{pageCopy.categoryOptions.Color}</option></select></div>
|
|
|
<div className="col-md-6"><label htmlFor="template-tags" className="customer-edit-label">{pageCopy.info.tags}</label><input id="template-tags" type="text" className="form-control customer-edit-control" value={tags} onChange={(event) => setTags(event.target.value)} placeholder={pageCopy.info.tagsPlaceholder} /></div>
|
|
<div className="col-md-6"><label htmlFor="template-tags" className="customer-edit-label">{pageCopy.info.tags}</label><input id="template-tags" type="text" className="form-control customer-edit-control" value={tags} onChange={(event) => setTags(event.target.value)} placeholder={pageCopy.info.tagsPlaceholder} /></div>
|
|
|
- <div className="col-12"><label htmlFor="template-description" className="customer-edit-label">{pageCopy.info.description}</label><textarea id="template-description" className="form-control customer-edit-control" rows={6} value={description} onChange={(event) => setDescription(event.target.value)} placeholder={pageCopy.info.descriptionPlaceholder}></textarea></div>
|
|
|
|
|
|
|
+ <div className="col-12"><label htmlFor="template-description" className="customer-edit-label">{pageCopy.info.description}</label><textarea aria-label={pageCopy.info.description} id="template-description" className="form-control customer-edit-control" rows={6} value={description} onChange={(event) => setDescription(event.target.value)} placeholder={pageCopy.info.descriptionPlaceholder}></textarea></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -286,6 +335,7 @@ export function TemplateFormPage() {
|
|
|
<div className="template-upload-inline">
|
|
<div className="template-upload-inline">
|
|
|
<label htmlFor="template-model-upload" className="customer-edit-label">{pageCopy.model.uploadLabel || '上传模特图片'}</label>
|
|
<label htmlFor="template-model-upload" className="customer-edit-label">{pageCopy.model.uploadLabel || '上传模特图片'}</label>
|
|
|
<input id="template-model-upload" aria-label={pageCopy.model.uploadLabel || '上传模特图片'} type="file" accept="image/*" className="form-control customer-edit-control" onChange={(event) => setCustomModelFile(event.target.files?.[0] || null)} />
|
|
<input id="template-model-upload" aria-label={pageCopy.model.uploadLabel || '上传模特图片'} type="file" accept="image/*" className="form-control customer-edit-control" onChange={(event) => setCustomModelFile(event.target.files?.[0] || null)} />
|
|
|
|
|
+ {customModelPreview ? <img src={customModelPreview} alt="自定义模特预览" className="template-upload-inline-preview" /> : null}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
@@ -318,6 +368,7 @@ export function TemplateFormPage() {
|
|
|
<div className="template-upload-inline">
|
|
<div className="template-upload-inline">
|
|
|
<label htmlFor="template-outfit-upload" className="customer-edit-label">{pageCopy.outfit.uploadLabel || '上传服装图片'}</label>
|
|
<label htmlFor="template-outfit-upload" className="customer-edit-label">{pageCopy.outfit.uploadLabel || '上传服装图片'}</label>
|
|
|
<input id="template-outfit-upload" aria-label={pageCopy.outfit.uploadLabel || '上传服装图片'} type="file" accept="image/*" className="form-control customer-edit-control" onChange={(event) => setCustomOutfitFile(event.target.files?.[0] || null)} />
|
|
<input id="template-outfit-upload" aria-label={pageCopy.outfit.uploadLabel || '上传服装图片'} type="file" accept="image/*" className="form-control customer-edit-control" onChange={(event) => setCustomOutfitFile(event.target.files?.[0] || null)} />
|
|
|
|
|
+ {customOutfitPreview ? <img src={customOutfitPreview} alt="自定义服装预览" className="template-upload-inline-preview" /> : null}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
@@ -350,6 +401,7 @@ export function TemplateFormPage() {
|
|
|
<div className="template-upload-inline">
|
|
<div className="template-upload-inline">
|
|
|
<label htmlFor="template-background-upload" className="customer-edit-label">{pageCopy.background.uploadLabel || '上传背景图片'}</label>
|
|
<label htmlFor="template-background-upload" className="customer-edit-label">{pageCopy.background.uploadLabel || '上传背景图片'}</label>
|
|
|
<input id="template-background-upload" aria-label={pageCopy.background.uploadLabel || '上传背景图片'} type="file" accept="image/*" className="form-control customer-edit-control" onChange={(event) => setCustomBackgroundFile(event.target.files?.[0] || null)} />
|
|
<input id="template-background-upload" aria-label={pageCopy.background.uploadLabel || '上传背景图片'} type="file" accept="image/*" className="form-control customer-edit-control" onChange={(event) => setCustomBackgroundFile(event.target.files?.[0] || null)} />
|
|
|
|
|
+ {customBackgroundPreview ? <img src={customBackgroundPreview} alt="自定义背景预览" className="template-upload-inline-preview" /> : null}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|