From 39209849ccf7e1ab9ca85b5c6f45b80b7190710c Mon Sep 17 00:00:00 2001 From: liulong <18539103286> Date: Fri, 15 May 2026 16:53:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=85=89=E6=A0=85?= =?UTF-8?q?=E5=8D=A1=E4=B8=8E=E9=95=AD=E5=B0=84=E5=8D=A1=E4=B8=8E=E6=96=B0?= =?UTF-8?q?=E7=89=88UI=E7=9A=84=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/lenticular/LenticularCard.vue | 16 +- .../useLenticularCraftTiltPreview.js | 163 +++++ frontend/pages/asset-detail/asset-detail.vue | 297 ++++++++- frontend/pages/castlove/create.vue | 266 ++++---- frontend/pages/castlove/success.vue | 25 +- frontend/pages/discover/create-feed.vue | 29 +- .../pages/discover/generation-loading.vue | 251 ++++---- frontend/pages/discover/generation-result.vue | 287 +++++++-- frontend/utils/castloveGenerationFlow.js | 275 +++++++++ frontend/utils/craftMintSubmit.js | 125 ++++ frontend/utils/laser-card/laserBatchExport.js | 579 ++++++++++++++++++ 11 files changed, 1951 insertions(+), 362 deletions(-) create mode 100644 frontend/composables/useLenticularCraftTiltPreview.js create mode 100644 frontend/utils/castloveGenerationFlow.js create mode 100644 frontend/utils/craftMintSubmit.js create mode 100644 frontend/utils/laser-card/laserBatchExport.js diff --git a/frontend/components/lenticular/LenticularCard.vue b/frontend/components/lenticular/LenticularCard.vue index 83e1fa6..a6b06e5 100644 --- a/frontend/components/lenticular/LenticularCard.vue +++ b/frontend/components/lenticular/LenticularCard.vue @@ -70,25 +70,11 @@ const pageProxy = getCurrentInstance()?.proxy const MOTION_HIDE_PX = 0.45 +/** 仅在画面因倾斜产生可见位移后隐藏提示(非陀螺仪连接即隐藏) */ function shouldHideTiltHint() { - if ( - props.gyroSource === 'accelerometer' || - props.gyroSource === 'gyroscope' || - props.gyroSource === 'orientation' - ) { - return true - } return Object.values(props.transforms || {}).some((t) => Math.abs(t.x) > MOTION_HIDE_PX) } -watch( - () => props.gyroSource, - () => { - if (shouldHideTiltHint()) showHint.value = false - }, - { immediate: true } -) - watch( () => props.transforms, () => { diff --git a/frontend/composables/useLenticularCraftTiltPreview.js b/frontend/composables/useLenticularCraftTiltPreview.js new file mode 100644 index 0000000..c000c1a --- /dev/null +++ b/frontend/composables/useLenticularCraftTiltPreview.js @@ -0,0 +1,163 @@ +/** + * 铸爱流程中的光栅预览:与 lenticular-studio 一致的物理参数、离散档位与陀螺仪启动节奏 + */ +import { ref, nextTick } from 'vue' +import { useLenticularPreview } from '@/composables/useLenticularPreview.js' +import { useLenticularStudioTilt } from '@/composables/useLenticularStudioTilt.js' + +const FLOW_PHYSICS = { + angleStability: 52, + transitionSmoothness: 40, + tiltSensitivity: 96, + sensorDeadzoneStrength: 0, + parallaxDepth: 18, + lenticularAnchorFloor: 0.1, + lenticularNonDominantResidualMin: 0.092, + lenticularPrevLayerGhostMin: 0.098, + lenticularBlendBaseScale: 1.1, +} + +const DISCRETE_STEP_DEG = 13 +const DISCRETE_STEP_HYST_DEG = 5 +const TILT_MAG_TAU_MS = 150 +const TILT_MAG_MAX_RISE_DPS = 82 +const TILT_MAG_MAX_FALL_DPS = 168 +const TILT_AWAY_FROM_LEVEL_DPS = 72 +const TILT_RISE_TIGHTEN = 0.42 +export const LENTICULAR_TILT_START_DELAY_MS = 560 + +/** + * @param {import('vue').Ref|import('vue').Ref} layersRef + */ +export function useLenticularCraftTiltPreview(layersRef) { + const gyroSourceLabel = ref('simulation') + const { physics, layerTransforms, simulate, relax, snapSimulatedTilt } = useLenticularPreview(layersRef) + Object.assign(physics, FLOW_PHYSICS) + + const discreteStableStep = ref(0) + let tiltMagEma = 0 + let lastTiltMagSampleAt = 0 + let lastSignedDegHint = null + + function resetTiltMagFilter() { + tiltMagEma = 0 + lastTiltMagSampleAt = 0 + lastSignedDegHint = null + } + + function getLayersArray() { + const v = layersRef.value !== undefined ? layersRef.value : layersRef + return Array.isArray(v) ? v : [] + } + + function simulateFromSignedDegrees(tiltMagDeg, signedDegHint) { + const ls = getLayersArray() + const n = Math.max(1, ls.length) + const raw = Math.abs(Number(tiltMagDeg) || 0) + const now = Date.now() + let dt = 33 + if (lastTiltMagSampleAt > 0) { + dt = now - lastTiltMagSampleAt + } + lastTiltMagSampleAt = now + dt = Math.max(16, Math.min(100, dt)) + const alpha = 1 - Math.exp(-dt / TILT_MAG_TAU_MS) + let nextEma = tiltMagEma + (raw - tiltMagEma) * alpha + + const sec = dt * 0.001 + let maxRise = TILT_MAG_MAX_RISE_DPS * sec + const maxFall = TILT_MAG_MAX_FALL_DPS * sec + + if (Number.isFinite(signedDegHint)) { + if (lastSignedDegHint != null && sec > 1e-6) { + const dAbsSignedDt = + (Math.abs(signedDegHint) - Math.abs(lastSignedDegHint)) / sec + if (dAbsSignedDt > TILT_AWAY_FROM_LEVEL_DPS) { + maxRise *= TILT_RISE_TIGHTEN + } + } + lastSignedDegHint = signedDegHint + } else { + lastSignedDegHint = null + } + + let dMag = nextEma - tiltMagEma + if (dMag > 0) { + dMag = Math.min(dMag, maxRise) + } else { + dMag = Math.max(dMag, -maxFall) + } + tiltMagEma += dMag + + const absDeg = tiltMagEma + const STEP = DISCRETE_STEP_DEG + const MARGIN = DISCRETE_STEP_HYST_DEG + const stepBefore = discreteStableStep.value + let s = stepBefore + while (absDeg >= (s + 1) * STEP + MARGIN) s++ + while (s > 0 && absDeg <= s * STEP - MARGIN) s-- + discreteStableStep.value = s + const idx = s % n + const sens = physics.tiltSensitivity / 100 + const mul = 0.44 + sens * 0.52 + const u = (idx + 0.5) / n + const gPick = Math.max(-1, Math.min(1, 2 * u - 1)) + const gamma = Math.max(-1, Math.min(1, gPick / mul)) + simulate(gamma, 0) + } + + function lockPreviewStill() { + discreteStableStep.value = 0 + resetTiltMagFilter() + simulateFromSignedDegrees(0) + snapSimulatedTilt({ resetLayerSmoothing: true }) + } + + const { start: startTilt, stop: stopTilt } = useLenticularStudioTilt({ + simulate, + simulateFromSignedDegrees, + gyroSourceLabel, + useStudioAccelDirect: true, + onTiltDriverFallback: () => { + discreteStableStep.value = 0 + resetTiltMagFilter() + }, + }) + + let entryTiltTimer = null + + function cancelScheduledTiltStart() { + if (entryTiltTimer != null) { + clearTimeout(entryTiltTimer) + entryTiltTimer = null + } + } + + function scheduleTiltStart() { + cancelScheduledTiltStart() + nextTick(() => { + relax(0.86) + lockPreviewStill() + entryTiltTimer = setTimeout(() => { + entryTiltTimer = null + startTilt() + }, LENTICULAR_TILT_START_DELAY_MS) + }) + } + + function stopTiltPreview() { + cancelScheduledTiltStart() + stopTilt() + } + + return { + physics, + layerTransforms, + simulate, + relax, + gyroSourceLabel, + scheduleTiltStart, + stopTiltPreview, + lockPreviewStill, + } +} diff --git a/frontend/pages/asset-detail/asset-detail.vue b/frontend/pages/asset-detail/asset-detail.vue index e6d19c2..afc6f34 100644 --- a/frontend/pages/asset-detail/asset-detail.vue +++ b/frontend/pages/asset-detail/asset-detail.vue @@ -10,8 +10,104 @@ + + + + + + + + + + + + + + + {{ craftEarningsHint }} + + + + + + + + 分类 + {{ craftCategoryLabel }} + + + + + 创作者 + {{ craftCreatorName }} + + + + + 铸爱时间 + {{ craftMintDate }} + + + + + + + + + 数根名称 + + {{ craftAssetName }} + + + + 数根发行方 + + TOPFANS + + + + 区块链编号 + + {{ craftBlockPlaceholder }} + + + + 交易哈希 + + {{ craftHashPlaceholder }} + + + + + + + + + + + - + @@ -27,7 +123,7 @@ - + {{ loadError }} @@ -189,15 +285,62 @@ @@ -1039,4 +1249,71 @@ onUnmounted(() => { font-size:16rpx; } +.craft-confirm-scroll { + flex: 1; + height: calc(100vh - 120rpx); + margin-top: 100rpx; +} + +.craft-card-section { + margin-top: 24rpx; +} + +.craft-card-wrapper { + position: relative; + width: 520rpx; + height: 680rpx; + margin: 0 auto; +} + +.craft-lenticular-slot { + position: absolute; + left: 50%; + top: 50%; + width: 78%; + height: 82%; + transform: translate(-50%, -50%) rotate(-6deg); + z-index: 2; +} + +.craft-lenticular-card { + width: 100%; + height: 100%; +} + +.craft-card-image { + position: absolute; + left: 50%; + top: 50%; + width: 78%; + height: 82%; + transform: translate(-50%, -50%) rotate(-6deg); + z-index: 2; + border-radius: 16rpx; +} + +.craft-info-row { + margin-top: 32rpx; +} + +.craft-mint-bar { + margin: 48rpx 32rpx 80rpx; +} + +.craft-mint-btn { + width: 100%; + height: 96rpx; + line-height: 96rpx; + border-radius: 48rpx; + background: linear-gradient(90deg, #ff6b9d 0%, #ffa8c5 100%); + color: #fff; + font-size: 32rpx; + font-weight: bold; + border: none; +} + +.craft-mint-btn[disabled] { + opacity: 0.65; +} + diff --git a/frontend/pages/castlove/create.vue b/frontend/pages/castlove/create.vue index 6bf7320..e0ab729 100644 --- a/frontend/pages/castlove/create.vue +++ b/frontend/pages/castlove/create.vue @@ -159,7 +159,7 @@ :class="isLaserCardCraft ? 'btn-confirm-laser' : 'btn-skip'" @click="handleSingleCraftLaserEntry" > - {{ isLaserCardCraft ? '确认' : '生成' }} + 生成 @@ -196,9 +196,13 @@ import { buildCastloveFormSnapshot, CRAFT_LENTICULAR_CN, CRAFT_LASER_CARD_CN, - CASTLOVE_LASER_ENTRY_KEY, - LENTICULAR_STUDIO_STORAGE_KEY, } from '@/utils/castloveMintForm.js'; +import { + startAiImageGenerationFlow, + startCraftGenerationFlow, + STUDIO_LENTICULAR, + STUDIO_LASER, +} from '@/utils/castloveGenerationFlow.js'; // 获取页面参数(H5 必须用 onLoad 读 query;仅靠 onMounted + getCurrentPages().options 常为 undefined) const pageType = ref(''); @@ -677,100 +681,57 @@ const handleBack = async () => { } }; -/** 光栅卡:双图校验后进入工作室预览 */ -const handleLenticularGenerate = () => { - if (!uploadedBg.value || !uploadedSubject.value) { - uni.showToast({ title: '请上传背景图与主体图', icon: 'none' }); - return; - } - if (!uploadedBgBase64.value || !uploadedSubjectBase64.value) { - uni.showToast({ title: '图片尚未处理完成', icon: 'none' }); - return; - } - if (!nftInfo.value.trim()) { - uni.showToast({ title: '请输入藏品信息', icon: 'none' }); - return; - } - try { - uni.setStorageSync( - LENTICULAR_STUDIO_STORAGE_KEY, - JSON.stringify({ - bgPath: uploadedBg.value, - subjectPath: uploadedSubject.value, - bgBase64: uploadedBgBase64.value, - subjectBase64: uploadedSubjectBase64.value, +/** 光栅 / 镭射:上传 → Loading → 选图 → 工作室 */ +const buildCraftFormData = () => { + const snap = isLenticularCraft.value + ? buildCastloveFormSnapshot({ nftInfo: nftInfo.value, + materialTypes, materialTypeIndex: materialTypeIndex.value, - aiDescription: aiDescription.value, - }) - ); - } catch (e) { - console.error('[CreatePage] lenticular studio payload', e); - uni.showToast({ title: '保存失败,请重试', icon: 'none' }); - return; - } - uni.navigateTo({ - url: '/pages/castlove/lenticular-studio', - }); -}; - -/** 单图工艺:进入镭射工坊(Laser-Card 配置页),保存后再上传并创建订单 */ -const handleSingleCraftLaserEntry = () => { - if (isLenticularCraft.value) { - uni.showToast({ title: '光栅卡请使用「生成」进入工作室', icon: 'none' }); - return; - } - if (!uploadedImage.value) { - uni.showToast({ title: '请上传藏品图片', icon: 'none' }); - return; - } - if (!uploadedImageBase64.value) { - uni.showToast({ title: '图片尚未处理完成', icon: 'none' }); - return; - } - if (!nftInfo.value.trim()) { - uni.showToast({ title: '请输入藏品信息', icon: 'none' }); - return; - } - try { - uni.setStorageSync( - CASTLOVE_LASER_ENTRY_KEY, - JSON.stringify({ + pageName: pageName.value, + uploadedImage: uploadedSubject.value, + uploadedImageBase64: uploadedSubjectBase64.value, + }) + : buildCastloveFormSnapshot({ nftInfo: nftInfo.value, - materialTypes: [...materialTypes], + materialTypes, materialTypeIndex: materialTypeIndex.value, pageName: pageName.value, uploadedImage: uploadedImage.value, uploadedImageBase64: uploadedImageBase64.value, - }) - ); - } catch (e) { - console.error('[CreatePage] laser entry payload', e); - uni.showToast({ title: '保存失败,请重试', icon: 'none' }); - return; - } - uni.navigateTo({ - url: '/pages/castlove/laser-card-studio', - }); + }); + + return { + ...snap, + image: isLenticularCraft.value ? uploadedSubject.value : uploadedImage.value, + imageBase64: isLenticularCraft.value ? uploadedSubjectBase64.value : uploadedImageBase64.value, + type: pageType.value, + typeName: pageName.value, + materialType: snap.material_type, + materialTypeIndex: materialTypeIndex.value, + materialTypes: [...materialTypes], + aiDescription: aiDescription.value, + nftInfo: nftInfo.value, + ...(isLenticularCraft.value + ? { + lenticularBgImage: uploadedBg.value, + lenticularBgBase64: uploadedBgBase64.value, + lenticularSubjectImage: uploadedSubject.value, + lenticularSubjectBase64: uploadedSubjectBase64.value, + } + : {}), + }; }; -// 底部导航切换 -const handleTabChange = (newTab) => { - const routes = [ - '/pages/square/square', - '/pages/starbook/index', - '/pages/castlove/mall', - '/pages/starcity/index', - '/pages/ai-dazi/index' - ]; - - if (newTab >= 0 && newTab < routes.length) { - uni.navigateTo({ url: routes[newTab] }); +const buildCraftPrompt = () => { + let fullText = aiDescription.value.trim() || nftInfo.value.trim(); + if (pageName.value) { + fullText = `${pageName.value},${fullText}`; } + return fullText; }; -// 开始生成 -const handleGenerate = async () => { +const startCraftStudioPipeline = (studioKind) => { if (isLenticularCraft.value) { if (!uploadedBg.value || !uploadedSubject.value) { uni.showToast({ title: '请上传背景图与主体图', icon: 'none' }); @@ -796,93 +757,110 @@ const handleGenerate = async () => { return; } - // 组合输入文字(如果有页面名称,作为隐藏前缀) + try { + const formData = buildCraftFormData(); + startCraftGenerationFlow({ formData, studioKind }); + } catch (e) { + console.error('[CreatePage] craft studio pipeline', e); + uni.showToast({ title: '启动失败,请重试', icon: 'none' }); + } +}; + +const handleLenticularGenerate = () => { + startCraftStudioPipeline(STUDIO_LENTICULAR); +}; + +const handleSingleCraftLaserEntry = () => { + if (isLenticularCraft.value) { + uni.showToast({ title: '光栅卡请使用「生成」', icon: 'none' }); + return; + } + startCraftStudioPipeline(STUDIO_LASER); +}; + +// 底部导航切换 +const handleTabChange = (newTab) => { + const routes = [ + '/pages/square/square', + '/pages/starbook/index', + '/pages/castlove/mall', + '/pages/starcity/index', + '/pages/ai-dazi/index' + ]; + + if (newTab >= 0 && newTab < routes.length) { + uni.navigateTo({ url: routes[newTab] }); + } +}; + +// 开始生成(星卡等走铸造;光栅/镭射走工作室链路) +const handleGenerate = async () => { + if (isLenticularCraft.value) { + startCraftStudioPipeline(STUDIO_LENTICULAR); + return; + } + if (isLaserCardCraft.value) { + startCraftStudioPipeline(STUDIO_LASER); + return; + } + + if (!uploadedImage.value) { + uni.showToast({ title: '请上传藏品图片', icon: 'none' }); + return; + } + if (!uploadedImageBase64.value) { + uni.showToast({ title: '图片尚未处理完成', icon: 'none' }); + return; + } + + if (!nftInfo.value.trim()) { + uni.showToast({ title: '请输入藏品信息', icon: 'none' }); + return; + } + let fullText = aiDescription.value.trim(); if (pageName.value) { fullText = `${pageName.value},${fullText}`; } - + if (!fullText || fullText === `${pageName.value},`) { uni.showToast({ title: '请输入AI描述', icon: 'none' }); return; } - const snap = isLenticularCraft.value - ? buildCastloveFormSnapshot({ - nftInfo: nftInfo.value, - materialTypes, - materialTypeIndex: materialTypeIndex.value, - pageName: pageName.value, - uploadedImage: uploadedSubject.value, - uploadedImageBase64: uploadedSubjectBase64.value, - }) - : buildCastloveFormSnapshot({ - nftInfo: nftInfo.value, - materialTypes, - materialTypeIndex: materialTypeIndex.value, - pageName: pageName.value, - uploadedImage: uploadedImage.value, - uploadedImageBase64: uploadedImageBase64.value, - }); + const snap = buildCastloveFormSnapshot({ + nftInfo: nftInfo.value, + materialTypes, + materialTypeIndex: materialTypeIndex.value, + pageName: pageName.value, + uploadedImage: uploadedImage.value, + uploadedImageBase64: uploadedImageBase64.value, + }); const formData = { ...snap, - image: isLenticularCraft.value ? uploadedSubject.value : uploadedImage.value, - imageBase64: isLenticularCraft.value ? uploadedSubjectBase64.value : uploadedImageBase64.value, + image: uploadedImage.value, + imageBase64: uploadedImageBase64.value, type: pageType.value, typeName: pageName.value, materialType: snap.material_type, - ...(isLenticularCraft.value - ? { - lenticularBgImage: uploadedBg.value, - lenticularBgBase64: uploadedBgBase64.value, - lenticularSubjectImage: uploadedSubject.value, - lenticularSubjectBase64: uploadedSubjectBase64.value, - } - : {}), }; try { uni.setStorageSync('castlove_form_data', JSON.stringify(formData)); - } catch (e) { console.error('保存表单数据失败:', e); return; } - // 发送到后端生成 sendToBackend(fullText); }; -// 发送数据到后端 +// 发送数据到后端 → 统一 Loading → 选择藏品 const sendToBackend = (aiDescription) => { - // 构建后端需要的数据格式(不包含base64图片) - const requestData = { - prompt: aiDescription, // AI描述文本 - model: "image-01", - aspect_ratio: "16:9", - subject_reference: [ - { - type: "character", - image_file: "" // 占位符,实际使用时从castlove_form_data读取 - } - ], - "n": 4 - }; - - // 将轻量数据存储到本地 - uni.setStorageSync('generation_request_data', JSON.stringify(requestData)); - console.log('[CreatePage] 生成请求数据已保存'); - - // base64图片已经在castlove_form_data中,不需要重复存储 - - // 清空表单 - // resetForm(); - - // 跳转到加载页面 - uni.navigateTo({ - url: '/pages/discover/generation-loading' - }); + const formDataStr = uni.getStorageSync('castlove_form_data'); + const formData = formDataStr ? JSON.parse(formDataStr) : {}; + startAiImageGenerationFlow({ prompt: aiDescription, formData }); }; // 重置表单 diff --git a/frontend/pages/castlove/success.vue b/frontend/pages/castlove/success.vue index 500766c..d09c0d5 100644 --- a/frontend/pages/castlove/success.vue +++ b/frontend/pages/castlove/success.vue @@ -45,6 +45,7 @@