|
@@ -1,269 +1,208 @@
|
|
|
-import { useEffect, useState } from 'react'
|
|
|
|
|
|
|
+import { useEffect, useState, type FormEvent } from 'react'
|
|
|
import { Link, useParams } from 'react-router-dom'
|
|
import { Link, useParams } from 'react-router-dom'
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
import { DashboardLayout } from '../../../layouts/DashboardLayout'
|
|
import { DashboardLayout } from '../../../layouts/DashboardLayout'
|
|
|
import {
|
|
import {
|
|
|
|
|
+ deleteTemplate,
|
|
|
|
|
+ getTemplateBasicInfo,
|
|
|
getTemplateMakeTaskById,
|
|
getTemplateMakeTaskById,
|
|
|
|
|
+ updateTemplateBasicInfo,
|
|
|
|
|
+ type TemplateBasicInfo,
|
|
|
type TemplateMakeTaskDetail,
|
|
type TemplateMakeTaskDetail,
|
|
|
} from '../../../services/api/templates'
|
|
} from '../../../services/api/templates'
|
|
|
import { usePageContext } from '../../../shared/pageContext'
|
|
import { usePageContext } from '../../../shared/pageContext'
|
|
|
-import { LOADING_ANIMATION_URL } from '../../../shared/uiAssets'
|
|
|
|
|
|
|
|
|
|
-const FALLBACK_PAGE_COPY = {
|
|
|
|
|
- title: '模板制作任务',
|
|
|
|
|
- description: '查看模板制作进度并等待任务完成。',
|
|
|
|
|
- badge: '模板任务',
|
|
|
|
|
- lead: '模板创建后会自动推进 step1 和 step2,请停留在当前页面等待结果。',
|
|
|
|
|
- pendingName: '正在创建模板...',
|
|
|
|
|
- resourceFallback: '暂无预览',
|
|
|
|
|
- bundleReady: '模板包已生成',
|
|
|
|
|
- bundlePending: '模板包生成后会显示',
|
|
|
|
|
- actions: {
|
|
|
|
|
- editTemplate: '编辑模板信息',
|
|
|
|
|
- backToList: '返回模板列表',
|
|
|
|
|
- },
|
|
|
|
|
- labels: {
|
|
|
|
|
- status: '任务状态',
|
|
|
|
|
- createdAt: '创建时间',
|
|
|
|
|
- taskId: '任务 ID',
|
|
|
|
|
- refHair: '参考发型',
|
|
|
|
|
- model: 'AI 模特',
|
|
|
|
|
- clothes: '服装',
|
|
|
|
|
- background: '背景',
|
|
|
|
|
- resultImage: '封面结果',
|
|
|
|
|
- error: '错误信息',
|
|
|
|
|
- },
|
|
|
|
|
|
|
+const FALLBACK_TASK_COPY = {
|
|
|
|
|
+ title: '模板制作任务', description: '查看模板制作与编辑状态。', badge: '模板任务', pendingName: '正在创建模板...', resourceFallback: '暂无预览',
|
|
|
|
|
+ actions: { editTemplate: '编辑模板信息', backToList: '返回模板列表' },
|
|
|
|
|
+ labels: { refHair: '参考发型', model: 'AI模特', clothes: '服装', background: '背景', resultImage: '封面结果' },
|
|
|
states: {
|
|
states: {
|
|
|
- WAIT: {
|
|
|
|
|
- label: '等待开始',
|
|
|
|
|
- description: '系统已创建模板记录,正在准备第一步任务。',
|
|
|
|
|
- },
|
|
|
|
|
- PROCESSING: {
|
|
|
|
|
- label: '制作中',
|
|
|
|
|
- description: '系统正在轮询模板制作状态,请稍候。',
|
|
|
|
|
- },
|
|
|
|
|
- STEP1_SUCCESS: {
|
|
|
|
|
- label: '第一步完成',
|
|
|
|
|
- description: '参考发型已解析完成,正在进入模板封面合成。',
|
|
|
|
|
- },
|
|
|
|
|
- STEP2_SUCCESS: {
|
|
|
|
|
- label: '制作完成',
|
|
|
|
|
- description: '模板封面与模板包已生成,可以进入编辑页补充模板基本信息。',
|
|
|
|
|
- },
|
|
|
|
|
- FAILED: {
|
|
|
|
|
- label: '制作失败',
|
|
|
|
|
- description: '本次模板制作未完成,请检查素材后重试。',
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ WAIT: { label: '等待开始', description: '正在准备第一步任务。' },
|
|
|
|
|
+ PROCESSING: { label: '制作中', description: '系统正在生成模板,请稍候。' },
|
|
|
|
|
+ STEP1_SUCCESS: { label: '第一步完成', description: '正在生成最终模板封面。' },
|
|
|
|
|
+ STEP2_SUCCESS: { label: '制作完成', description: '模板封面与模板包已生成。' },
|
|
|
|
|
+ FAILED: { label: '制作失败', description: '请检查素材后重试。' },
|
|
|
},
|
|
},
|
|
|
steps: {
|
|
steps: {
|
|
|
- step1: {
|
|
|
|
|
- title: 'Step 1 参考发型解析',
|
|
|
|
|
- description: '读取 step1 模板并生成中间发型图。',
|
|
|
|
|
- },
|
|
|
|
|
- step2: {
|
|
|
|
|
- title: 'Step 2 模板封面生成',
|
|
|
|
|
- description: '组合模板包并生成最终模板封面。',
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ step1: { title: 'Step 1 参考发型解析', description: '读取参考发型并生成中间图。' },
|
|
|
|
|
+ step2: { title: 'Step 2 模板封面生成', description: '组合素材并生成最终模板封面。' },
|
|
|
},
|
|
},
|
|
|
} as const
|
|
} as const
|
|
|
|
|
|
|
|
|
|
+const FALLBACK_EDIT_COPY = {
|
|
|
|
|
+ badge: '模板编辑', info: { title: '发型模板基本信息', name: '模板名称', status: '状态', category: '分类', tags: '标签', tagsPlaceholder: '层次, 空气感', description: '说明' }, preview: { imageAlt: '模板封面预览', downloadBundle: '下载模板包' }, success: { saveSucceeded: '保存成功' }, errors: { saveFailed: '保存失败' }, submitTitle: '保存模板信息', submitLead: '保存模板名称、分类、状态、标签和说明。', statusOptions: { DRAFT: '草稿', PUBLIC: '公开中' }, categoryOptions: { LONG: '长发', MEDIUM: '中长发', SHORT: '短发', DYE: '染发' },
|
|
|
|
|
+} as const
|
|
|
|
|
+
|
|
|
function formatTaskTime(locale: 'zh' | 'ja', value?: string) {
|
|
function formatTaskTime(locale: 'zh' | 'ja', value?: string) {
|
|
|
if (!value) return '--'
|
|
if (!value) return '--'
|
|
|
-
|
|
|
|
|
const date = new Date(value)
|
|
const date = new Date(value)
|
|
|
- if (Number.isNaN(date.getTime())) return value
|
|
|
|
|
-
|
|
|
|
|
- return new Intl.DateTimeFormat(locale === 'ja' ? 'ja-JP' : 'zh-CN', {
|
|
|
|
|
- dateStyle: 'medium',
|
|
|
|
|
- timeStyle: 'short',
|
|
|
|
|
- }).format(date)
|
|
|
|
|
|
|
+ return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(locale === 'ja' ? 'ja-JP' : 'zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function getTaskTone(status?: TemplateMakeTaskDetail['taskStatus']) {
|
|
|
|
|
- if (status === 'STEP2_SUCCESS') return 'success'
|
|
|
|
|
|
|
+function taskTone(status?: string) {
|
|
|
|
|
+ if (status === 'STEP2_SUCCESS' || status === 'SUCCESSES') return 'success'
|
|
|
if (status === 'FAILED') return 'failed'
|
|
if (status === 'FAILED') return 'failed'
|
|
|
if (status === 'PROCESSING' || status === 'STEP1_SUCCESS') return 'processing'
|
|
if (status === 'PROCESSING' || status === 'STEP1_SUCCESS') return 'processing'
|
|
|
return 'waiting'
|
|
return 'waiting'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function TemplateMakeTaskPage() {
|
|
export function TemplateMakeTaskPage() {
|
|
|
- const params = useParams<{ taskId: string }>()
|
|
|
|
|
- const { locale, path, copy } = usePageContext()
|
|
|
|
|
- const pageCopy = copy.templateTaskPage ?? FALLBACK_PAGE_COPY
|
|
|
|
|
|
|
+ const { taskId } = useParams<{ taskId: string }>()
|
|
|
|
|
+ const { locale, navigate, path, copy } = usePageContext()
|
|
|
|
|
+ const rawTaskCopy = copy.templateTaskPage ?? {}
|
|
|
|
|
+ const pageCopy = {
|
|
|
|
|
+ ...FALLBACK_TASK_COPY,
|
|
|
|
|
+ ...rawTaskCopy,
|
|
|
|
|
+ actions: { ...FALLBACK_TASK_COPY.actions, ...rawTaskCopy.actions },
|
|
|
|
|
+ labels: { ...FALLBACK_TASK_COPY.labels, ...rawTaskCopy.labels },
|
|
|
|
|
+ states: { ...FALLBACK_TASK_COPY.states, ...rawTaskCopy.states },
|
|
|
|
|
+ steps: { ...FALLBACK_TASK_COPY.steps, ...rawTaskCopy.steps },
|
|
|
|
|
+ }
|
|
|
|
|
+ const rawEditCopy = copy.templateEditPage ?? {}
|
|
|
|
|
+ const commonCopy = copy.common ?? { sending: '保存中...', save: '保存', cancel: '取消' }
|
|
|
|
|
+ const editCopy = {
|
|
|
|
|
+ ...FALLBACK_EDIT_COPY,
|
|
|
|
|
+ ...rawEditCopy,
|
|
|
|
|
+ info: { ...FALLBACK_EDIT_COPY.info, ...rawEditCopy.info },
|
|
|
|
|
+ preview: { ...FALLBACK_EDIT_COPY.preview, ...rawEditCopy.preview },
|
|
|
|
|
+ success: { ...FALLBACK_EDIT_COPY.success, ...rawEditCopy.success },
|
|
|
|
|
+ errors: { ...FALLBACK_EDIT_COPY.errors, ...rawEditCopy.errors },
|
|
|
|
|
+ statusOptions: { ...FALLBACK_EDIT_COPY.statusOptions, ...rawEditCopy.statusOptions },
|
|
|
|
|
+ categoryOptions: { ...FALLBACK_EDIT_COPY.categoryOptions, ...rawEditCopy.categoryOptions },
|
|
|
|
|
+ }
|
|
|
const [task, setTask] = useState<TemplateMakeTaskDetail | null>(null)
|
|
const [task, setTask] = useState<TemplateMakeTaskDetail | null>(null)
|
|
|
|
|
+ const [template, setTemplate] = useState<TemplateBasicInfo | null>(null)
|
|
|
|
|
+ const [templateName, setTemplateName] = useState('')
|
|
|
|
|
+ const [status, setStatus] = useState<TemplateBasicInfo['status']>('DRAFT')
|
|
|
|
|
+ const [category, setCategory] = useState<TemplateBasicInfo['category']>('LONG')
|
|
|
|
|
+ const [tags, setTags] = useState('')
|
|
|
|
|
+ const [description, setDescription] = useState('')
|
|
|
|
|
+ const [saving, setSaving] = useState(false)
|
|
|
|
|
+ const [message, setMessage] = useState('')
|
|
|
|
|
+ const [error, setError] = useState('')
|
|
|
|
|
+ const [deleteOpen, setDeleteOpen] = useState(false)
|
|
|
|
|
|
|
|
- const taskStatus = task?.taskStatus ?? 'WAIT'
|
|
|
|
|
- const stateCopy =
|
|
|
|
|
- pageCopy.states?.[taskStatus as keyof typeof pageCopy.states] ??
|
|
|
|
|
- FALLBACK_PAGE_COPY.states[taskStatus as keyof typeof FALLBACK_PAGE_COPY.states]
|
|
|
|
|
- const step1Copy = pageCopy.steps?.step1 ?? FALLBACK_PAGE_COPY.steps.step1
|
|
|
|
|
- const step2Copy = pageCopy.steps?.step2 ?? FALLBACK_PAGE_COPY.steps.step2
|
|
|
|
|
- const labels = pageCopy.labels ?? FALLBACK_PAGE_COPY.labels
|
|
|
|
|
- const actions = pageCopy.actions ?? FALLBACK_PAGE_COPY.actions
|
|
|
|
|
- const tone = getTaskTone(taskStatus)
|
|
|
|
|
- const previewItems = [
|
|
|
|
|
- {
|
|
|
|
|
- key: 'refHair',
|
|
|
|
|
- label: labels.refHair,
|
|
|
|
|
- image: task?.refHairUrl ?? null,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- key: 'model',
|
|
|
|
|
- label: labels.model,
|
|
|
|
|
- image: task?.modelUrl ?? null,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- key: 'clothes',
|
|
|
|
|
- label: labels.clothes,
|
|
|
|
|
- image: task?.clothesUrl ?? null,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- key: 'background',
|
|
|
|
|
- label: labels.background,
|
|
|
|
|
- image: task?.backgroundUrl ?? null,
|
|
|
|
|
- },
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ const complete = task?.taskStatus === 'STEP2_SUCCESS'
|
|
|
|
|
+ const generating = !complete
|
|
|
|
|
+ const currentState = task ? pageCopy.states[task.taskStatus] : pageCopy.states.WAIT
|
|
|
|
|
+ const previewItems = task ? [
|
|
|
|
|
+ [pageCopy.labels.refHair, task.refHairUrl],
|
|
|
|
|
+ [pageCopy.labels.model, task.modelUrl],
|
|
|
|
|
+ [pageCopy.labels.clothes, task.clothesUrl],
|
|
|
|
|
+ [pageCopy.labels.background, task.backgroundUrl],
|
|
|
|
|
+ ] : []
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
let cancelled = false
|
|
let cancelled = false
|
|
|
let timer: number | undefined
|
|
let timer: number | undefined
|
|
|
-
|
|
|
|
|
|
|
+ const applyTemplate = (data: TemplateBasicInfo) => {
|
|
|
|
|
+ setTemplate(data)
|
|
|
|
|
+ setTemplateName(data.templateName)
|
|
|
|
|
+ setStatus(data.status)
|
|
|
|
|
+ setCategory(data.category)
|
|
|
|
|
+ setTags(data.tags.join(', '))
|
|
|
|
|
+ setDescription(data.description)
|
|
|
|
|
+ }
|
|
|
const load = async () => {
|
|
const load = async () => {
|
|
|
- if (!params.taskId) return
|
|
|
|
|
- const data = await getTemplateMakeTaskById(params.taskId)
|
|
|
|
|
- if (cancelled) return
|
|
|
|
|
- setTask(data)
|
|
|
|
|
-
|
|
|
|
|
- if (['WAIT', 'PROCESSING', 'STEP1_SUCCESS'].includes(data.taskStatus)) {
|
|
|
|
|
- timer = window.setTimeout(() => {
|
|
|
|
|
- void load()
|
|
|
|
|
- }, 1500)
|
|
|
|
|
|
|
+ if (!taskId) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await getTemplateMakeTaskById(taskId)
|
|
|
|
|
+ if (cancelled) return
|
|
|
|
|
+ setTask(data)
|
|
|
|
|
+ setError('')
|
|
|
|
|
+ if (data.taskStatus === 'STEP2_SUCCESS') {
|
|
|
|
|
+ const info = await getTemplateBasicInfo(data.templateId)
|
|
|
|
|
+ if (!cancelled) applyTemplate(info)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (['WAIT', 'PROCESSING', 'STEP1_SUCCESS'].includes(data.taskStatus)) {
|
|
|
|
|
+ timer = window.setTimeout(() => void load(), 1500)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (!cancelled) setError(err instanceof Error ? err.message : FALLBACK_TASK_COPY.states.FAILED.description)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
void load()
|
|
void load()
|
|
|
|
|
+ return () => { cancelled = true; if (timer) window.clearTimeout(timer) }
|
|
|
|
|
+ }, [taskId])
|
|
|
|
|
|
|
|
- return () => {
|
|
|
|
|
- cancelled = true
|
|
|
|
|
- if (timer) {
|
|
|
|
|
- window.clearTimeout(timer)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const handleSave = async (event: FormEvent<HTMLFormElement>) => {
|
|
|
|
|
+ event.preventDefault()
|
|
|
|
|
+ if (!task || saving) return
|
|
|
|
|
+ setSaving(true); setError(''); setMessage('')
|
|
|
|
|
+ try {
|
|
|
|
|
+ const updated = await updateTemplateBasicInfo(task.templateId, {
|
|
|
|
|
+ templateName: templateName.trim(), status, category,
|
|
|
|
|
+ tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
|
|
|
+ description: description.trim(),
|
|
|
|
|
+ })
|
|
|
|
|
+ setTemplate(updated)
|
|
|
|
|
+ setMessage(editCopy.success.saveSucceeded)
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setError(err instanceof Error ? err.message : editCopy.errors.saveFailed)
|
|
|
|
|
+ } finally { setSaving(false) }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleDelete = async () => {
|
|
|
|
|
+ if (!task) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ await deleteTemplate(task.templateId)
|
|
|
|
|
+ navigate(path('/templates'))
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setDeleteOpen(false)
|
|
|
|
|
+ setError(err instanceof Error ? err.message : pageCopy.delete?.failed || '删除失败')
|
|
|
}
|
|
}
|
|
|
- }, [params.taskId])
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<DashboardLayout>
|
|
<DashboardLayout>
|
|
|
- <Helmet>
|
|
|
|
|
- <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
|
|
|
|
|
- <title>{`Aitori Stylist | ${pageCopy.title}`}</title>
|
|
|
|
|
- <meta name="description" content={pageCopy.description} />
|
|
|
|
|
- </Helmet>
|
|
|
|
|
- <section className="stylist-workbench-top ipad-top-space-margin mb-2">
|
|
|
|
|
|
|
+ <Helmet><html lang={locale === 'ja' ? 'ja' : 'zh-CN'} /><title>{`Aitori Stylist | ${pageCopy.title}`}</title><meta name="description" content={pageCopy.description} /></Helmet>
|
|
|
|
|
+ <section className={`stylist-workbench-top ipad-top-space-margin mb-2 template-task-demo ${complete ? 'is-complete' : 'is-generating'}`}>
|
|
|
<div className="container">
|
|
<div className="container">
|
|
|
- <div className="template-task-layout">
|
|
|
|
|
- <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px template-task-summary-panel">
|
|
|
|
|
- <span className="fs-14 fw-700 text-base-color text-uppercase">
|
|
|
|
|
- {pageCopy.badge}
|
|
|
|
|
- </span>
|
|
|
|
|
- <h1 className="fw-700 text-dark-gray fs-30 mb-10px mt-15px">
|
|
|
|
|
- {pageCopy.title}
|
|
|
|
|
- </h1>
|
|
|
|
|
- <p className="fs-16 text-medium-gray mb-25px">
|
|
|
|
|
- {pageCopy.lead}
|
|
|
|
|
- </p>
|
|
|
|
|
- <div className={`template-task-state-pill is-${tone}`}>
|
|
|
|
|
- <span>{labels.status}</span>
|
|
|
|
|
- <strong>{stateCopy?.label ?? taskStatus}</strong>
|
|
|
|
|
- </div>
|
|
|
|
|
- {tone === 'processing' || tone === 'waiting' ? (
|
|
|
|
|
- <img src={LOADING_ANIMATION_URL} alt="模板制作中" className="template-task-loading-gif" />
|
|
|
|
|
- ) : null}
|
|
|
|
|
- <div className="template-task-headline">
|
|
|
|
|
- <h2 className="fw-700 text-dark-gray fs-24 mb-10px">
|
|
|
|
|
- {task?.templateName || pageCopy.pendingName || FALLBACK_PAGE_COPY.pendingName}
|
|
|
|
|
- </h2>
|
|
|
|
|
- <p className="fs-15 text-medium-gray mb-0">
|
|
|
|
|
- {stateCopy?.description ?? pageCopy.lead}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="template-task-meta-list">
|
|
|
|
|
- <div>
|
|
|
|
|
- <span>{labels.createdAt}</span>
|
|
|
|
|
- <strong>{formatTaskTime(locale, task?.createTime)}</strong>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <span>{labels.taskId}</span>
|
|
|
|
|
- <strong>{params.taskId ?? '--'}</strong>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- {task?.errorMessage ? (
|
|
|
|
|
- <div className="template-task-error-box">
|
|
|
|
|
- <span>{labels.error}</span>
|
|
|
|
|
- <strong>{task.errorMessage}</strong>
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : null}
|
|
|
|
|
- {task?.taskStatus === 'STEP2_SUCCESS' ? (
|
|
|
|
|
- <Link
|
|
|
|
|
- to={path(`/templates/${task.templateId}/edit?taskId=${params.taskId ?? ''}`)}
|
|
|
|
|
- className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 mt-20px"
|
|
|
|
|
- >
|
|
|
|
|
- {actions.editTemplate}
|
|
|
|
|
- </Link>
|
|
|
|
|
- ) : null}
|
|
|
|
|
- <Link
|
|
|
|
|
- to={path('/templates')}
|
|
|
|
|
- className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 mt-10px"
|
|
|
|
|
- >
|
|
|
|
|
- {actions.backToList}
|
|
|
|
|
- </Link>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
|
|
|
- <div className="template-task-step-grid">
|
|
|
|
|
- <div className={`template-task-step-card is-${getTaskTone(task?.step1.resultImage ? 'STEP2_SUCCESS' : task?.step1.status as TemplateMakeTaskDetail['taskStatus'])}`}>
|
|
|
|
|
- <div className="template-task-step-header">
|
|
|
|
|
- <strong>{step1Copy.title}</strong>
|
|
|
|
|
- <span>{task?.step1.status || 'WAIT'}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p>{step1Copy.description}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className={`template-task-step-card is-${getTaskTone(task?.step2.resultImage ? 'STEP2_SUCCESS' : task?.step2.status as TemplateMakeTaskDetail['taskStatus'])}`}>
|
|
|
|
|
- <div className="template-task-step-header">
|
|
|
|
|
- <strong>{step2Copy.title}</strong>
|
|
|
|
|
- <span>{task?.step2.status || 'WAIT'}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p>{step2Copy.description}</p>
|
|
|
|
|
- {task?.step2.resultImage ? (
|
|
|
|
|
- <img
|
|
|
|
|
- src={task.step2.resultImage}
|
|
|
|
|
- alt={labels.resultImage}
|
|
|
|
|
- className="template-task-step-preview"
|
|
|
|
|
- />
|
|
|
|
|
- ) : null}
|
|
|
|
|
|
|
+ <div className="template-task-page-head mb-4">
|
|
|
|
|
+ <div><span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.badge}</span><h1 className="fw-700 text-dark-gray fs-30 mb-10px mt-10px">{task?.templateName || pageCopy.pendingName}</h1><p className="mb-0 text-medium-gray">{currentState.description}</p></div>
|
|
|
|
|
+ <div className="template-task-head-actions"><Link to={path('/templates')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{pageCopy.actions.backToList}</Link>{complete ? <a href="#template-edit-section" className="btn btn-small btn-rounded btn-base-color fw-700">{pageCopy.actions.editTemplate}</a> : null}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {error ? <p className="alert alert-danger" role="alert">{error}</p> : null}
|
|
|
|
|
+ {message ? <p className="alert alert-success" role="status">{message}</p> : null}
|
|
|
|
|
+
|
|
|
|
|
+ <details key={complete ? 'complete' : 'generating'} className="template-task-history" open={generating}>
|
|
|
|
|
+ <summary><span>{pageCopy.badge}</span><strong>{complete ? pageCopy.history?.closedTitle || '查看生成任务信息' : currentState.label}</strong><em>{formatTaskTime(locale, task?.createTime)}</em></summary>
|
|
|
|
|
+ <div className="template-task-generating-visual"><div className="template-task-generating-gif"><span></span><span></span><span></span></div><div><strong>{currentState.label}</strong><p>{currentState.description}</p></div></div>
|
|
|
|
|
+ <div className="template-task-detail-layout">
|
|
|
|
|
+ <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
|
|
|
+ <h2 className="fw-700 text-dark-gray fs-24 mb-20px">{pageCopy.history?.stepsTitle || '生成步骤'}</h2>
|
|
|
|
|
+ <div className="template-task-step-grid">
|
|
|
|
|
+ <div className={`template-task-step-card is-${taskTone(task?.step1.resultImage ? 'SUCCESSES' : task?.step1.status)}`}><div className="template-task-step-header"><strong>{pageCopy.steps.step1.title}</strong><span>{task?.step1.status || 'WAIT'}</span></div><p>{pageCopy.steps.step1.description}</p></div>
|
|
|
|
|
+ <div className={`template-task-step-card is-${taskTone(task?.step2.resultImage ? 'SUCCESSES' : task?.step2.status)}`}><div className="template-task-step-header"><strong>{pageCopy.steps.step2.title}</strong><span>{task?.step2.status || 'WAIT'}</span></div><p>{pageCopy.steps.step2.description}</p>{task?.step2.resultImage ? <img className="template-task-step-preview" src={task.step2.resultImage} alt={pageCopy.labels.resultImage} /> : null}</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="template-task-resource-grid">
|
|
|
|
|
- {previewItems.map((item) => (
|
|
|
|
|
- <div key={item.key} className="template-task-resource-card">
|
|
|
|
|
- {item.image ? (
|
|
|
|
|
- <img src={item.image} alt={item.label} />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div className="template-task-resource-placeholder">
|
|
|
|
|
- {pageCopy.resourceFallback || FALLBACK_PAGE_COPY.resourceFallback}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <div>
|
|
|
|
|
- <span>{item.label}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
|
|
|
+ <h2 className="fw-700 text-dark-gray fs-24 mb-20px">{pageCopy.history?.assetsTitle || '输入素材'}</h2>
|
|
|
|
|
+ <div className="template-task-resource-grid">{previewItems.map(([label, image]) => <div key={label} className="template-task-resource-card">{image ? <img src={image} alt={label} /> : <div>{pageCopy.resourceFallback}</div>}<div><span>{label}</span></div></div>)}</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="template-task-bundle-note">
|
|
|
|
|
- <span>{task?.step2.bundleUrl ? pageCopy.bundleReady || FALLBACK_PAGE_COPY.bundleReady : pageCopy.bundlePending || FALLBACK_PAGE_COPY.bundlePending}</span>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </details>
|
|
|
|
|
+
|
|
|
|
|
+ {complete && template ? <form id="template-edit-section" className="template-task-edit-section workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mt-4" onSubmit={handleSave}>
|
|
|
|
|
+ <div className="template-task-edit-head mb-25px"><div><span className="fs-14 fw-700 text-base-color text-uppercase">{editCopy.badge}</span><h2 className="fw-700 text-dark-gray fs-26 mb-0">{editCopy.info.title}</h2></div><span className="template-status-badge">{editCopy.statusOptions[status]}</span></div>
|
|
|
|
|
+ <div className="template-task-edit-layout">
|
|
|
|
|
+ <div><img className="template-task-cover-preview mt-0" src={template.coverUrl || task.step2.resultImage || task.refHairUrl} alt={editCopy.preview.imageAlt} />{template.templateUrl ? <a href={template.templateUrl} target="_blank" rel="noreferrer" className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 w-100 mt-15px">{editCopy.preview.downloadBundle}</a> : null}</div>
|
|
|
|
|
+ <div className="row g-3">
|
|
|
|
|
+ <div className="col-md-7"><label htmlFor="task-template-name" className="customer-edit-label">{editCopy.info.name}</label><input id="task-template-name" className="form-control customer-edit-control" value={templateName} onChange={(event) => setTemplateName(event.target.value)} required /></div>
|
|
|
|
|
+ <div className="col-md-5"><label htmlFor="task-template-status" className="customer-edit-label">{editCopy.info.status}</label><select id="task-template-status" className="form-select customer-edit-control" value={status} onChange={(event) => setStatus(event.target.value as TemplateBasicInfo['status'])}><option value="DRAFT">{editCopy.statusOptions.DRAFT}</option><option value="PUBLIC">{editCopy.statusOptions.PUBLIC}</option></select></div>
|
|
|
|
|
+ <div className="col-md-6"><label htmlFor="task-template-category" className="customer-edit-label">{editCopy.info.category}</label><select id="task-template-category" className="form-select customer-edit-control" value={category} onChange={(event) => setCategory(event.target.value as TemplateBasicInfo['category'])}><option value="LONG">{editCopy.categoryOptions.LONG}</option><option value="MEDIUM">{editCopy.categoryOptions.MEDIUM}</option><option value="SHORT">{editCopy.categoryOptions.SHORT}</option><option value="DYE">{editCopy.categoryOptions.DYE}</option></select></div>
|
|
|
|
|
+ <div className="col-md-6"><label htmlFor="task-template-tags" className="customer-edit-label">{editCopy.info.tags}</label><input id="task-template-tags" className="form-control customer-edit-control" value={tags} onChange={(event) => setTags(event.target.value)} placeholder={editCopy.info.tagsPlaceholder} /></div>
|
|
|
|
|
+ <div className="col-12"><label htmlFor="task-template-description" className="customer-edit-label">{editCopy.info.description}</label><textarea id="task-template-description" className="form-control customer-edit-control" rows={6} value={description} onChange={(event) => setDescription(event.target.value)} /></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div className="customer-edit-actions border-radius-8px p-25px mt-4"><div><strong>{editCopy.submitTitle}</strong><span>{editCopy.submitLead}</span></div><div className="d-flex flex-wrap gap-2"><button type="button" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-red text-red fw-700" onClick={() => setDeleteOpen(true)}>{pageCopy.delete?.button || '删除模板'}</button><button type="submit" disabled={saving} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700">{saving ? commonCopy.sending : commonCopy.save}</button></div></div>
|
|
|
|
|
+ </form> : null}
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
+
|
|
|
|
|
+ {deleteOpen ? <dialog className="template-delete-dialog" open aria-modal="true"><h2 className="fw-700 text-dark-gray fs-24">{pageCopy.delete?.title || '确认删除模板?'}</h2><p>{pageCopy.delete?.body || '删除后模板和生成任务将不再显示,且无法恢复。'}</p><div className="d-flex justify-content-end gap-2"><button type="button" className="btn btn-small btn-rounded btn-transparent-light-gray border" onClick={() => setDeleteOpen(false)}>{commonCopy.cancel}</button><button type="button" className="btn btn-small btn-rounded btn-base-color" onClick={() => void handleDelete()}>{pageCopy.delete?.confirm || '确认删除'}</button></div></dialog> : null}
|
|
|
</DashboardLayout>
|
|
</DashboardLayout>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|