|
@@ -4,12 +4,13 @@ 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 {
|
|
import {
|
|
|
- createTemplateAndStartGeneration,
|
|
|
|
|
|
|
+ createTemplateMakeTask,
|
|
|
getTemplateAssets,
|
|
getTemplateAssets,
|
|
|
|
|
+ type TemplateAssetOption,
|
|
|
|
|
+ type TemplateAssets,
|
|
|
} from '../../../services/api/templates'
|
|
} from '../../../services/api/templates'
|
|
|
import { uploadMedia } from '../../../services/api/media'
|
|
import { uploadMedia } from '../../../services/api/media'
|
|
|
import { ApiError } from '../../../services/http/client'
|
|
import { ApiError } from '../../../services/http/client'
|
|
|
-import type { TemplateAssetOption } from '../../../services/api/contracts'
|
|
|
|
|
|
|
|
|
|
function getSubmitErrorMessage(err: unknown, fallback: string) {
|
|
function getSubmitErrorMessage(err: unknown, fallback: string) {
|
|
|
if (
|
|
if (
|
|
@@ -23,28 +24,18 @@ function getSubmitErrorMessage(err: unknown, fallback: string) {
|
|
|
return err instanceof Error ? err.message : fallback
|
|
return err instanceof Error ? err.message : fallback
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type TemplateOption = {
|
|
|
|
|
- id: string
|
|
|
|
|
- label: string
|
|
|
|
|
- url: string
|
|
|
|
|
- previewUrl: string
|
|
|
|
|
-}
|
|
|
|
|
|
|
+type AssetSource = 'preset' | 'upload'
|
|
|
|
|
|
|
|
-function toTemplateOptions(items: TemplateAssetOption[]): TemplateOption[] {
|
|
|
|
|
- return items.map((item) => ({
|
|
|
|
|
- id: item.id,
|
|
|
|
|
- label: item.name,
|
|
|
|
|
- url: item.url,
|
|
|
|
|
- previewUrl: item.url,
|
|
|
|
|
- }))
|
|
|
|
|
|
|
+const EMPTY_ASSETS: TemplateAssets = {
|
|
|
|
|
+ models: [],
|
|
|
|
|
+ clothes: [],
|
|
|
|
|
+ backgrounds: [],
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
- const [modelOptions, setModelOptions] = useState<TemplateOption[]>([])
|
|
|
|
|
- const [outfitOptions, setOutfitOptions] = useState<TemplateOption[]>([])
|
|
|
|
|
- const [backgroundOptions, setBackgroundOptions] = useState<TemplateOption[]>([])
|
|
|
|
|
|
|
+ const [assets, setAssets] = useState<TemplateAssets>(EMPTY_ASSETS)
|
|
|
const [assetsLoading, setAssetsLoading] = useState(true)
|
|
const [assetsLoading, setAssetsLoading] = useState(true)
|
|
|
const [assetsError, setAssetsError] = useState('')
|
|
const [assetsError, setAssetsError] = useState('')
|
|
|
const [name, setName] = useState('')
|
|
const [name, setName] = useState('')
|
|
@@ -56,6 +47,12 @@ export function TemplateFormPage() {
|
|
|
const [selectedModelId, setSelectedModelId] = useState('')
|
|
const [selectedModelId, setSelectedModelId] = useState('')
|
|
|
const [selectedOutfitId, setSelectedOutfitId] = useState('')
|
|
const [selectedOutfitId, setSelectedOutfitId] = useState('')
|
|
|
const [selectedBackgroundId, setSelectedBackgroundId] = useState('')
|
|
const [selectedBackgroundId, setSelectedBackgroundId] = useState('')
|
|
|
|
|
+ const [modelSource, setModelSource] = useState<AssetSource>('preset')
|
|
|
|
|
+ const [outfitSource, setOutfitSource] = useState<AssetSource>('preset')
|
|
|
|
|
+ const [backgroundSource, setBackgroundSource] = useState<AssetSource>('preset')
|
|
|
|
|
+ const [customModelFile, setCustomModelFile] = useState<File | null>(null)
|
|
|
|
|
+ const [customOutfitFile, setCustomOutfitFile] = 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, '/images/stylist-profile/hairstyle/long/User3-4.jpg')
|
|
|
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')
|
|
@@ -72,35 +69,18 @@ export function TemplateFormPage() {
|
|
|
const data = await getTemplateAssets()
|
|
const data = await getTemplateAssets()
|
|
|
if (cancelled) return
|
|
if (cancelled) return
|
|
|
|
|
|
|
|
- const nextModels = toTemplateOptions(data.models)
|
|
|
|
|
- const nextOutfits = toTemplateOptions(data.clothes)
|
|
|
|
|
- const nextBackgrounds = toTemplateOptions(data.backgrounds)
|
|
|
|
|
-
|
|
|
|
|
- setModelOptions(nextModels)
|
|
|
|
|
- setOutfitOptions(nextOutfits)
|
|
|
|
|
- setBackgroundOptions(nextBackgrounds)
|
|
|
|
|
|
|
+ setAssets(data)
|
|
|
|
|
+ setSelectedModelId((current) => current || data.models[0]?.id || '')
|
|
|
|
|
+ setSelectedOutfitId((current) => current || data.clothes[0]?.id || '')
|
|
|
|
|
+ setSelectedBackgroundId((current) => current || data.backgrounds[0]?.id || '')
|
|
|
|
|
|
|
|
- if (nextModels.length === 0 || nextOutfits.length === 0 || nextBackgrounds.length === 0) {
|
|
|
|
|
- setSelectedModelId('')
|
|
|
|
|
- setSelectedOutfitId('')
|
|
|
|
|
- setSelectedBackgroundId('')
|
|
|
|
|
|
|
+ if (data.models.length === 0 || data.clothes.length === 0 || data.backgrounds.length === 0) {
|
|
|
setAssetsError(pageCopy.assetEmpty || '素材未配置完成,请检查服务器配置')
|
|
setAssetsError(pageCopy.assetEmpty || '素材未配置完成,请检查服务器配置')
|
|
|
- return
|
|
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- setSelectedModelId(nextModels[0].id)
|
|
|
|
|
- setSelectedOutfitId(nextOutfits[0].id)
|
|
|
|
|
- setSelectedBackgroundId(nextBackgrounds[0].id)
|
|
|
|
|
} catch {
|
|
} catch {
|
|
|
- if (!cancelled) {
|
|
|
|
|
- setModelOptions([])
|
|
|
|
|
- setOutfitOptions([])
|
|
|
|
|
- setBackgroundOptions([])
|
|
|
|
|
- setSelectedModelId('')
|
|
|
|
|
- setSelectedOutfitId('')
|
|
|
|
|
- setSelectedBackgroundId('')
|
|
|
|
|
- setAssetsError(pageCopy.assetLoadFailed || '素材加载失败,请刷新后重试')
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (cancelled) return
|
|
|
|
|
+ setAssets(EMPTY_ASSETS)
|
|
|
|
|
+ setAssetsError(pageCopy.assetLoadFailed || '素材加载失败,请刷新后重试')
|
|
|
} finally {
|
|
} finally {
|
|
|
if (!cancelled) {
|
|
if (!cancelled) {
|
|
|
setAssetsLoading(false)
|
|
setAssetsLoading(false)
|
|
@@ -115,15 +95,24 @@ export function TemplateFormPage() {
|
|
|
}
|
|
}
|
|
|
}, [pageCopy.assetEmpty, pageCopy.assetLoadFailed])
|
|
}, [pageCopy.assetEmpty, pageCopy.assetLoadFailed])
|
|
|
|
|
|
|
|
- const hasReadyAssets = useMemo(
|
|
|
|
|
- () =>
|
|
|
|
|
- !assetsLoading &&
|
|
|
|
|
- !assetsError &&
|
|
|
|
|
- modelOptions.length > 0 &&
|
|
|
|
|
- outfitOptions.length > 0 &&
|
|
|
|
|
- backgroundOptions.length > 0,
|
|
|
|
|
- [assetsLoading, assetsError, modelOptions, outfitOptions, backgroundOptions],
|
|
|
|
|
|
|
+ const selectedModel = useMemo(
|
|
|
|
|
+ () => assets.models.find((item) => item.id === selectedModelId) ?? null,
|
|
|
|
|
+ [assets.models, selectedModelId],
|
|
|
)
|
|
)
|
|
|
|
|
+ const selectedOutfit = useMemo(
|
|
|
|
|
+ () => assets.clothes.find((item) => item.id === selectedOutfitId) ?? null,
|
|
|
|
|
+ [assets.clothes, selectedOutfitId],
|
|
|
|
|
+ )
|
|
|
|
|
+ const selectedBackground = useMemo(
|
|
|
|
|
+ () => assets.backgrounds.find((item) => item.id === selectedBackgroundId) ?? null,
|
|
|
|
|
+ [assets.backgrounds, selectedBackgroundId],
|
|
|
|
|
+ )
|
|
|
|
|
+ const hasReadyAssets =
|
|
|
|
|
+ !assetsLoading &&
|
|
|
|
|
+ !assetsError &&
|
|
|
|
|
+ assets.models.length > 0 &&
|
|
|
|
|
+ assets.clothes.length > 0 &&
|
|
|
|
|
+ assets.backgrounds.length > 0
|
|
|
|
|
|
|
|
const submitMessage =
|
|
const submitMessage =
|
|
|
submitPhase === 'uploading'
|
|
submitPhase === 'uploading'
|
|
@@ -143,32 +132,60 @@ export function TemplateFormPage() {
|
|
|
throw new Error(pageCopy.validation?.referenceRequired || '请先上传参考图')
|
|
throw new Error(pageCopy.validation?.referenceRequired || '请先上传参考图')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const selectedModel = modelOptions.find((item) => item.id === selectedModelId)
|
|
|
|
|
- const selectedOutfit = outfitOptions.find((item) => item.id === selectedOutfitId)
|
|
|
|
|
- const selectedBackground = backgroundOptions.find(
|
|
|
|
|
- (item) => item.id === selectedBackgroundId,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ setSubmitPhase('uploading')
|
|
|
|
|
+ const refHairUrl = (
|
|
|
|
|
+ await uploadMedia(referenceFile, 'ai-template-origin-image')
|
|
|
|
|
+ ).url
|
|
|
|
|
+
|
|
|
|
|
+ let modelUrl = selectedModel?.url ?? ''
|
|
|
|
|
+ let clothesUrl = selectedOutfit?.url ?? ''
|
|
|
|
|
+ let backgroundUrl = selectedBackground?.url ?? ''
|
|
|
|
|
|
|
|
- if (!selectedModel || !selectedOutfit || !selectedBackground) {
|
|
|
|
|
- throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
|
|
+ if (modelSource === 'upload') {
|
|
|
|
|
+ if (!customModelFile) {
|
|
|
|
|
+ throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
+ }
|
|
|
|
|
+ modelUrl = (await uploadMedia(customModelFile, 'ai-template-model-image')).url
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- setSubmitPhase('uploading')
|
|
|
|
|
- const referenceImage = (await uploadMedia(referenceFile)).url
|
|
|
|
|
|
|
+ if (outfitSource === 'upload') {
|
|
|
|
|
+ if (!customOutfitFile) {
|
|
|
|
|
+ throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
+ }
|
|
|
|
|
+ clothesUrl = (await uploadMedia(customOutfitFile, 'ai-template-clothing-image')).url
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (backgroundSource === 'upload') {
|
|
|
|
|
+ if (!customBackgroundFile) {
|
|
|
|
|
+ throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
+ }
|
|
|
|
|
+ backgroundUrl = (await uploadMedia(customBackgroundFile, 'ai-template-background-image')).url
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!modelUrl || !clothesUrl || !backgroundUrl) {
|
|
|
|
|
+ throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
setSubmitPhase('generating')
|
|
setSubmitPhase('generating')
|
|
|
- const template = await createTemplateAndStartGeneration({
|
|
|
|
|
- name,
|
|
|
|
|
- category,
|
|
|
|
|
- status,
|
|
|
|
|
|
|
+ const task = await createTemplateMakeTask({
|
|
|
|
|
+ templateName: name,
|
|
|
|
|
+ category:
|
|
|
|
|
+ category === 'Long'
|
|
|
|
|
+ ? 'LONG'
|
|
|
|
|
+ : category === 'Medium'
|
|
|
|
|
+ ? 'MEDIUM'
|
|
|
|
|
+ : category === 'Short'
|
|
|
|
|
+ ? 'SHORT'
|
|
|
|
|
+ : 'DYE',
|
|
|
|
|
+ status: status === 'Published' ? 'PUBLIC' : 'DRAFT',
|
|
|
description,
|
|
description,
|
|
|
tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
|
- referenceImage,
|
|
|
|
|
- modelImageUrl: selectedModel.url,
|
|
|
|
|
- outfitImageUrl: selectedOutfit.url,
|
|
|
|
|
- backgroundImageUrl: selectedBackground.url,
|
|
|
|
|
|
|
+ refHairUrl,
|
|
|
|
|
+ modelUrl,
|
|
|
|
|
+ clothesUrl,
|
|
|
|
|
+ backgroundUrl,
|
|
|
})
|
|
})
|
|
|
- navigate(path(`/templates/${template.id}`))
|
|
|
|
|
|
|
+ navigate(path(`/templates/tasks/${task.taskId}`))
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
setError(getSubmitErrorMessage(err, pageCopy.errors?.saveFailed || '保存失败'))
|
|
setError(getSubmitErrorMessage(err, pageCopy.errors?.saveFailed || '保存失败'))
|
|
|
} finally {
|
|
} finally {
|
|
@@ -223,7 +240,7 @@ export function TemplateFormPage() {
|
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.info.badge}</span>
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.info.badge}</span>
|
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.info.title}</h2>
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.info.title}</h2>
|
|
|
<div className="row g-3">
|
|
<div className="row g-3">
|
|
|
- <div className="col-md-7"><label htmlFor="template-name" className="customer-edit-label">{pageCopy.info.name} <span className="text-base-color">*</span></label><input id="template-name" type="text" className="form-control customer-edit-control" value={name} onChange={(event) => setName(event.target.value)} placeholder={pageCopy.info.namePlaceholder} required /></div>
|
|
|
|
|
|
|
+ <div className="col-md-7"><label htmlFor="template-name" className="customer-edit-label">{pageCopy.info.name} <span className="text-base-color">*</span></label><input aria-label={`${pageCopy.info.name} *`} id="template-name" type="text" className="form-control customer-edit-control" value={name} onChange={(event) => setName(event.target.value)} placeholder={pageCopy.info.namePlaceholder} required /></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-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>
|
|
@@ -246,20 +263,31 @@ export function TemplateFormPage() {
|
|
|
{assetsError}
|
|
{assetsError}
|
|
|
</p>
|
|
</p>
|
|
|
) : null}
|
|
) : null}
|
|
|
- <div className="template-option-grid">
|
|
|
|
|
- {modelOptions.map((item) => (
|
|
|
|
|
- <label key={item.id} className="template-option-card">
|
|
|
|
|
- <input
|
|
|
|
|
- type="radio"
|
|
|
|
|
- name="ai-model"
|
|
|
|
|
- checked={selectedModelId === item.id}
|
|
|
|
|
- onChange={() => setSelectedModelId(item.id)}
|
|
|
|
|
- />
|
|
|
|
|
- <img src={item.previewUrl} alt={item.label} />
|
|
|
|
|
- <span>{item.label}</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <div className="template-source-toggle">
|
|
|
|
|
+ <button type="button" className={`btn btn-small btn-rounded ${modelSource === 'preset' ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`} onClick={() => setModelSource('preset')}>{pageCopy.model.preset || '使用预设模特'}</button>
|
|
|
|
|
+ <button type="button" className={`btn btn-small btn-rounded ${modelSource === 'upload' ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`} onClick={() => setModelSource('upload')}>{pageCopy.model.upload || '上传自定义模特'}</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {modelSource === 'preset' ? (
|
|
|
|
|
+ <div className="template-option-grid">
|
|
|
|
|
+ {assets.models.map((item: TemplateAssetOption) => (
|
|
|
|
|
+ <label key={item.id} className="template-option-card">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="ai-model"
|
|
|
|
|
+ checked={selectedModelId === item.id}
|
|
|
|
|
+ onChange={() => setSelectedModelId(item.id)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <img src={item.url} alt={item.name} />
|
|
|
|
|
+ <span>{item.name}</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="template-upload-inline">
|
|
|
|
|
+ <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)} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="row g-4 mt-1">
|
|
<div className="row g-4 mt-1">
|
|
@@ -267,20 +295,31 @@ export function TemplateFormPage() {
|
|
|
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
|
|
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
|
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.outfit.badge}</span>
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.outfit.badge}</span>
|
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.outfit.title}</h2>
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.outfit.title}</h2>
|
|
|
- <div className="template-option-grid">
|
|
|
|
|
- {outfitOptions.map((item) => (
|
|
|
|
|
- <label key={item.id} className="template-option-card">
|
|
|
|
|
- <input
|
|
|
|
|
- type="radio"
|
|
|
|
|
- name="outfit"
|
|
|
|
|
- checked={selectedOutfitId === item.id}
|
|
|
|
|
- onChange={() => setSelectedOutfitId(item.id)}
|
|
|
|
|
- />
|
|
|
|
|
- <img src={item.previewUrl} alt={item.label} />
|
|
|
|
|
- <span>{item.label}</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <div className="template-source-toggle">
|
|
|
|
|
+ <button type="button" className={`btn btn-small btn-rounded ${outfitSource === 'preset' ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`} onClick={() => setOutfitSource('preset')}>{pageCopy.outfit.preset || '使用预设服装'}</button>
|
|
|
|
|
+ <button type="button" className={`btn btn-small btn-rounded ${outfitSource === 'upload' ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`} onClick={() => setOutfitSource('upload')}>{pageCopy.outfit.upload || '上传自定义服装'}</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {outfitSource === 'preset' ? (
|
|
|
|
|
+ <div className="template-option-grid">
|
|
|
|
|
+ {assets.clothes.map((item: TemplateAssetOption) => (
|
|
|
|
|
+ <label key={item.id} className="template-option-card">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="outfit"
|
|
|
|
|
+ checked={selectedOutfitId === item.id}
|
|
|
|
|
+ onChange={() => setSelectedOutfitId(item.id)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <img src={item.url} alt={item.name} />
|
|
|
|
|
+ <span>{item.name}</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="template-upload-inline">
|
|
|
|
|
+ <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)} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -288,20 +327,31 @@ export function TemplateFormPage() {
|
|
|
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
|
|
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
|
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.background.badge}</span>
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.background.badge}</span>
|
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.background.title}</h2>
|
|
<h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.background.title}</h2>
|
|
|
- <div className="template-option-grid">
|
|
|
|
|
- {backgroundOptions.map((item) => (
|
|
|
|
|
- <label key={item.id} className="template-option-card">
|
|
|
|
|
- <input
|
|
|
|
|
- type="radio"
|
|
|
|
|
- name="background"
|
|
|
|
|
- checked={selectedBackgroundId === item.id}
|
|
|
|
|
- onChange={() => setSelectedBackgroundId(item.id)}
|
|
|
|
|
- />
|
|
|
|
|
- <img src={item.previewUrl} alt={item.label} />
|
|
|
|
|
- <span>{item.label}</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <div className="template-source-toggle">
|
|
|
|
|
+ <button type="button" className={`btn btn-small btn-rounded ${backgroundSource === 'preset' ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`} onClick={() => setBackgroundSource('preset')}>{pageCopy.background.preset || '使用预设背景'}</button>
|
|
|
|
|
+ <button type="button" className={`btn btn-small btn-rounded ${backgroundSource === 'upload' ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`} onClick={() => setBackgroundSource('upload')}>{pageCopy.background.upload || '上传自定义背景'}</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {backgroundSource === 'preset' ? (
|
|
|
|
|
+ <div className="template-option-grid">
|
|
|
|
|
+ {assets.backgrounds.map((item: TemplateAssetOption) => (
|
|
|
|
|
+ <label key={item.id} className="template-option-card">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="background"
|
|
|
|
|
+ checked={selectedBackgroundId === item.id}
|
|
|
|
|
+ onChange={() => setSelectedBackgroundId(item.id)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <img src={item.url} alt={item.name} />
|
|
|
|
|
+ <span>{item.name}</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="template-upload-inline">
|
|
|
|
|
+ <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)} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|