feat:增加守卫组合式函数本身

This commit is contained in:
zerosaturation 2026-07-13 14:56:08 +08:00
parent 7467190e38
commit 6a6d5f9deb
10 changed files with 524 additions and 398 deletions

View File

@ -0,0 +1,89 @@
// frontend/composables/useAliveGuard.js
//
// 用途:UniApp iOS WebView 的 JS Framework 在原生异步回调回来时,如果页面已销毁,
// 调 n.fireEvent 会报 "undefined is not an object (evaluating 'n.fireEvent')"
// (warning 级别,不影响原生操作结果,但日志噪音大且难定位)
//
// 解法:在 setup() 里创建 alive 守卫,页面卸载/组件销毁时翻成 false;
// 所有原生异步回调(uni.* / plus.io.* / plus.nativeObj.* 的 success/fail/complete)
// 包一层 guard,守卫死亡后直接吞掉结果,不更新 state、不弹 toast、不继续后续链式调用。
//
// 注意:我们不取消原生操作本身 — 用户已经触发了上传/保存/文件读取,这些操作的实际副作用
// (网络请求发出、文件落盘)应该正常完成。只是 JS 这一侧不应当再触碰一个已死页面的 state。
//
// 用法:
// import { useAliveGuard } from '@/composables/useAliveGuard.js'
//
// const { isAlive, die, guard } = useAliveGuard()
//
// uni.uploadFile({
// url, filePath,
// success: guard((res) => { /* 仅 alive=true 时执行 */ }),
// fail: guard((err) => { /* 同上 */ }),
// complete: guard(() => { /* 同上 */ })
// })
//
// // plus.io 链式回调:
// plus.io.resolveLocalFileSystemURL(
// path,
// guard((entry) => { entry.file(guard((file) => { ... })) }),
// guard((err) => { ... })
// )
//
// // 主动检查:
// if (!isAlive()) return
//
// 自动绑定生命周期:
// - 页面上下文(setup 里当前实例是 page):绑定 onUnload(页面关闭时死)
// - 组件上下文(setup 里当前实例是 component):绑定 onBeforeUnmount(v-if=false / 父组件销毁时死)
// - 两者都不存在(罕见):返回的 alive 永远为 true(等同不守卫,留给调用方自行 die())
import { onBeforeUnmount } from 'vue'
import { onUnload } from '@dcloudio/uni-app'
/**
* @returns {{
* isAlive: () => boolean,
* die: () => void,
* guard: <T extends Function>(fn: T) => T
* }}
*/
export function useAliveGuard() {
let alive = true
let bound = false
// 优先 onUnload(页面级生命周期,从 @dcloudio/uni-app 导入)
// 页面里调 onUnload 是注册的页面 onUnload 钩子;组件里调可能抛 "onUnload is not a function"
try {
onUnload(() => { alive = false })
bound = true
} catch (_) {
/* 组件上下文,onUnload 不可用,下面兜底 */
}
// 兜底 onBeforeUnmount(Vue 组件级生命周期)
if (!bound) {
try {
onBeforeUnmount(() => { alive = false })
} catch (_) {
/* 都不在 setup 里调,保持 alive=true,调用方自行 die() */
}
}
return {
isAlive: () => alive,
/** 主动翻成 false(罕见用 — 大多数场景生命周期钩子已经处理) */
die: () => { alive = false },
/**
* 包装回调:页面/组件已死时 no-op,活着时透传执行
* 非函数参数原样返回,方便链式写:guard(maybeUndefined)
*/
guard(fn) {
if (typeof fn !== 'function') return fn
return function guardedCallback(...args) {
if (!alive) return undefined
return fn.apply(this, args)
}
}
}
}

View File

@ -2,11 +2,12 @@
// 分享状态机 + 合成调度 + 错误处理 + 监控埋点
// (spec § 5 + § 5.3.1 + § 7 + § 8)
import { ref, nextTick, onBeforeUnmount } from 'vue';
import { onLoad, onShow, onHide, onUnload } from '@dcloudio/uni-app';
import { ref, nextTick } from 'vue';
import { onShow, onHide, onUnload } from '@dcloudio/uni-app';
import { composeShareImage, computeComposeKey } from '@/utils/image-compositor.js';
import { pickRandomSlogan } from '@/utils/brand-slogans.js';
import { getShareQrcodeApi, trackShareApi } from '@/utils/api.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
const APP_PACKAGES = {
weixin_friend: { pname: 'com.tencent.mm', bundleid: 'com.tencent.xinWeChat' },
@ -39,6 +40,12 @@ export function useShare(props) {
const currentSlogan = ref(pickRandomSlogan());
const failCount = ref(0); // L3 degradation counter
// ============ Alive Guard ============
// 分享进行中若用户关闭弹窗/页面,所有原生异步回调(plus.io / uni.saveImageToPhotosAlbum /
// plus.gallery.save)回来时会被 guard() 吞掉,不再触碰 state、不弹 toast、不继续链式调用,
// 避免 iOS WebView 上 "TypeError: undefined is not an object (evaluating 'n.fireEvent')"
const { isAlive, guard } = useAliveGuard();
// ============ Timers ============
const shareTimeoutTimer = ref(null);
const fallbackTimer = ref(null);
@ -49,8 +56,11 @@ export function useShare(props) {
// ============ Lifecycle ============
(async () => {
try { const info = await uni.getSystemInfo(); systemType.value = info.platform || 'other'; }
catch { systemType.value = 'other'; }
try {
const info = await uni.getSystemInfo();
if (!isAlive()) return;
systemType.value = info.platform || 'other';
} catch { /* alive 守卫兜底 */ }
})();
onShow(() => {
if (state.value === 'sharing') {
@ -60,7 +70,8 @@ export function useShare(props) {
onHide(() => {
if (shareTimeoutTimer.value) { clearTimeout(shareTimeoutTimer.value); shareTimeoutTimer.value = null; }
});
onBeforeUnmount(clearShareTimers);
// useAliveGuard 已绑 onUnload 清活守卫,这里再清一次 timers 保持同步
onUnload(() => { clearShareTimers(); });
// ============ L1 Cache ============
const l1Cache = new Map();
@ -84,7 +95,10 @@ export function useShare(props) {
const pkg = APP_PACKAGES[action];
if (!pkg) return true;
return new Promise(resolve => {
plus.runtime.isApplicationExist({ pname: pkg.pname, bundleid: pkg.bundleid }, e => resolve(!!e.exist));
plus.runtime.isApplicationExist(
{ pname: pkg.pname, bundleid: pkg.bundleid },
guard(e => resolve(!!e.exist))
);
});
}
@ -128,34 +142,34 @@ export function useShare(props) {
// 先确保 _doc 存在
plus.io.resolveLocalFileSystemURL(
'_doc/',
(docDir) => {
guard((docDir) => {
// 解析 _www 源文件
plus.io.resolveLocalFileSystemURL(
srcPath,
(srcEntry) => {
guard((srcEntry) => {
srcEntry.copyTo(
docDir,
fileName,
(destEntry) => {
guard((destEntry) => {
console.log('[useShare] static copied to', destEntry.fullPath);
resolve(destEntry.fullPath);
},
(copyErr) => {
}),
guard((copyErr) => {
console.warn('[useShare] static copy fail, fallback raw', copyErr);
resolve(staticPath);
}
})
);
},
(resolveErr) => {
}),
guard((resolveErr) => {
console.warn('[useShare] static resolve fail, fallback raw', resolveErr);
resolve(staticPath);
}
})
);
},
(docErr) => {
}),
guard((docErr) => {
console.warn('[useShare] _doc resolve fail', docErr);
resolve(staticPath);
}
})
);
});
}
@ -180,38 +194,38 @@ export function useShare(props) {
// plus.io.resolveLocalFileSystemURL 拿到 source FileEntry
plus.io.resolveLocalFileSystemURL(
tempPath,
(srcEntry) => {
guard((srcEntry) => {
// 拿沙盒目录 DirectoryEntry
plus.io.resolveLocalFileSystemURL(
sandboxRoot,
(sandboxDir) => {
guard((sandboxDir) => {
srcEntry.copyTo(
sandboxDir,
fileName,
(destEntry) => {
guard((destEntry) => {
clearTimeout(timeoutId);
console.log('[useShare] copyToSandbox done:', destEntry.fullPath);
resolve(destEntry.fullPath);
},
(copyErr) => {
}),
guard((copyErr) => {
clearTimeout(timeoutId);
console.warn('[useShare] copyToSandbox fail:', copyErr);
reject(new Error(copyErr.message || 'copyTo failed'));
}
})
);
},
(dirErr) => {
}),
guard((dirErr) => {
clearTimeout(timeoutId);
console.warn('[useShare] resolve sandbox dir fail:', dirErr);
reject(new Error(dirErr.message || 'sandbox dir resolve failed'));
}
})
);
},
(srcErr) => {
}),
guard((srcErr) => {
clearTimeout(timeoutId);
console.warn('[useShare] resolve temp fail:', srcErr);
reject(new Error(srcErr.message || 'temp resolve failed'));
}
})
);
});
}
@ -221,9 +235,9 @@ export function useShare(props) {
if (typeof plus === 'undefined' || !plus.io || !sandboxPath) return;
plus.io.resolveLocalFileSystemURL(
sandboxPath,
(entry) => {
guard((entry) => {
try { entry.remove(() => {}, () => {}); } catch {}
},
}),
() => {}
);
}
@ -368,15 +382,15 @@ export function useShare(props) {
await new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: (res) => {
success: guard((res) => {
console.log('[useShare] uni.saveImageToPhotosAlbum success', res);
uni.showToast({ title: '已保存到相册' });
resolve();
},
fail: (e) => {
}),
fail: guard((e) => {
console.warn('[useShare] uni.saveImageToPhotosAlbum fail, fallback to plus.gallery.save', e);
reject(new Error(e.errMsg || JSON.stringify(e) || 'save failed'));
}
})
});
});
} catch (eUni) {
@ -391,17 +405,17 @@ export function useShare(props) {
await new Promise((resolve, reject) => {
plus.gallery.save(
sandboxPath,
(res) => {
guard((res) => {
console.log('[useShare] plus.gallery.save success', res);
uni.showToast({ title: '已保存到相册' });
cleanupSandboxFile(sandboxPath);
resolve();
},
(err) => {
}),
guard((err) => {
console.warn('[useShare] plus.gallery.save fail', err);
cleanupSandboxFile(sandboxPath);
reject(err);
}
})
);
});
} catch (eSandbox) {
@ -419,8 +433,8 @@ export function useShare(props) {
const shareRes = await new Promise((resolve) => {
uni.share({
provider, scene, imageUrl: tempFilePath,
success: (r) => resolve({ ok: true, msg: r.errMsg }),
fail: (e) => resolve({ ok: false, msg: e.errMsg || 'share failed' })
success: guard((r) => resolve({ ok: true, msg: r.errMsg })),
fail: guard((e) => resolve({ ok: false, msg: e.errMsg || 'share failed' }))
});
});
if (shareRes.ok) result = 'success';

View File

@ -160,6 +160,7 @@ import { onLoad, onUnload } from '@dcloudio/uni-app';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import { getSandboxFileUri, clearSandboxSubdir } from '@/utils/ioPath.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
import ConfirmModal from '@/components/ConfirmModal.vue';
import './create-laser-upload.css';
import {
@ -239,6 +240,10 @@ const onConfirmModal = () => {
confirmModal.value.visible = false;
};
// alive :,(chooseImage/getFileInfo/plus.io.FileReader)
// state, iOS WebView "TypeError: undefined is not an object (evaluating 'n.fireEvent')"
const { guard } = useAliveGuard();
//
const onCancelModal = () => {
if (confirmModal.value.confirmCallback) {
@ -332,13 +337,13 @@ const openImagePicker = (lenticularSlot) => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
success: guard((res) => {
const filePath = res.tempFilePaths[0];
const tempFile = res.tempFiles && res.tempFiles[0];
uni.getFileInfo({
filePath: filePath,
success: (fileInfo) => {
success: guard((fileInfo) => {
const maxSize = 5 * 1024 * 1024;
if (fileInfo.size > maxSize) {
pendingLenticularSlot.value = '';
@ -359,19 +364,19 @@ const openImagePicker = (lenticularSlot) => {
const rawName = (tempFile && tempFile.name) ? tempFile.name : filePath.split('/').pop();
originalFileName.value = rawName;
convertImageToBase64(filePath, originalFileName.value);
},
fail: (error) => {
}),
fail: guard((error) => {
pendingLenticularSlot.value = '';
console.error('获取文件信息失败:', error);
uni.showToast({ title: '获取文件信息失败', icon: 'none' });
}
})
});
},
fail: (err) => {
}),
fail: guard((err) => {
pendingLenticularSlot.value = '';
console.error('选择图片失败:', err);
uni.showToast({ title: '选择图片失败', icon: 'none' });
}
})
});
};
@ -423,7 +428,7 @@ const convertImageToBase64Native = (filePath, fileName) => {
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
success: guard((res) => {
const ext = fileName.toLowerCase().split('.').pop();
let mimeType = 'image/jpeg';
if (ext === 'png') mimeType = 'image/png';
@ -431,42 +436,42 @@ const convertImageToBase64Native = (filePath, fileName) => {
applyUploadResult(filePath, dataUrl);
console.log('[CreatePage] Base64转换成功 (小程序)');
console.log('[CreatePage] Base64长度:', dataUrl.length);
},
fail: (error) => {
}),
fail: guard((error) => {
pendingLenticularSlot.value = '';
console.error('[CreatePage] Base64转换失败 (小程序):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
}
})
});
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
plus.io.resolveLocalFileSystemURL(filePath, guard((entry) => {
entry.file(guard((file) => {
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
reader.onloadend = guard((e) => {
applyUploadResult(filePath, e.target.result);
console.log('[CreatePage] Base64转换成功 (App)');
console.log('[CreatePage] Base64长度:', (e.target.result && e.target.result.length) || 0);
};
reader.onerror = (error) => {
});
reader.onerror = guard((error) => {
pendingLenticularSlot.value = '';
console.error('[CreatePage] Base64转换失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
});
reader.readAsDataURL(file);
});
}, (error) => {
}));
}), guard((error) => {
pendingLenticularSlot.value = '';
console.error('[CreatePage] 读取文件失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
});
}));
// #endif
};
@ -528,7 +533,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
filePath: filePath,
data: base64Content,
encoding: 'base64',
success: () => {
success: guard(() => {
uni.uploadFile({
url: ossData.host,
filePath: filePath,
@ -543,22 +548,22 @@ const uploadImageToOss = async (base64Data, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(imageUrl);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
// #endif
@ -569,10 +574,10 @@ const uploadImageToOss = async (base64Data, ossData) => {
const base64ContentApp = base64Data.split(',')[1];
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64Data, () => {
bitmap.loadBase64Data(base64Data, guard(() => {
// file:// save
getSandboxFileUri(['castlove', 'tmp'], fileName).then((tempFilePath) => {
bitmap.save(tempFilePath, { overwrite: true }, () => {
bitmap.save(tempFilePath, { overwrite: true }, guard(() => {
console.log('[CreatePage] App临时文件保存成功:', tempFilePath);
bitmap.clear();
@ -590,7 +595,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
console.log('[CreatePage] App上传响应:', uploadRes);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
@ -602,27 +607,27 @@ const uploadImageToOss = async (base64Data, ossData) => {
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[CreatePage] App上传失败:', error);
reject(error);
}
})
});
}, (error) => {
}, guard((error) => {
console.error('[CreatePage] App保存临时文件失败:', error);
bitmap.clear();
reject(new Error('保存临时文件失败'));
});
})));
}).catch((err) => {
console.error('[CreatePage] getSandboxFileUri 失败:', err);
bitmap.clear();
reject(err);
});
}, (error) => {
}, guard((error) => {
console.error('[CreatePage] App加载base64失败:', error);
bitmap.clear();
reject(new Error('加载图片数据失败'));
});
})));
// #endif
});
};

View File

@ -105,9 +105,13 @@ import Header from "../components/Header.vue";
import ConfirmModal from '@/components/ConfirmModal.vue';
import BottomNav from "../components/BottomNav.vue";
import { getOssSignatureApi, deleteMintOrderApi } from '@/utils/api.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
const navExpanded = ref(false);
// alive :, state, iOS WebView
const { guard } = useAliveGuard();
//
const confirmModal = ref({
visible: false,
@ -176,14 +180,14 @@ const chooseImage = () => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
success: guard((res) => {
const filePath = res.tempFilePaths[0];
const tempFile = res.tempFiles && res.tempFiles[0];
//
uni.getFileInfo({
filePath: filePath,
success: (fileInfo) => {
success: guard((fileInfo) => {
// 5MB = 5 * 1024 * 1024 bytes
const maxSize = 5 * 1024 * 1024;
if (fileInfo.size > maxSize) {
@ -217,8 +221,8 @@ const chooseImage = () => {
// base64
convertImageToBase64(filePath, originalFileName.value);
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('获取文件信息失败:', error);
uni.showToast({
title: '获取文件信息失败',
@ -226,14 +230,14 @@ const chooseImage = () => {
});
}
});
},
fail: (err) => {
}),
fail: guard((err) => {
console.error('选择图片失败:', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
})
});
};
@ -311,28 +315,28 @@ const convertImageToBase64Native = (filePath, fileName) => {
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
success: guard((res) => {
const ext = fileName.toLowerCase().split('.').pop();
let mimeType = 'image/jpeg';
if (ext === 'png') {
mimeType = 'image/png';
}
uploadedImageBase64.value = `data:${mimeType};base64,${res.data}`;
uploadedImage.value = filePath;
console.log('[CastloveContent] Base64转换成功 (小程序)');
uni.hideLoading();
uni.showToast({
title: '图片加载成功',
icon: 'success',
duration: 1500
});
isUploading.value = false;
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[CastloveContent] Base64转换失败:', error);
uni.hideLoading();
uni.showToast({
@ -341,31 +345,31 @@ const convertImageToBase64Native = (filePath, fileName) => {
duration: 2000
});
isUploading.value = false;
}
})
});
// #endif
// #ifdef APP-PLUS
// App使plus.io API
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
plus.io.resolveLocalFileSystemURL(filePath, guard((entry) => {
entry.file(guard((file) => {
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
reader.onloadend = guard((e) => {
uploadedImageBase64.value = e.target.result;
uploadedImage.value = filePath;
console.log('[CastloveContent] Base64转换成功 (App)');
uni.hideLoading();
uni.showToast({
title: '图片加载成功',
icon: 'success',
duration: 1500
});
isUploading.value = false;
};
reader.onerror = (error) => {
});
reader.onerror = guard((error) => {
console.error('[CastloveContent] Base64转换失败:', error);
uni.hideLoading();
uni.showToast({
@ -374,10 +378,10 @@ const convertImageToBase64Native = (filePath, fileName) => {
duration: 2000
});
isUploading.value = false;
};
});
reader.readAsDataURL(file);
});
}, (error) => {
}));
}), guard((error) => {
console.error('[CastloveContent] 读取文件失败:', error);
uni.hideLoading();
uni.showToast({
@ -386,7 +390,7 @@ const convertImageToBase64Native = (filePath, fileName) => {
duration: 2000
});
isUploading.value = false;
});
}));
// #endif
};
@ -420,7 +424,7 @@ const uploadImageToOss = async (filePath, fileName) => {
'x-oss-signature': signRes.data.signature,
'x-oss-signature-version': signRes.data.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
try {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
// 3. URL
@ -449,8 +453,8 @@ const uploadImageToOss = async (filePath, fileName) => {
} finally {
isUploading.value = false;
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('OSS上传失败:', error);
uni.hideLoading();
uni.showToast({
@ -459,7 +463,7 @@ const uploadImageToOss = async (filePath, fileName) => {
duration: 2000
});
isUploading.value = false;
}
})
});
} catch (error) {

View File

@ -111,6 +111,7 @@ import { onLoad,onUnload } from '@dcloudio/uni-app';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import { getSandboxFileUri, clearSandboxSubdir } from '@/utils/ioPath.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
import ConfirmModal from '@/components/ConfirmModal.vue';
import {
buildCastloveFormSnapshot,
@ -146,6 +147,9 @@ onUnload(() => {
}
});
// alive :, state, iOS WebView
const { guard } = useAliveGuard();
function safeDecodeParam(v) {
if (v == null || v === '') return '';
const s = typeof v === 'string' ? v : String(v);
@ -260,13 +264,13 @@ const openImagePicker = (lenticularSlot) => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
success: guard((res) => {
const filePath = res.tempFilePaths[0];
const tempFile = res.tempFiles && res.tempFiles[0];
uni.getFileInfo({
filePath: filePath,
success: (fileInfo) => {
success: guard((fileInfo) => {
const maxSize = 5 * 1024 * 1024;
const slotLabel = lenticularSlot === 'bg' ? '背景图' : '主体图';
if (fileInfo.size > maxSize) {
@ -298,19 +302,19 @@ const openImagePicker = (lenticularSlot) => {
const rawName = (tempFile && tempFile.name) ? tempFile.name : filePath.split('/').pop();
originalFileName.value = rawName;
convertImageToBase64(filePath, originalFileName.value);
},
fail: (error) => {
}),
fail: guard((error) => {
pendingLenticularSlot.value = '';
console.error('获取文件信息失败:', error);
uni.showToast({ title: '获取文件信息失败', icon: 'none' });
}
})
});
},
fail: (err) => {
}),
fail: guard((err) => {
pendingLenticularSlot.value = '';
console.error('选择图片失败:', err);
uni.showToast({ title: '选择图片失败', icon: 'none' });
}
})
});
};
@ -362,7 +366,7 @@ const convertImageToBase64Native = (filePath, fileName) => {
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
success: guard((res) => {
const ext = fileName.toLowerCase().split('.').pop();
let mimeType = 'image/jpeg';
if (ext === 'png') mimeType = 'image/png';
@ -370,42 +374,42 @@ const convertImageToBase64Native = (filePath, fileName) => {
applyUploadResult(filePath, dataUrl);
console.log('[LenticularCreate] Base64转换成功 (小程序)');
console.log('[LenticularCreate] Base64长度:', dataUrl.length);
},
fail: (error) => {
}),
fail: guard((error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] Base64转换失败 (小程序):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
}
})
});
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
plus.io.resolveLocalFileSystemURL(filePath, guard((entry) => {
entry.file(guard((file) => {
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
reader.onloadend = guard((e) => {
applyUploadResult(filePath, e.target.result);
console.log('[LenticularCreate] Base64转换成功 (App)');
console.log('[LenticularCreate] Base64长度:', (e.target.result && e.target.result.length) || 0);
};
reader.onerror = (error) => {
});
reader.onerror = guard((error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] Base64转换失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
});
reader.readAsDataURL(file);
});
}, (error) => {
}));
}), guard((error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] 读取文件失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
});
}));
// #endif
};
@ -465,7 +469,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
filePath: filePath,
data: base64Content,
encoding: 'base64',
success: () => {
success: guard(() => {
uni.uploadFile({
url: ossData.host,
filePath: filePath,
@ -480,22 +484,22 @@ const uploadImageToOss = async (base64Data, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(imageUrl);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
// #endif
@ -505,9 +509,9 @@ const uploadImageToOss = async (base64Data, ossData) => {
const base64ContentApp = base64Data.split(',')[1];
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64ContentApp, () => {
bitmap.loadBase64Data(base64ContentApp, guard(() => {
getSandboxFileUri(['castlove-lenticular', 'tmp'], fileName).then((tempFilePath) => {
bitmap.save(tempFilePath, { overwrite: true }, () => {
bitmap.save(tempFilePath, { overwrite: true }, guard(() => {
console.log('[LenticularCreate] App临时文件保存成功:', tempFilePath);
bitmap.clear();
@ -525,7 +529,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
console.log('[LenticularCreate] App上传响应:', uploadRes);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
@ -537,17 +541,17 @@ const uploadImageToOss = async (base64Data, ossData) => {
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[LenticularCreate] App上传失败:', error);
reject(error);
}
})
});
}, (error) => {
}, guard((error) => {
console.error('[LenticularCreate] App保存临时文件失败:', error);
bitmap.clear();
reject(new Error('保存临时文件失败'));
});
})));
}).catch((err) => {
console.error('[LenticularCreate] getSandboxFileUri 失败:', err);
bitmap.clear();

View File

@ -65,9 +65,11 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onUnload } from '@dcloudio/uni-app';
import { getOssSignatureApi, createMintOrderApi, estimateMintCostApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import { getSandboxFileUri, getSandboxFile, clearSandboxSubdir } from '@/utils/ioPath.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import ConfirmModal from '@/components/ConfirmModal.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
@ -177,6 +179,9 @@ const base64ToBlob = (base64Data) => {
return new Blob([uInt8Array], { type: contentType });
};
// alive :, state, iOS WebView
const { guard } = useAliveGuard();
// OSS
const uploadImageToOss = async (base64Image) => {
console.log('[GenerationResult] uploadImageToOss 开始');
@ -254,14 +259,14 @@ const uploadImageToOss = async (base64Image) => {
if (uni.base64ToTempFilePath) {
uni.base64ToTempFilePath({
base64Data: base64Image,
success: (res) => {
success: guard((res) => {
console.log('[GenerationResult] base64转临时文件成功:', res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] base64转临时文件失败:', error);
reject(new Error('base64转临时文件失败'));
}
})
});
} else {
// 使plus.io
@ -269,21 +274,21 @@ const uploadImageToOss = async (base64Image) => {
console.log('[GenerationResult] 使用plus.io方案');
getSandboxFileUri(['lenticular-result', 'tmp'], fileName).then((tempPath) => {
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64Image, () => {
bitmap.save(tempPath, { overwrite: true }, () => {
bitmap.loadBase64Data(base64Image, guard(() => {
bitmap.save(tempPath, { overwrite: true }, guard(() => {
console.log('[GenerationResult] 图片保存成功:', tempPath);
bitmap.clear();
resolve(tempPath);
}, (error) => {
}, guard((error) => {
console.error('[GenerationResult] 图片保存失败:', error);
bitmap.clear();
reject(new Error('图片保存失败'));
});
}, (error) => {
}));
}, guard((error) => {
console.error('[GenerationResult] 加载base64失败:', error);
bitmap.clear();
reject(new Error('加载base64失败'));
});
})));
}).catch(reject);
}
});
@ -309,14 +314,14 @@ const uploadImageToOss = async (base64Image) => {
filePath: tempFilePath,
data: base64Data,
encoding: 'base64',
success: () => {
success: guard(() => {
console.log('[GenerationResult] 文件写入成功');
resolve();
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] 文件写入失败:', error);
reject(new Error('保存临时文件失败'));
}
})
});
});
// #endif
@ -339,7 +344,7 @@ const uploadImageToOss = async (base64Image) => {
'x-oss-signature': signRes.data.signature,
'x-oss-signature-version': signRes.data.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
console.log('[GenerationResult] OSS上传响应:', uploadRes.statusCode);
console.log('[GenerationResult] OSS上传完整响应:', uploadRes);
@ -354,11 +359,11 @@ const uploadImageToOss = async (base64Image) => {
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] OSS上传失败:', error);
reject(error);
}
})
});
});
// #endif
@ -489,16 +494,16 @@ const uploadToOssNative = async (base64Image, fileName, ossData) => {
}).then((fileEntry) => {
console.log('[GenerationResult] 获取文件系统成功');
fileEntry.createWriter((writer) => {
writer.onwrite = () => {
writer.onwrite = guard(() => {
console.log('[GenerationResult] 文件写入成功');
uploadFileToOss(fileEntry.toLocalURL(), fileName, ossData, resolve, reject, null);
};
writer.onerror = (error) => {
});
writer.onerror = guard((error) => {
console.error('[GenerationResult] 写入文件失败:', error);
uni.hideLoading();
isUploading.value = false;
reject(new Error('写入文件失败'));
};
});
console.log('[GenerationResult] 开始转换base64为Blob');
try {
const blob = base64ToBlob(base64Image);
@ -527,16 +532,16 @@ const uploadToOssNative = async (base64Image, fileName, ossData) => {
filePath: tempFilePath,
data: base64Data,
encoding: 'base64',
success: () => {
success: guard(() => {
console.log('[GenerationResult] 文件写入成功');
uploadFileToOss(tempFilePath, fileName, ossData, resolve, reject, fs);
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] 保存临时文件失败:', error);
uni.hideLoading();
isUploading.value = false;
reject(new Error('保存临时文件失败'));
}
})
});
}
// #endif
@ -570,7 +575,7 @@ const uploadFileToOss = (tempFilePath, fileName, ossData, resolve, reject, fs) =
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
console.log('[GenerationResult] OSS上传响应:', uploadRes);
console.log('[GenerationResult] 状态码:', uploadRes.statusCode);
@ -592,11 +597,11 @@ const uploadFileToOss = (tempFilePath, fileName, ossData, resolve, reject, fs) =
console.error('[GenerationResult] 上传失败,状态码:', uploadRes.statusCode);
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] OSS上传失败:', error);
reject(error);
}
})
});
};

View File

@ -108,12 +108,16 @@ defineOptions({
import { ref, onMounted } from 'vue';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import { getSandboxFileUri, clearSandboxSubdir } from '@/utils/ioPath.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
import ConfirmModal from '@/components/ConfirmModal.vue';
//
const pageType = ref('');
const pageName = ref('');
// alive :, state, iOS WebView
const { guard } = useAliveGuard();
//
const confirmModal = ref({
visible: false,
@ -179,13 +183,13 @@ const chooseImage = () => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
success: guard((res) => {
const filePath = res.tempFilePaths[0];
const tempFile = res.tempFiles && res.tempFiles[0];
uni.getFileInfo({
filePath: filePath,
success: (fileInfo) => {
success: guard((fileInfo) => {
const maxSize = 5 * 1024 * 1024;
if (fileInfo.size > maxSize) {
uni.showToast({ title: '图片大小不能超过5MB', icon: 'none', duration: 2000 });
@ -204,17 +208,17 @@ const chooseImage = () => {
const rawName = (tempFile && tempFile.name) ? tempFile.name : filePath.split('/').pop();
originalFileName.value = rawName;
convertImageToBase64(filePath, originalFileName.value);
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('获取文件信息失败:', error);
uni.showToast({ title: '获取文件信息失败', icon: 'none' });
}
})
});
},
fail: (err) => {
}),
fail: guard((err) => {
console.error('选择图片失败:', err);
uni.showToast({ title: '选择图片失败', icon: 'none' });
}
})
});
};
@ -271,59 +275,59 @@ const convertImageToBase64Native = (filePath, fileName) => {
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
success: guard((res) => {
const ext = fileName.toLowerCase().split('.').pop();
let mimeType = 'image/jpeg';
if (ext === 'png') mimeType = 'image/png';
uploadedImageBase64.value = `data:${mimeType};base64,${res.data}`;
uploadedImage.value = filePath;
console.log('[CreatePage] Base64转换成功 (小程序)');
console.log('[CreatePage] Base64长度:', uploadedImageBase64.value.length);
uni.hideLoading();
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
isUploading.value = false;
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[CreatePage] Base64转换失败 (小程序):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
}
})
});
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
plus.io.resolveLocalFileSystemURL(filePath, guard((entry) => {
entry.file(guard((file) => {
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
reader.onloadend = guard((e) => {
uploadedImageBase64.value = e.target.result;
uploadedImage.value = filePath;
console.log('[CreatePage] Base64转换成功 (App)');
console.log('[CreatePage] Base64长度:', uploadedImageBase64.value.length);
uni.hideLoading();
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
isUploading.value = false;
};
reader.onerror = (error) => {
});
reader.onerror = guard((error) => {
console.error('[CreatePage] Base64转换失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
});
reader.readAsDataURL(file);
});
}, (error) => {
}));
}), guard((error) => {
console.error('[CreatePage] 读取文件失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
});
}));
// #endif
};
@ -386,7 +390,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
filePath: filePath,
data: base64Content,
encoding: 'base64',
success: () => {
success: guard(() => {
uni.uploadFile({
url: ossData.host,
filePath: filePath,
@ -401,25 +405,25 @@ const uploadImageToOss = async (base64Data, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(imageUrl);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
// #endif
// #ifdef APP-PLUS
// App - base64
// Android 10+ :bitmap.save file:// ( '_doc/' )
@ -427,12 +431,12 @@ const uploadImageToOss = async (base64Data, ossData) => {
const base64Content = base64Data.split(',')[1];
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64Data, () => {
bitmap.loadBase64Data(base64Data, guard(() => {
getSandboxFileUri(['castlove-self', 'tmp'], fileName).then((tempFilePath) => {
bitmap.save(tempFilePath, { overwrite: true }, () => {
bitmap.save(tempFilePath, { overwrite: true }, guard(() => {
console.log('[CreatePage] App临时文件保存成功:', tempFilePath);
bitmap.clear();
uni.uploadFile({
url: ossData.host,
filePath: tempFilePath,
@ -447,7 +451,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
console.log('[CreatePage] App上传响应:', uploadRes);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
@ -459,27 +463,27 @@ const uploadImageToOss = async (base64Data, ossData) => {
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[CreatePage] App上传失败:', error);
reject(error);
}
})
});
}, (error) => {
}, guard((error) => {
console.error('[CreatePage] App保存临时文件失败:', error);
bitmap.clear();
reject(new Error('保存临时文件失败'));
});
})));
}).catch((err) => {
console.error('[CreatePage] getSandboxFileUri 失败:', err);
bitmap.clear();
reject(err);
});
}, (error) => {
}, guard((error) => {
console.error('[CreatePage] App加载base64失败:', error);
bitmap.clear();
reject(new Error('加载图片数据失败'));
});
})));
// #endif
});
};

View File

@ -53,6 +53,7 @@ import { ref, computed, watch, getCurrentInstance } from 'vue';
import SharePreviewCard from './SharePreviewCard.vue';
import ShareActionBar from './ShareActionBar.vue';
import { useShare } from '@/composables/useShare.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
import { LANDING_BASE, SHARE_TARGETS } from '@/utils/constants.js';
import { trackShareApi } from '@/utils/api.js';
@ -96,6 +97,10 @@ const { state, pick, currentSlogan, systemType } = useShare({
canvasId: props.externalCanvasId || 'shareCanvas'
});
// ShareModal (uni.setClipboardData)
// useShare , ShareModal
const { guard } = useAliveGuard();
watch(() => props.visible, (v) => { visibleLocal.value = v; });
function handleClose() { emit('close'); visibleLocal.value = false; }
@ -129,7 +134,7 @@ function copyLink() {
uni.setClipboardData({
data: url,
success: () => {
success: guard(() => {
uni.showToast({ title: '链接已复制' });
// :""
// (,)
@ -146,10 +151,10 @@ function copyLink() {
console.warn('[ShareModal] trackShare(copy_link) failed:', e);
});
}
},
fail: () => {
}),
fail: guard(() => {
uni.showToast({ title: '复制失败', icon: 'none' });
}
})
});
}
</script>

View File

@ -47,10 +47,14 @@ import CreateFeed from './create-feed.vue';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import { getSandboxFileUri } from '@/utils/ioPath.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
const activeTab = ref(0);
const formData = ref(null);
// alive :, state, iOS WebView
const { guard } = useAliveGuard();
//
const switchTab = (index) => {
activeTab.value = index;
@ -118,21 +122,21 @@ const uploadImageToOss = async (base64Image, ossData) => {
const sandboxUri = await getSandboxFileUri(['discover', 'tmp'], fileName)
return new Promise((resolve, reject) => {
const bitmap = new plus.nativeObj.Bitmap('temp_skip');
bitmap.loadBase64Data(base64Image, () => {
bitmap.save(sandboxUri, { overwrite: true }, () => {
bitmap.loadBase64Data(base64Image, guard(() => {
bitmap.save(sandboxUri, { overwrite: true }, guard(() => {
bitmap.clear();
resolve(sandboxUri);
}, (error) => {
}, guard((error) => {
bitmap.clear();
reject(new Error('图片保存失败'));
});
}, (error) => {
})));
}, guard((error) => {
bitmap.clear();
reject(new Error('加载base64失败'));
});
})));
});
})();
// OSS
imageUrl = await new Promise((resolve, reject) => {
uni.uploadFile({
@ -149,17 +153,17 @@ const uploadImageToOss = async (base64Image, ossData) => {
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const url = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(url);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
reject(error);
}
})
});
});
// #endif

View File

@ -2,12 +2,12 @@
<view class="generation-result">
<!-- 背景图 -->
<image class="background-image" src="/static/background/exhibitionSuccess.png" mode="aspectFill" />
<!-- 星星装饰 -->
<view class="stars-container">
<view v-for="i in 20" :key="i" class="star" :style="getStarStyle(i)"></view>
</view>
<!-- 顶部奖励提示 -->
<view class="reward-tips">
<view class="reward-item">
@ -21,68 +21,39 @@
<text class="reward-text">X 2</text>
</view>
</view>
<!-- 光栅卡单张工作台预览陀螺仪 -->
<view
v-if="isLenticularDisplay"
class="lenticular-result-wrap"
:class="{ 'cards-visible': isGiftOpened }"
>
<view v-if="isLenticularDisplay" class="lenticular-result-wrap" :class="{ 'cards-visible': isGiftOpened }">
<view class="lenticular-result-card">
<LenticularCard
class="lenticular-preview"
:layers="lenticularLayers"
:transforms="layerTransforms"
gyro-source="simulation"
tilt-hint-text="晃动查看"
:shimmer-mid-opacity="0.16"
:simulate-tilt-from-normalized="simulate"
/>
<LenticularCard class="lenticular-preview" :layers="lenticularLayers" :transforms="layerTransforms"
gyro-source="simulation" tilt-hint-text="晃动查看" :shimmer-mid-opacity="0.16"
:simulate-tilt-from-normalized="simulate" />
</view>
</view>
<!-- 星卡 / AI 四图多图候选 -->
<view v-else class="cards-container" :class="{ 'cards-visible': isGiftOpened }">
<view
v-for="(image, index) in generatedImages"
:key="index"
class="card-item"
:class="{ 'card-selected': selectedIndex === index }"
:style="getCardStyle(index)"
@click="selectCard(index)"
>
<view v-for="(image, index) in generatedImages" :key="index" class="card-item"
:class="{ 'card-selected': selectedIndex === index }" :style="getCardStyle(index)"
@click="selectCard(index)">
<view class="card-frame">
<image class="card-image" :src="image" mode="aspectFill" />
</view>
</view>
</view>
<!-- 礼盒 -->
<view class="gift-box" :class="{ 'gift-opened': isGiftOpened }">
<image
v-if="!isGiftOpened"
class="gift-image"
src="/static/nft/lihe.png"
mode="aspectFit"
/>
<image
v-else
class="gift-image gift-image-opened"
src="/static/nft/lihe_kaiqi.png"
mode="aspectFit"
/>
<image v-if="!isGiftOpened" class="gift-image" src="/static/nft/lihe.png" mode="aspectFit" />
<image v-else class="gift-image gift-image-opened" src="/static/nft/lihe_kaiqi.png" mode="aspectFit" />
</view>
<!-- 底部按钮 -->
<view class="bottom-action" :class="{ 'bottom-action--row': isCraftDetailFlow }">
<view
v-if="isCraftDetailFlow"
class="action-button action-button--secondary"
@tap="handleRegenerate"
>
<view v-if="isCraftDetailFlow" class="action-button action-button--secondary" @tap="handleRegenerate">
<text class="button-text">重新生成</text>
</view>
<view class="action-button" @tap="selectAsset">
<view class="action-button" @tap="selectAsset">
<text class="button-text">{{ primaryActionLabel }}</text>
</view>
</view>
@ -91,9 +62,11 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import { getSandboxFileUri, getSandboxFile, clearSandboxSubdir } from '@/utils/ioPath.js';
import { useAliveGuard } from '@/composables/useAliveGuard.js';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import {
@ -154,7 +127,7 @@ const getStarStyle = (index) => {
const top = Math.random() * 100;
const size = Math.random() * 3 + 1;
const delay = Math.random() * 3;
return {
left: `${left}%`,
top: `${top}%`,
@ -184,7 +157,7 @@ const getCardStyle = (index) => {
// 44
{ left: 510, top: 624, rotate: '5deg', scale: 0.72, zIndex: 10 }
];
//
let posIndex;
if (selectedIndex.value === -1) {
@ -216,9 +189,9 @@ const getCardStyle = (index) => {
}
}
}
const pos = allPositions[posIndex];
// 1.5
if (selectedIndex.value === index) {
return {
@ -229,7 +202,7 @@ const getCardStyle = (index) => {
zIndex: 100 //
};
}
return {
left: `${pos.left}rpx`,
top: `${pos.top}rpx`,
@ -238,10 +211,13 @@ const getCardStyle = (index) => {
};
};
// alive :, state, iOS WebView
const { guard } = useAliveGuard();
//
const selectCard = (index) => {
selectedIndex.value = index;
//
// uni.showToast({
// title: `${index + 1}`,
@ -257,11 +233,11 @@ const base64ToBlob = (base64Data) => {
const raw = atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; i++) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
};
@ -269,35 +245,35 @@ const base64ToBlob = (base64Data) => {
const uploadImageToOss = async (base64Image) => {
console.log('[GenerationResult] uploadImageToOss 开始');
isUploading.value = true;
try {
uni.showLoading({ title: '上传中...', mask: true });
// 1. OSS
const signRes = await getOssSignatureApi('asset');
console.log('[GenerationResult] 获取签名结果:', signRes);
if (signRes.code !== 0) {
throw new Error(signRes.message || '获取签名失败');
}
// order_id
currentOrderId.value = signRes.data.order_id || '';
console.log('[GenerationResult] order_id:', currentOrderId.value);
// 2.
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 8);
const fileName = `ai_generated_${timestamp}_${randomStr}.png`;
let imageUrl;
// #ifdef H5
// H5使FormData + Blob
console.log('[GenerationResult] H5环境使用Blob上传');
const blob = base64ToBlob(base64Image);
console.log('[GenerationResult] Blob创建成功大小:', blob.size);
const formData = new FormData();
formData.append('key', signRes.data.dir + fileName);
formData.append('policy', signRes.data.policy);
@ -308,48 +284,48 @@ const uploadImageToOss = async (base64Image) => {
formData.append('x-oss-signature', signRes.data.signature);
formData.append('x-oss-signature-version', signRes.data.x_oss_signature_version);
formData.append('file', blob, fileName);
const response = await fetch(signRes.data.host, {
method: 'POST',
body: formData
});
console.log('[GenerationResult] OSS响应状态:', response.status);
if (response.ok || response.status === 204) {
imageUrl = `${signRes.data.host}/${signRes.data.dir}${fileName}`;
} else {
throw new Error(`上传失败,状态码: ${response.status}`);
}
// #endif
// #ifndef H5
// App/使uni.uploadFile
console.log('[GenerationResult] 非H5环境使用uni.uploadFile');
let tempFilePath; //
// #ifdef APP-PLUS
// App使base64
console.log('[GenerationResult] App环境使用base64临时文件');
// 使uni.saveFilebase64
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
//
tempFilePath = await new Promise((resolve, reject) => {
// 使unibase64ToTempFilePath
if (uni.base64ToTempFilePath) {
uni.base64ToTempFilePath({
base64Data: base64Image,
success: (res) => {
success: guard((res) => {
console.log('[GenerationResult] base64转临时文件成功:', res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] base64转临时文件失败:', error);
reject(new Error('base64转临时文件失败'));
}
})
});
} else {
// 使plus.io
@ -357,60 +333,60 @@ const uploadImageToOss = async (base64Image) => {
console.log('[GenerationResult] 使用plus.io方案');
getSandboxFileUri(['discover', 'tmp'], fileName).then((tempPath) => {
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64Image, () => {
bitmap.save(tempPath, { overwrite: true }, () => {
bitmap.loadBase64Data(base64Image, guard(() => {
bitmap.save(tempPath, { overwrite: true }, guard(() => {
console.log('[GenerationResult] 图片保存成功:', tempPath);
bitmap.clear();
resolve(tempPath);
}, (error) => {
}), guard((error) => {
console.error('[GenerationResult] 图片保存失败:', error);
bitmap.clear();
reject(new Error('图片保存失败'));
});
}, (error) => {
}));
}, guard((error) => {
console.error('[GenerationResult] 加载base64失败:', error);
bitmap.clear();
reject(new Error('加载base64失败'));
});
})));
}).catch(reject);
}
});
// #endif
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
// 使FileSystemManager
console.log('[GenerationResult] 小程序环境');
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
// #ifdef MP-WEIXIN
tempFilePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
// #endif
// #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
tempFilePath = `${uni.env.USER_DATA_PATH}/${fileName}`;
// #endif
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
const fs = uni.getFileSystemManager();
await new Promise((resolve, reject) => {
fs.writeFile({
filePath: tempFilePath,
data: base64Data,
encoding: 'base64',
success: () => {
success: guard(() => {
console.log('[GenerationResult] 文件写入成功');
resolve();
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] 文件写入失败:', error);
reject(new Error('保存临时文件失败'));
}
})
});
});
// #endif
console.log('[GenerationResult] 开始上传到OSS文件路径:', tempFilePath);
// OSS
imageUrl = await new Promise((resolve, reject) => {
uni.uploadFile({
@ -427,10 +403,10 @@ const uploadImageToOss = async (base64Image) => {
'x-oss-signature': signRes.data.signature,
'x-oss-signature-version': signRes.data.x_oss_signature_version
},
success: (uploadRes) => {
success: guard((uploadRes) => {
console.log('[GenerationResult] OSS上传响应:', uploadRes.statusCode);
console.log('[GenerationResult] OSS上传完整响应:', uploadRes);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const url = `${signRes.data.host}/${signRes.data.dir}${fileName}`;
console.log('[GenerationResult] 上传成功URL:', url);
@ -442,15 +418,15 @@ const uploadImageToOss = async (base64Image) => {
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
}),
fail: guard((error) => {
console.error('[GenerationResult] OSS上传失败:', error);
reject(error);
}
})
});
});
// #endif
console.log('[GenerationResult] 上传成功URL:', imageUrl);
uni.hideLoading();
uni.showToast({
@ -460,7 +436,7 @@ const uploadImageToOss = async (base64Image) => {
});
isUploading.value = false;
return imageUrl;
} catch (error) {
console.error('[GenerationResult] 上传失败:', error);
uni.hideLoading();
@ -480,7 +456,7 @@ const uploadToOssH5 = async (base64Image, fileName, ossData) => {
try {
// base64Blob
const blob = base64ToBlob(base64Image);
// FormData
const formData = new FormData();
formData.append('key', ossData.dir + fileName);
@ -492,32 +468,32 @@ const uploadToOssH5 = async (base64Image, fileName, ossData) => {
formData.append('x-oss-signature', ossData.signature);
formData.append('x-oss-signature-version', ossData.x_oss_signature_version);
formData.append('file', blob, fileName);
// 使fetch
fetch(resolveH5OssPostUrl(ossData.host), {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok || response.status === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
.then(response => {
if (response.ok || response.status === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
});
isUploading.value = false;
resolve(imageUrl); // URL
} else {
throw new Error(`上传失败,状态码: ${response.status}`);
}
})
.catch(error => {
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
});
isUploading.value = false;
resolve(imageUrl); // URL
} else {
throw new Error(`上传失败,状态码: ${response.status}`);
}
})
.catch(error => {
uni.hideLoading();
isUploading.value = false;
reject(error);
});
reject(error);
});
} catch (error) {
uni.hideLoading();
isUploading.value = false;
@ -530,20 +506,20 @@ const uploadToOssH5 = async (base64Image, fileName, ossData) => {
const uploadToOssNative = async (base64Image, fileName, ossData) => {
console.log('[GenerationResult] uploadToOssNative 开始');
console.log('[GenerationResult] fileName:', fileName);
return new Promise((resolve, reject) => {
try {
// base64
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
console.log('[GenerationResult] base64数据长度:', base64Data.length);
// #ifdef MP-WEIXIN
//
console.log('[GenerationResult] 使用微信小程序环境');
const fs = uni.getFileSystemManager();
const tempFilePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
fs.writeFile({
filePath: tempFilePath,
data: base64Data,
@ -560,13 +536,13 @@ const uploadToOssNative = async (base64Image, fileName, ossData) => {
}
});
// #endif
// #ifdef APP-PLUS
// App
console.log('[GenerationResult] 使用App环境');
const fs = plus.io.getFileSystemManager ? uni.getFileSystemManager() : null;
console.log('[GenerationResult] FileSystemManager:', fs ? '可用' : '不可用');
if (!fs) {
// 使plus.io API
console.log('[GenerationResult] 使用plus.io API');
@ -610,7 +586,7 @@ const uploadToOssNative = async (base64Image, fileName, ossData) => {
console.log('[GenerationResult] 使用FileSystemManager API');
const tempFilePath = `${plus.io.getStorageRootPath()}${fileName}`;
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
fs.writeFile({
filePath: tempFilePath,
data: base64Data,
@ -628,7 +604,7 @@ const uploadToOssNative = async (base64Image, fileName, ossData) => {
});
}
// #endif
} catch (error) {
console.error('[GenerationResult] uploadToOssNative异常:', error);
uni.hideLoading();
@ -643,7 +619,7 @@ const uploadFileToOss = (tempFilePath, fileName, ossData, resolve, reject, fs) =
console.log('[GenerationResult] uploadFileToOss 开始');
console.log('[GenerationResult] tempFilePath:', tempFilePath);
console.log('[GenerationResult] OSS host:', ossData.host);
uni.uploadFile({
url: ossData.host,
filePath: tempFilePath,
@ -661,11 +637,11 @@ const uploadFileToOss = (tempFilePath, fileName, ossData, resolve, reject, fs) =
success: (uploadRes) => {
console.log('[GenerationResult] OSS上传响应:', uploadRes);
console.log('[GenerationResult] 状态码:', uploadRes.statusCode);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
console.log('[GenerationResult] 上传成功图片URL:', imageUrl);
//
if (fs) {
fs.unlink({
@ -674,7 +650,7 @@ const uploadFileToOss = (tempFilePath, fileName, ossData, resolve, reject, fs) =
fail: (err) => console.error('[GenerationResult] 删除临时文件失败:', err)
});
}
resolve(imageUrl);
} else {
console.error('[GenerationResult] 上传失败,状态码:', uploadRes.statusCode);
@ -697,7 +673,7 @@ const selectAsset = async () => {
});
return;
}
if (isUploading.value) {
uni.showToast({
title: '上传中,请稍候',
@ -711,7 +687,7 @@ const selectAsset = async () => {
const selectedImage = getSelectedImageUrl();
console.log('1')
if (isDetailAfterSelect(orderValue)) {
console.log('1')
console.log('1')
isUploading.value = true;
uni.showLoading({ title: '准备中…', mask: true });
@ -733,14 +709,14 @@ const selectAsset = async () => {
}
return;
}
try {
// OSScurrentOrderId
const imageUrl = await uploadImageToOss(selectedImage);
//
uni.showLoading({ title: '创建订单中...', mask: true });
// CreateMintOrderRequestDTO
const orderData = {
order_id: currentOrderId.value,
@ -752,11 +728,11 @@ const selectAsset = async () => {
material_type: orderValue.material_type || orderValue.materialType || '',
info: orderValue.info || orderValue.event || '',
};
// API
const response = await createMintOrderApi(orderData);
uni.hideLoading();
if (response.code !== 0) {
throw new Error(response.message || '创建订单失败');
}
@ -772,10 +748,10 @@ const selectAsset = async () => {
info: orderValue.info || '',
event: orderValue.info || orderValue.event || '',
};
// storage
uni.setStorageSync('temp_nft_data', JSON.stringify(nftData));
//
uni.navigateTo({
url: '/pages/castlove/success'
@ -846,7 +822,7 @@ onMounted(() => {
<style scoped>
.generation-result {
position: relative ;
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
@ -881,10 +857,13 @@ onMounted(() => {
}
@keyframes starTwinkle {
0%, 100% {
0%,
100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
@ -922,7 +901,7 @@ onMounted(() => {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 600;
text-shadow:
text-shadow:
0 2rpx 8rpx rgba(0, 0, 0, 0.6),
0 0 20rpx rgba(255, 107, 157, 0.5);
white-space: nowrap;
@ -966,6 +945,7 @@ onMounted(() => {
transform: scale(0) translateY(100rpx);
opacity: 0;
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
@ -997,9 +977,12 @@ onMounted(() => {
}
@keyframes cardFloat {
0%, 100% {
0%,
100% {
transform: translateY(0) rotate(var(--rotate, 0deg));
}
50% {
transform: translateY(-20rpx) rotate(var(--rotate, 0deg));
}
@ -1009,13 +992,13 @@ onMounted(() => {
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(135deg,
rgba(180, 220, 255, 0.95) 0%,
rgba(200, 230, 255, 0.95) 50%,
rgba(220, 240, 255, 0.95) 100%);
background: linear-gradient(135deg,
rgba(180, 220, 255, 0.95) 0%,
rgba(200, 230, 255, 0.95) 50%,
rgba(220, 240, 255, 0.95) 100%);
border-radius: 20rpx;
padding: 10rpx;
box-shadow:
box-shadow:
0 0 40rpx rgba(100, 200, 255, 0.5),
0 20rpx 60rpx rgba(0, 0, 0, 0.4),
inset 0 2rpx 10rpx rgba(255, 255, 255, 0.6);
@ -1030,10 +1013,10 @@ onMounted(() => {
left: -8rpx;
right: -8rpx;
bottom: -8rpx;
background: linear-gradient(135deg,
rgba(100, 200, 255, 0.6) 0%,
rgba(150, 220, 255, 0.6) 50%,
rgba(200, 240, 255, 0.6) 100%);
background: linear-gradient(135deg,
rgba(100, 200, 255, 0.6) 0%,
rgba(150, 220, 255, 0.6) 50%,
rgba(200, 240, 255, 0.6) 100%);
border-radius: 22rpx;
z-index: -1;
filter: blur(10rpx);
@ -1074,10 +1057,10 @@ onMounted(() => {
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.5) 50%,
transparent 100%);
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.5) 50%,
transparent 100%);
animation: shine 3s ease-in-out infinite;
}
@ -1085,7 +1068,9 @@ onMounted(() => {
0% {
left: -100%;
}
50%, 100% {
50%,
100% {
left: 100%;
}
}
@ -1153,7 +1138,7 @@ onMounted(() => {
display: flex;
align-items: center;
justify-content: center;
box-shadow:
box-shadow:
0 0 30rpx rgba(0, 212, 255, 0.8),
0 8rpx 30rpx rgba(0, 0, 0, 0.4);
animation: checkPop 0.3s ease-out;
@ -1165,9 +1150,11 @@ onMounted(() => {
0% {
transform: translate(-50%, -50%) scale(0);
}
50% {
transform: translate(-50%, -50%) scale(1.2);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
@ -1203,9 +1190,12 @@ onMounted(() => {
}
@keyframes giftFloat {
0%, 100% {
0%,
100% {
transform: translateX(-50%) translateY(0);
}
50% {
transform: translateX(-50%) translateY(-20rpx);
}
@ -1227,9 +1217,11 @@ onMounted(() => {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;