/** * 铸爱 — 统一「Thinking → 选择 → 详情确认 → 铸造」路由与 Storage * * 本模块管理铸爱(castlove)生成流程的页面跳转、数据持久化和状态管理。 * 支持四种模式:API生成、预填充选择、光栅工作室、镭射工作室 */ 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' /** 模式:光栅工作室(工作台生成,不走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() } /** * 持久化光栅工作室预览元数据 * 在进入光栅结果页面前调用,保存背景图、主体图、材质等信息 * * @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 } /** * 判断是否为光栅工作室类型 * @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 }