468 lines
14 KiB
JavaScript
468 lines
14 KiB
JavaScript
/**
|
||
* 铸爱 — 统一「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 } |