topfans/frontend/utils/castloveGenerationFlow.js
2026-05-16 02:42:32 +08:00

276 lines
7.6 KiB
JavaScript

/**
* 铸爱 — 统一「Thinking → 选择 → 详情确认 → 铸造」路由与 Storage
*/
import {
LENTICULAR_STUDIO_STORAGE_KEY,
CASTLOVE_LASER_ENTRY_KEY,
} from '@/utils/castloveMintForm.js'
export const GENERATION_FLOW_KEY = 'generation_flow_payload'
export const GENERATION_REQUEST_KEY = 'generation_request_data'
export const GENERATED_IMAGES_KEY = 'generated_images'
export const GENERATION_RESULT_META_KEY = 'generation_result_meta'
export const CASTLOVE_FORM_KEY = 'castlove_form_data'
export const CRAFT_SELECTED_IMAGE_KEY = 'craft_selected_image'
export const CRAFT_SELECTED_INDEX_KEY = 'craft_selected_index'
export const FLOW_MODE_API = 'api'
export const FLOW_MODE_PREFILLED = 'prefilled'
export const FLOW_MODE_LENTICULAR = 'lenticular'
export const FLOW_MODE_LASER = 'laser'
export const AFTER_SELECT_MINT = 'mint'
export const AFTER_SELECT_DETAIL = 'detail'
export const STUDIO_LENTICULAR = 'lenticular'
export const STUDIO_LASER = 'laser'
const LOADING_URL = '/pages/discover/generation-loading'
const RESULT_URL = '/pages/discover/generation-result'
const ASSET_DETAIL_URL = '/pages/asset-detail/asset-detail'
export function padImagesForSelection(images, minCount = 4) {
if (!Array.isArray(images) || images.length === 0) {
return []
}
if (images.length >= minCount) {
return images.slice(0, minCount)
}
const out = [...images]
while (out.length < minCount) {
out.push(images[out.length % images.length])
}
return out
}
function persistFormData(formData) {
if (formData != null) {
uni.setStorageSync(CASTLOVE_FORM_KEY, JSON.stringify(formData))
}
}
function enrichFormData(formData, { afterSelect, studioKind } = {}) {
const next = { ...(formData || {}) }
if (afterSelect) {
next.generation_after = afterSelect
}
if (studioKind) {
next.studio_kind = studioKind
}
return next
}
export function materializeImageRef(src) {
const s = String(src || '').trim()
if (!s) {
return Promise.resolve({ path: '', base64: '' })
}
if (s.startsWith('data:')) {
return Promise.resolve({ path: '', base64: s })
}
if (s.startsWith('http://') || s.startsWith('https://')) {
return new Promise((resolve, reject) => {
uni.downloadFile({
url: s,
success: (res) => {
if (res.statusCode === 200 && res.tempFilePath) {
resolve({ path: res.tempFilePath, base64: '' })
} else {
reject(new Error('图片下载失败'))
}
},
fail: (err) => reject(err || new Error('图片下载失败')),
})
})
}
return Promise.resolve({ path: s, base64: '' })
}
function navigateToLoading() {
uni.navigateTo({ url: LOADING_URL })
}
export function startAiImageGenerationFlow({
prompt,
formData,
n = 4,
model = 'image-01',
aspectRatio = '16:9',
afterSelect = AFTER_SELECT_MINT,
studioKind = '',
}) {
const requestData = {
prompt,
model,
aspect_ratio: aspectRatio,
subject_reference: [{ type: 'character', image_file: '' }],
n,
}
const merged = enrichFormData(formData, { afterSelect, studioKind })
persistFormData(merged)
uni.setStorageSync(GENERATION_REQUEST_KEY, JSON.stringify(requestData))
uni.setStorageSync(
GENERATION_FLOW_KEY,
JSON.stringify({
mode: FLOW_MODE_API,
craft: merged?.craft_name || merged?.typeName || '',
afterSelect,
studioKind,
})
)
navigateToLoading()
}
/** 光栅 / 镭射:工作台生成预览,不走通用 AI 四图 */
export function startCraftGenerationFlow({ formData, studioKind }) {
const merged = enrichFormData(formData, {
afterSelect: AFTER_SELECT_DETAIL,
studioKind,
})
persistFormData(merged)
uni.removeStorageSync(GENERATION_REQUEST_KEY)
const mode =
studioKind === STUDIO_LENTICULAR ? FLOW_MODE_LENTICULAR : FLOW_MODE_LASER
uni.setStorageSync(
GENERATION_FLOW_KEY,
JSON.stringify({
mode,
studioKind,
afterSelect: AFTER_SELECT_DETAIL,
craft: merged?.craft_name || merged?.typeName || '',
minDurationMs: 1600,
})
)
navigateToLoading()
}
export function startPrefilledSelectionFlow({
images,
formData,
minDurationMs = 1400,
padToFour = true,
afterSelect = AFTER_SELECT_MINT,
studioKind = '',
}) {
const list = padToFour ? padImagesForSelection(images) : images
if (!list.length) {
throw new Error('startPrefilledSelectionFlow: images 不能为空')
}
const merged = enrichFormData(formData, { afterSelect, studioKind })
persistFormData(merged)
uni.removeStorageSync(GENERATION_REQUEST_KEY)
uni.setStorageSync(
GENERATION_FLOW_KEY,
JSON.stringify({
mode: FLOW_MODE_PREFILLED,
images: list,
minDurationMs,
craft: merged?.craft_name || merged?.typeName || '',
afterSelect,
studioKind,
})
)
navigateToLoading()
}
export function persistLenticularPreviewMeta(formData) {
uni.setStorageSync(
LENTICULAR_STUDIO_STORAGE_KEY,
JSON.stringify({
bgPath: formData.lenticularBgImage || '',
subjectPath: formData.lenticularSubjectImage || '',
bgBase64: formData.lenticularBgBase64 || '',
subjectBase64: formData.lenticularSubjectBase64 || '',
nftInfo: formData.info || formData.nftInfo || '',
materialTypeIndex: formData.materialTypeIndex ?? 0,
aiDescription: formData.aiDescription || '',
})
)
uni.setStorageSync(
GENERATION_RESULT_META_KEY,
JSON.stringify({ displayMode: STUDIO_LENTICULAR, imageCount: 1 })
)
uni.setStorageSync(GENERATED_IMAGES_KEY, JSON.stringify([{ type: 'lenticular' }]))
}
export function persistLaserPreviewImages(paths) {
uni.setStorageSync(
GENERATION_RESULT_META_KEY,
JSON.stringify({ displayMode: STUDIO_LASER, imageCount: paths.length })
)
uni.setStorageSync(GENERATED_IMAGES_KEY, JSON.stringify(paths))
}
export async function completeSelectionAndOpenDetail({
selectedImage,
selectedIndex,
formData,
}) {
const img = await materializeImageRef(selectedImage)
const storedImage = img.path || img.base64 || selectedImage
uni.setStorageSync(CRAFT_SELECTED_IMAGE_KEY, storedImage)
uni.setStorageSync(CRAFT_SELECTED_INDEX_KEY, String(selectedIndex ?? 0))
if (formData?.studio_kind === STUDIO_LENTICULAR) {
const subjectRef = storedImage
uni.setStorageSync(
LENTICULAR_STUDIO_STORAGE_KEY,
JSON.stringify({
bgPath: formData.lenticularBgImage || '',
subjectPath: subjectRef,
bgBase64: formData.lenticularBgBase64 || '',
subjectBase64: img.base64 || formData.lenticularSubjectBase64 || '',
nftInfo: formData.info || formData.nftInfo || '',
materialTypeIndex: formData.materialTypeIndex ?? 0,
aiDescription: formData.aiDescription || '',
})
)
} else if (formData?.studio_kind === STUDIO_LASER) {
uni.setStorageSync(
CASTLOVE_LASER_ENTRY_KEY,
JSON.stringify({
nftInfo: formData.info || formData.nftInfo || '',
materialTypes: formData.materialTypes || [],
materialTypeIndex: formData.materialTypeIndex ?? 0,
pageName: formData.craft_name || formData.typeName || '',
uploadedImage: storedImage,
uploadedImageBase64: img.base64 || formData.uploadedImageBase64 || '',
})
)
}
const kind = formData?.studio_kind || ''
uni.navigateTo({
url: `${ASSET_DETAIL_URL}?from=craft_confirm&studio_kind=${encodeURIComponent(kind)}`,
})
}
export function isDetailAfterSelect(formData) {
return formData?.generation_after === AFTER_SELECT_DETAIL
}
export function isLenticularKind(formData) {
return formData?.studio_kind === STUDIO_LENTICULAR
}
export function isLaserKind(formData) {
return formData?.studio_kind === STUDIO_LASER
}
export function consumeGenerationFlowPayload() {
try {
const raw = uni.getStorageSync(GENERATION_FLOW_KEY)
uni.removeStorageSync(GENERATION_FLOW_KEY)
if (!raw) {
return null
}
return JSON.parse(raw)
} catch (e) {
console.error('[castloveGenerationFlow] consume payload', e)
return null
}
}
export { RESULT_URL }