topfans/frontend/utils/castloveGenerationFlow.js

468 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 铸爱 — 统一「Thinking → 选择 → 详情确认 → 铸造」路由与 Storage
*
* 本模块管理铸爱castlove生成流程的页面跳转、数据持久化和状态管理。
* 支持四种模式API生成、预填充选择、光栅卡流程lenticular/*)、镭射工作室
*/
import {
LENTICULAR_STUDIO_STORAGE_KEY,
CASTLOVE_LASER_ENTRY_KEY,
} from '@/utils/castloveMintForm.js'
// ========== Storage Keys ==========
/** 生成流程的完整Payload包含mode、images等 */
export const GENERATION_FLOW_KEY = 'generation_flow_payload'
/** AI生成请求的数据prompt、model、aspect_ratio等 */
export const GENERATION_REQUEST_KEY = 'generation_request_data'
/** 已生成的图片列表预填充或AI生成的结果 */
export const GENERATED_IMAGES_KEY = 'generated_images'
/** 生成结果的元数据displayMode、imageCount */
export const GENERATION_RESULT_META_KEY = 'generation_result_meta'
/** 铸爱表单数据craft_name、typeName等 */
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'
// ========== Flow Modes ==========
/** 模式API生成AI生成四图 */
export const FLOW_MODE_API = 'api'
/** 模式:预填充(用户已有所需图片,直接进入选择) */
export const FLOW_MODE_PREFILLED = 'prefilled'
/** 模式光栅卡lenticular-create → thinking → result不走 AI 四图) */
export const FLOW_MODE_LENTICULAR = 'lenticular'
/** 模式镭射工作室工作台生成不走AI四图 */
export const FLOW_MODE_LASER = 'laser'
// ========== After Select Actions ==========
/** 选择后的动作:直接进入铸造 */
export const AFTER_SELECT_MINT = 'mint'
/** 选择后的动作:进入详情确认 */
export const AFTER_SELECT_DETAIL = 'detail'
// ========== Studio Types ==========
/** 工作室类型:光栅 */
export const STUDIO_LENTICULAR = 'lenticular'
/** 工作室类型:镭射 */
export const STUDIO_LASER = 'laser'
// ========== Page URLs ==========
const LOADING_URL = '/pages/discover/generation-loading'
const RESULT_URL = '/pages/discover/generation-result'
const LENTICULAR_THINKING_URL = '/pages/castlove/lenticular/lenticular-thinking'
// const ASSET_DETAIL_URL = '/pages/asset-detail/asset-detail'
// ========== Core Functions ==========
/**
* 将图片数组补足到最少指定数量
* - 如果图片数量已达到最低要求直接返回前minCount张
* - 否则循环追加图片直到达到最低数量用于保持UI四宫格布局
*
* @param {Array} images - 图片数组
* @param {number} minCount - 最少图片数量默认4
* @returns {Array} 补足后的图片数组
*/
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
}
/**
* 将表单数据持久化到Storage
* 用于在页面跳转过程中保存用户填写的表单信息
*
* @param {Object|null} formData - 表单数据对象
*/
function persistFormData(formData) {
if (formData != null) {
try {
uni.setStorageSync(CASTLOVE_FORM_KEY, JSON.stringify(formData))
} catch (e) {
console.error('persistFormData failed:', e)
if (e.name === 'QuotaExceededError' || e.message?.includes('quota')) {
return {
success: false,
error: {
title: '存储空间不足',
content: '图片数据过大,请尝试压缩图片后重新提交'
}
}
} else {
return {
success: false,
error: {
title: '存储失败',
content: '表单数据保存失败,请重试'
}
}
}
}
}
return { success: true }
}
/**
* enrich丰富表单数据添加流程控制字段
* - generation_after: 选择后的动作mint/detail
* - studio_kind: 工作室类型lenticular/laser
*
* @param {Object|null} formData - 原始表单数据
* @param {Object} options - { afterSelect, studioKind }
* @returns {Object} 丰富后的表单数据
*/
function enrichFormData(formData, { afterSelect, studioKind } = {}) {
const next = { ...(formData || {}) }
if (afterSelect) {
next.generation_after = afterSelect
}
if (studioKind) {
next.studio_kind = studioKind
}
return next
}
/**
* 将图片引用转换为可用的本地路径或Base64
* 支持三种形式:
* 1. data:URL格式 → 直接返回base64
* 2. http/https URL → 下载到本地临时文件
* 3. 本地路径 → 直接返回path
*
* @param {string} src - 图片引用URL、路径或base64
* @returns {Promise<{path: string, base64: string}>}
*/
export function materializeImageRef(src) {
const s = String(src || '').trim()
if (!s) {
return Promise.resolve({ path: '', base64: '' })
}
// data:URL 格式(如 data:image/png;base64,XXXX
if (s.startsWith('data:')) {
return Promise.resolve({ path: '', base64: s })
}
// 网络URL需要下载
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 })
}
/**
* 启动AI图片生成流程
* 1. 构建生成请求数据prompt、model、aspect_ratio等
* 2. 保存表单数据和请求数据到Storage
* 3. 设置流程模式为API模式
* 4. 跳转到加载页面
*
* @param {Object} options
* @param {string} options.prompt - 生成提示词
* @param {Object} options.formData - 表单数据craft_name等
* @param {number} options.n - 生成数量默认4
* @param {string} options.model - 模型,默认'image-01'
* @param {string} options.aspectRatio - 宽高比,默认'16:9'
* @param {string} options.afterSelect - 选择后动作默认mint
* @param {string} options.studioKind - 工作室类型
*/
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()
}
/**
* 导航到对应类型的加载页面(生成中动画)
* @param {string} studioKind - 工作室类型lenticular/laser
*/
function navigateToStudioLoading(studioKind) {
if (studioKind === STUDIO_LENTICULAR) {
uni.navigateTo({ url: LENTICULAR_THINKING_URL })
} else {
uni.navigateTo({ url: LOADING_URL })
}
}
/**
* 启动工作台生成流程(光栅/镭射)
* 与AI生成流程不同工作台生成不走通用AI四图而是直接在工作台生成预览
*
* @param {Object} options
* @param {Object} options.formData - 表单数据
* @param {string} options.studioKind - 工作室类型lenticular/laser
*/
export function startCraftGenerationFlow({ formData, studioKind }) {
const merged = enrichFormData(formData, {
afterSelect: AFTER_SELECT_DETAIL, // 工作台模式默认选择后进入详情
studioKind,
})
const result = persistFormData(merged)
if (!result.success) {
return result // { success: false, error: { title, content } }
}
uni.removeStorageSync(GENERATION_REQUEST_KEY) // 不需要AI请求数据
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, // 至少展示1600ms的加载动画
})
)
navigateToStudioLoading(studioKind)
}
/**
* 启动预填充选择流程(用户已有所需图片)
* 用户从相册或历史记录选择图片,直接进入选择界面
*
* @param {Object} options
* @param {Array} options.images - 图片数组
* @param {Object} options.formData - 表单数据
* @param {number} options.minDurationMs - 加载动画最少展示时间默认1400ms
* @param {boolean} options.padToFour - 是否补足到4张默认true
* @param {string} options.afterSelect - 选择后动作默认mint
* @param {string} options.studioKind - 工作室类型
*/
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 })
const result = persistFormData(merged)
if (!result.success) {
return result
}
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()
}
/**
* 持久化光栅卡预览元数据(写入 LENTICULAR_STUDIO_STORAGE_KEY
* 在进入光栅结果页面前调用,保存背景图、主体图、材质等信息
*
* @param {Object} formData - 包含lenticularBgImage、lenticularSubjectImage等字段
*/
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' }]))
}
/**
* 持久化镭射工作室预览图片
* 在进入镭射结果页面前调用,保存生成的图片路径列表
*
* @param {Array} paths - 镭射生成的图片路径数组
*/
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))
}
/**
* 完成图片选择并跳转到详情页
* 1. 将选中图片转换为可用的本地引用
* 2. 保存选中图片和索引到Storage
* 3. 根据工作室类型保存特定的元数据
* 4. 跳转到资产详情页
*
* @param {Object} options
* @param {string} options.selectedImage - 用户选中的图片
* @param {number} options.selectedIndex - 用户选中的图片索引
* @param {Object} options.formData - 表单数据包含studio_kind等
*/
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)}`,
// })
}
// ========== Query Helpers ==========
/**
* 判断选择后的动作是否为"详情确认"
* @param {Object} formData - 表单数据
* @returns {boolean}
*/
export function isDetailAfterSelect(formData) {
return formData?.generation_after === AFTER_SELECT_DETAIL
}
/**
* 判断是否为光栅卡工作室流程studioKind === lenticular
* @param {Object} formData - 表单数据
* @returns {boolean}
*/
export function isLenticularKind(formData) {
return formData?.studio_kind === STUDIO_LENTICULAR
}
/**
* 判断是否为镭射工作室类型
* @param {Object} formData - 表单数据
* @returns {boolean}
*/
export function isLaserKind(formData) {
return formData?.studio_kind === STUDIO_LASER
}
// ========== Payload Consumer ==========
/**
* 消费读取并清除生成流程的Payload
* 在目标页面读取后自动清除,保证一次性使用
*
* @returns {Object|null} 流程Payload包含mode、images、craft等
*/
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
}
}
// ========== Exports ==========
export { RESULT_URL }