| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- import { useEffect, useState } from 'react'
- import { Link, useParams } from 'react-router-dom'
- import { Helmet } from 'react-helmet-async'
- import type { AiHairSimulationDetail } from '../../../services/api/contracts'
- import { getSimulationById } from '../../../services/api/simulations'
- import { usePageContext } from '../../../shared/pageContext'
- import { DEFAULT_USER_AVATAR_URL, LOADING_ANIMATION_URL } from '../../../shared/uiAssets'
- import { demoSimulationResults, resolveLocalizedTemplateName } from '../../../shared/demoContent'
- import { ImageLightbox } from '../../../components/common/ImageLightbox'
- import { DashboardLayout } from '../../../layouts/DashboardLayout'
- function formatDisplayDate(input: string) {
- const date = new Date(input)
- if (Number.isNaN(date.getTime())) return input
- return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
- }
- const demoDetail: AiHairSimulationDetail = {
- id: '10',
- createdAt: '2026-07-03T23:52:00',
- customer: { id: '1', name: '山田', avatar: DEFAULT_USER_AVATAR_URL, phone: '', expectContact: 'NONE' },
- originImage: '/images/mvp-simulation-result-10/source-photo.jpg',
- simulateRemark: null,
- totalTaskCount: 3,
- processingTaskCount: 0,
- successTaskCount: 3,
- failedTaskCount: 0,
- overallStatus: 'SUCCESSES',
- tasks: demoSimulationResults.map((item, index) => ({
- id: item.id,
- templateId: item.id,
- templateName: item.title,
- templateCoverUrl: item.image,
- templateCategory: 'MEDIUM',
- templateSource: 'SYSTEM',
- taskId: `demo-${index}`,
- taskStatus: 'SUCCESSES',
- resultImages: [item.image],
- errorMsg: null,
- taskStartTime: '2026-07-03T23:52:00',
- taskFinishTime: '2026-07-03T23:52:10',
- })),
- }
- export function SimulationResultPage() {
- const { simulationId = '' } = useParams()
- const { locale, path, copy } = usePageContext()
- const pageCopy = copy.simulationResultPage
- const [detail, setDetail] = useState<AiHairSimulationDetail | null>(simulationId === '10' ? demoDetail : null)
- const [loading, setLoading] = useState(simulationId !== '10')
- const [error, setError] = useState('')
- const [preview, setPreview] = useState<{ image: string; alt: string } | null>(null)
- useEffect(() => {
- if (simulationId === '10') {
- setDetail(demoDetail)
- setLoading(false)
- return undefined
- }
- let cancelled = false
- let timer: number | undefined
- const load = async () => {
- try {
- const data = await getSimulationById(simulationId)
- if (cancelled) return
- setDetail(data)
- setError('')
- setLoading(false)
- if (data.overallStatus === 'PROCESSING') timer = window.setTimeout(() => void load(), 5000)
- } catch (loadError) {
- if (cancelled) return
- setError(loadError instanceof Error ? loadError.message : pageCopy.loadFailed)
- setLoading(false)
- }
- }
- void load()
- return () => {
- cancelled = true
- if (timer) window.clearTimeout(timer)
- }
- }, [pageCopy.loadFailed, simulationId])
- const backTarget = detail?.customer ? path(`/customers/${detail.customer.id}`) : path('/simulation')
- const customerName = simulationId === '10' ? pageCopy.demoCustomerName || detail?.customer?.name : detail?.customer?.name
- return (
- <DashboardLayout>
- <Helmet><html lang={locale === 'ja' ? 'ja' : locale === 'en' ? 'en' : 'zh-CN'} /><title>{`Aitori Stylist | ${pageCopy.title}`}</title></Helmet>
- <section className="stylist-workbench-top ipad-top-space-margin mb-2"><div className="container"><div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4"><Link to={backTarget} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>{pageCopy.back}</Link></div></div></section>
- <section className="pt-0"><div className="container">
- {error ? <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-3"><p role="alert" className="text-red mb-0">{error}</p></div> : null}
- {loading ? <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px"><div className="simulation-loading-state"><img src={LOADING_ANIMATION_URL} alt={pageCopy.card.processing} className="simulation-loading-gif" /><p className="mb-0 text-medium-gray">{pageCopy.loading}</p></div></div> : null}
- {detail ? <div className="simulation-result-layout gallery-box">
- <aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-25px">
- <h1 className="fw-700 text-dark-gray fs-24 mb-20px">{pageCopy.original.title}</h1>
- <button type="button" className="simulation-original-card aitori-image-button" onClick={() => setPreview({ image: detail.originImage || DEFAULT_USER_AVATAR_URL, alt: pageCopy.original.title })}><img className="simulation-result-image" src={detail.originImage || DEFAULT_USER_AVATAR_URL} alt={pageCopy.original.title} /></button>
- <div className="template-detail-summary mt-20px"><div><span>{pageCopy.original.customer}</span><strong>{customerName || pageCopy.anonymousTitle}</strong></div><div><span>{pageCopy.original.generatedAt}</span><strong>{formatDisplayDate(detail.createdAt)}</strong></div><div><span>{pageCopy.original.count}</span><strong>{pageCopy.original.countValue.replace('{{count}}', String(detail.totalTaskCount))}</strong></div></div>
- </aside>
- <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
- <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px"><h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.results.title}</h2><span className="simulation-count-badge">{pageCopy.results.count.replace('{{count}}', String(detail.totalTaskCount))}</span></div>
- <div className="simulation-result-grid">{detail.tasks.map((task) => {
- const image = task.resultImages[0] || task.templateCoverUrl || '/images/aitori-simulation-result.png'
- const templateName = task.templateSource === 'SYSTEM'
- ? resolveLocalizedTemplateName(task.templateName, task.templateNameI18n, locale, copy.systemTemplateNames)
- : resolveLocalizedTemplateName(task.templateName, task.templateNameI18n, locale)
- return <article key={task.id} className="simulation-result-card"><button type="button" className="aitori-image-button" disabled={task.taskStatus === 'PROCESSING'} onClick={() => setPreview({ image, alt: templateName })}><img className="simulation-result-image" src={image} alt={templateName} /></button><div className="simulation-result-body"><h3>{templateName}</h3><span className="template-status-badge">{task.templateSource === 'SYSTEM' ? pageCopy.source.system : pageCopy.source.stylist}</span>{task.taskStatus === 'PROCESSING' ? <img src={LOADING_ANIMATION_URL} alt={pageCopy.card.loadingAlt || '生成中'} className="simulation-result-card-loading" /> : null}<p>{task.taskStatus === 'FAILED' ? task.errorMsg || pageCopy.card.failed : task.taskStatus === 'PROCESSING' ? pageCopy.card.processing : pageCopy.card.success}</p></div></article>
- })}</div>
- </div>
- </div> : null}
- </div></section>
- <ImageLightbox image={preview?.image || null} alt={preview?.alt} onClose={() => setPreview(null)} />
- </DashboardLayout>
- )
- }
|