feat:增加守卫组合式函数本身
This commit is contained in:
parent
7467190e38
commit
6a6d5f9deb
89
frontend/composables/useAliveGuard.js
Normal file
89
frontend/composables/useAliveGuard.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) => {
|
||||
// 位置4(图片4):底部右
|
||||
{ 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.saveFile保存base64为临时文件
|
||||
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
|
||||
|
||||
|
||||
// 先转换为临时文件路径
|
||||
tempFilePath = await new Promise((resolve, reject) => {
|
||||
// 使用uni的base64ToTempFilePath(如果可用)
|
||||
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 {
|
||||
// 将base64转换为Blob
|
||||
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 {
|
||||
// 上传到OSS(会自动设置currentOrderId)
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user