1414 lines
37 KiB
Vue
1414 lines
37 KiB
Vue
<template>
|
||
<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">
|
||
<text class="reward-text">恭喜您获得水晶</text>
|
||
<image class="reward-icon" src="/static/icon/crystal.png" mode="aspectFit" />
|
||
<text class="reward-text">X 32</text>
|
||
</view>
|
||
<view class="reward-item">
|
||
<text class="reward-text">恭喜您获小卡碎片</text>
|
||
<image class="reward-icon" src="/static/starcity/xiaoka.png" mode="aspectFit" />
|
||
<text class="reward-text">X 2</text>
|
||
</view>
|
||
</view> -->
|
||
|
||
<!-- 光栅卡:单张工作台预览(陀螺仪) -->
|
||
<view
|
||
class="lenticular-result-wrap"
|
||
:class="{ 'cards-visible': isGiftOpened }"
|
||
>
|
||
<view class="lenticular-result-card">
|
||
<view class="card-wrapper craft-card-wrapper">
|
||
<image
|
||
class="card-frame"
|
||
src="/static/square/gerenzhongxincangpinkuang.png"
|
||
mode="aspectFit"
|
||
/>
|
||
<view class="craft-lenticular-slot">
|
||
<LenticularCard
|
||
v-if="lenticularLayers.length > 0"
|
||
class="craft-lenticular-card"
|
||
:layers="lenticularLayers"
|
||
:transforms="layerTransforms"
|
||
:show-slider="true"
|
||
:simulate-tilt="simulateTilt"
|
||
:simulate-tilt-from-normalized="simulate"
|
||
:simulate-tilt-reset="simulateTiltReset"
|
||
tilt-hint-text="晃动或滑动查看"
|
||
:shimmer-mid-opacity="0.16"
|
||
/>
|
||
</view>
|
||
</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"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 底部按钮 -->
|
||
<view
|
||
class="bottom-action"
|
||
:class="{ 'bottom-action--row': isCraftDetailFlow }"
|
||
>
|
||
<view
|
||
v-if="isCraftDetailFlow"
|
||
class="action-button action-button--secondary"
|
||
@tap="handleRegenerate"
|
||
>
|
||
<text class="button-text">重新生成</text>
|
||
</view>
|
||
<view class="action-button" @tap="selectAsset">
|
||
<text class="button-text">开始铸造</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 铸造确认弹窗 -->
|
||
<ConfirmModal
|
||
v-if="showConfirmModal"
|
||
:visible="showConfirmModal"
|
||
:cost-crystal="confirmCostInfo.costCrystal"
|
||
:current-balance="confirmCostInfo.currentBalance"
|
||
:mint-count="confirmCostInfo.mintCount"
|
||
:next-tier-cost="confirmCostInfo.nextTierCost"
|
||
@confirm="handleConfirmMint"
|
||
@cancel="handleCancelMint"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<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";
|
||
import { buildLenticularLayersTwo } from "@/utils/castloveMintForm.js";
|
||
import {
|
||
CASTLOVE_FORM_KEY,
|
||
completeSelectionAndOpenDetail,
|
||
isDetailAfterSelect,
|
||
STUDIO_LENTICULAR,
|
||
} from "@/utils/castloveGenerationFlow.js";
|
||
import { readDraft, removeDraft } from "@/utils/draftStorage";
|
||
import { submitCraftMintFromPath } from "@/utils/craftMintSubmit.js";
|
||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||
|
||
const generatedImages = ref([]);
|
||
const selectedIndex = ref(-1);
|
||
const isUploading = ref(false);
|
||
const currentOrderId = ref("");
|
||
const isGiftOpened = ref(false);
|
||
const craftFormData = ref(null);
|
||
const resultMeta = ref("lenticular");
|
||
|
||
// 确认铸造弹窗
|
||
const showConfirmModal = ref(false);
|
||
const confirmCostInfo = ref({
|
||
costCrystal: 0,
|
||
currentBalance: 0,
|
||
mintCount: 0,
|
||
nextTierCost: 0,
|
||
});
|
||
|
||
const lenticularLayers = ref([]);
|
||
const {
|
||
layerTransforms,
|
||
simulate, // 连续版(re-exported from useLenticularPreview),给 auto + 触摸用,避免卡顿
|
||
simulateTilt, // 离散 craft 版(snap + 滞回),给滑块用,保留清晰分层
|
||
simulateTiltFromNormalized,
|
||
simulateTiltReset,
|
||
} = useLenticularCraftTiltPreview(lenticularLayers);
|
||
|
||
// 注:tiltValue / onSliderChange / onSliderReset / onTouchSimulate 都已迁移到
|
||
// LenticularCard 组件内部(auto 控制 + 触摸/滑块/复位 都由组件自己处理)。
|
||
// 这里只需要把引擎函数作为 props 传给 card 即可。
|
||
|
||
// 光栅卡详情确认流程
|
||
const isCraftDetailFlow = computed(() =>
|
||
isDetailAfterSelect(craftFormData.value),
|
||
);
|
||
// 光栅展示模式:结果显示为光栅 或 表单数据为光栅类型
|
||
const isLenticularDisplay = computed(
|
||
() =>
|
||
resultMeta.value?.displayMode === STUDIO_LENTICULAR ||
|
||
craftFormData.value?.studio_kind === STUDIO_LENTICULAR,
|
||
);
|
||
|
||
// const primaryActionLabel = computed(() =>
|
||
// isCraftDetailFlow.value ? '选择作品' : '选择您的藏品'
|
||
// );
|
||
|
||
const handleRegenerate = () => {
|
||
uni.navigateBack({ delta: 2 });
|
||
};
|
||
|
||
// 更新本地存储的余额
|
||
function updateLocalBalance(newBalance) {
|
||
try {
|
||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,消除 `typeof === 'string' ? JSON.parse : spread`
|
||
// 在 userStr === "" 时仍会 JSON.parse 抛错的隐患
|
||
const user = getStoredUser();
|
||
if (!user) return;
|
||
user.crystal_balance = Number(newBalance) || 0;
|
||
uni.setStorageSync("user", JSON.stringify(user));
|
||
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
|
||
} catch (e) {
|
||
console.warn("[lenticular-result] 更新本地余额失败:", e);
|
||
}
|
||
}
|
||
|
||
// 获取星星样式
|
||
const getStarStyle = (index) => {
|
||
const left = Math.random() * 100;
|
||
const top = Math.random() * 100;
|
||
const size = Math.random() * 3 + 1;
|
||
const delay = Math.random() * 3;
|
||
|
||
return {
|
||
left: `${left}%`,
|
||
top: `${top}%`,
|
||
width: `${size}rpx`,
|
||
height: `${size}rpx`,
|
||
animationDelay: `${delay}s`,
|
||
};
|
||
};
|
||
|
||
// 将base64转换为Blob
|
||
const base64ToBlob = (base64Data) => {
|
||
const parts = base64Data.split(";base64,");
|
||
const contentType = parts[0].split(":")[1];
|
||
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 });
|
||
};
|
||
|
||
// alive 守卫:页面销毁后,原生异步回调不再触碰 state,避免 iOS WebView 报错
|
||
const { guard } = useAliveGuard();
|
||
|
||
// 上传图片到OSS
|
||
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);
|
||
formData.append("success_action_status", "200");
|
||
formData.append("x-oss-credential", signRes.data.x_oss_credential);
|
||
formData.append("x-oss-date", signRes.data.x_oss_date);
|
||
formData.append("x-oss-security-token", signRes.data.security_token);
|
||
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: guard((res) => {
|
||
console.log(
|
||
"[GenerationResult] base64转临时文件成功:",
|
||
res.tempFilePath,
|
||
);
|
||
resolve(res.tempFilePath);
|
||
}),
|
||
fail: guard((error) => {
|
||
console.error("[GenerationResult] base64转临时文件失败:", error);
|
||
reject(new Error("base64转临时文件失败"));
|
||
}),
|
||
});
|
||
} else {
|
||
// 降级方案:使用plus.io
|
||
// Android 10+ 适配:bitmap.save 接受沙盒 file:// 绝对路径(避免 '_doc/' 字符串写失败)
|
||
console.log("[GenerationResult] 使用plus.io方案");
|
||
getSandboxFileUri(["lenticular-result", "tmp"], fileName)
|
||
.then((tempPath) => {
|
||
const bitmap = new plus.nativeObj.Bitmap("temp");
|
||
bitmap.loadBase64Data(
|
||
base64Image,
|
||
guard(
|
||
() => {
|
||
bitmap.save(
|
||
tempPath,
|
||
{ overwrite: true },
|
||
guard(
|
||
() => {
|
||
console.log(
|
||
"[GenerationResult] 图片保存成功:",
|
||
tempPath,
|
||
);
|
||
bitmap.clear();
|
||
resolve(tempPath);
|
||
},
|
||
guard((error) => {
|
||
console.error(
|
||
"[GenerationResult] 图片保存失败:",
|
||
error,
|
||
);
|
||
bitmap.clear();
|
||
reject(new 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: guard(() => {
|
||
console.log("[GenerationResult] 文件写入成功");
|
||
resolve();
|
||
}),
|
||
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({
|
||
url: signRes.data.host,
|
||
filePath: tempFilePath,
|
||
name: "file",
|
||
formData: {
|
||
key: signRes.data.dir + fileName,
|
||
policy: signRes.data.policy,
|
||
success_action_status: "200",
|
||
"x-oss-credential": signRes.data.x_oss_credential,
|
||
"x-oss-date": signRes.data.x_oss_date,
|
||
"x-oss-security-token": signRes.data.security_token,
|
||
"x-oss-signature": signRes.data.signature,
|
||
"x-oss-signature-version": signRes.data.x_oss_signature_version,
|
||
},
|
||
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);
|
||
// 上传成功后清理临时文件(doc/lenticular-result/tmp/),避免 doc 残留垃圾
|
||
clearSandboxSubdir(["lenticular-result", "tmp"]).catch((e) => {
|
||
console.warn(
|
||
"[GenerationResult] clear tmp dir failed:",
|
||
e?.message,
|
||
);
|
||
});
|
||
resolve(url);
|
||
} else {
|
||
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
|
||
}
|
||
}),
|
||
fail: guard((error) => {
|
||
console.error("[GenerationResult] OSS上传失败:", error);
|
||
reject(error);
|
||
}),
|
||
});
|
||
});
|
||
// #endif
|
||
|
||
console.log("[GenerationResult] 上传成功,URL:", imageUrl);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "上传成功",
|
||
icon: "success",
|
||
duration: 1500,
|
||
});
|
||
isUploading.value = false;
|
||
return imageUrl;
|
||
} catch (error) {
|
||
console.error("[GenerationResult] 上传失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || "上传失败",
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
isUploading.value = false;
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// H5环境上传
|
||
const uploadToOssH5 = async (base64Image, fileName, ossData) => {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// 将base64转换为Blob
|
||
const blob = base64ToBlob(base64Image);
|
||
|
||
// 构建FormData
|
||
const formData = new FormData();
|
||
formData.append("key", ossData.dir + fileName);
|
||
formData.append("policy", ossData.policy);
|
||
formData.append("success_action_status", "200");
|
||
formData.append("x-oss-credential", ossData.x_oss_credential);
|
||
formData.append("x-oss-date", ossData.x_oss_date);
|
||
formData.append("x-oss-security-token", ossData.security_token);
|
||
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) {
|
||
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();
|
||
isUploading.value = false;
|
||
reject(error);
|
||
});
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
isUploading.value = false;
|
||
reject(error);
|
||
}
|
||
});
|
||
};
|
||
|
||
// App/小程序环境上传
|
||
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,
|
||
encoding: "base64",
|
||
success: () => {
|
||
console.log("[GenerationResult] 文件写入成功");
|
||
uploadFileToOss(tempFilePath, fileName, ossData, resolve, reject, fs);
|
||
},
|
||
fail: (error) => {
|
||
console.error("[GenerationResult] 保存临时文件失败:", error);
|
||
uni.hideLoading();
|
||
isUploading.value = false;
|
||
reject(new Error("保存临时文件失败"));
|
||
},
|
||
});
|
||
// #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
|
||
// Android 10+ 适配:用 PRIVATE_DOC 沙盒根替代 '_doc' 字符串
|
||
console.log("[GenerationResult] 使用plus.io API");
|
||
getSandboxFileUri(["lenticular-result", "tmp"], fileName)
|
||
.then((tempFilePath) => {
|
||
console.log("[GenerationResult] 临时文件路径:", tempFilePath);
|
||
return getSandboxFile(["lenticular-result", "tmp"], fileName, true);
|
||
})
|
||
.then((fileEntry) => {
|
||
console.log("[GenerationResult] 获取文件系统成功");
|
||
fileEntry.createWriter((writer) => {
|
||
writer.onwrite = guard(() => {
|
||
console.log("[GenerationResult] 文件写入成功");
|
||
uploadFileToOss(
|
||
fileEntry.toLocalURL(),
|
||
fileName,
|
||
ossData,
|
||
resolve,
|
||
reject,
|
||
null,
|
||
);
|
||
});
|
||
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);
|
||
console.log(
|
||
"[GenerationResult] Blob创建成功,大小:",
|
||
blob.size,
|
||
);
|
||
writer.write(blob);
|
||
} catch (blobError) {
|
||
console.error("[GenerationResult] Blob转换失败:", blobError);
|
||
uni.hideLoading();
|
||
isUploading.value = false;
|
||
reject(new Error("Blob转换失败"));
|
||
}
|
||
});
|
||
})
|
||
.catch((error) => {
|
||
console.error("[GenerationResult] plus.io流程失败:", error);
|
||
uni.hideLoading();
|
||
isUploading.value = false;
|
||
reject(error);
|
||
});
|
||
} else {
|
||
// 使用FileSystemManager API
|
||
console.log("[GenerationResult] 使用FileSystemManager API");
|
||
const tempFilePath = `${plus.io.getStorageRootPath()}${fileName}`;
|
||
console.log("[GenerationResult] 临时文件路径:", tempFilePath);
|
||
|
||
fs.writeFile({
|
||
filePath: tempFilePath,
|
||
data: base64Data,
|
||
encoding: "base64",
|
||
success: guard(() => {
|
||
console.log("[GenerationResult] 文件写入成功");
|
||
uploadFileToOss(
|
||
tempFilePath,
|
||
fileName,
|
||
ossData,
|
||
resolve,
|
||
reject,
|
||
fs,
|
||
);
|
||
}),
|
||
fail: guard((error) => {
|
||
console.error("[GenerationResult] 保存临时文件失败:", error);
|
||
uni.hideLoading();
|
||
isUploading.value = false;
|
||
reject(new Error("保存临时文件失败"));
|
||
}),
|
||
});
|
||
}
|
||
// #endif
|
||
} catch (error) {
|
||
console.error("[GenerationResult] uploadToOssNative异常:", error);
|
||
uni.hideLoading();
|
||
isUploading.value = false;
|
||
reject(error);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 统一的文件上传逻辑
|
||
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,
|
||
name: "file",
|
||
formData: {
|
||
key: ossData.dir + fileName,
|
||
policy: ossData.policy,
|
||
success_action_status: "200",
|
||
"x-oss-credential": ossData.x_oss_credential,
|
||
"x-oss-date": ossData.x_oss_date,
|
||
"x-oss-security-token": ossData.security_token,
|
||
"x-oss-signature": ossData.signature,
|
||
"x-oss-signature-version": ossData.x_oss_signature_version,
|
||
},
|
||
success: guard((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({
|
||
filePath: tempFilePath,
|
||
success: () => console.log("[GenerationResult] 临时文件已删除"),
|
||
fail: (err) =>
|
||
console.error("[GenerationResult] 删除临时文件失败:", err),
|
||
});
|
||
}
|
||
|
||
resolve(imageUrl);
|
||
} else {
|
||
console.error(
|
||
"[GenerationResult] 上传失败,状态码:",
|
||
uploadRes.statusCode,
|
||
);
|
||
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
|
||
}
|
||
}),
|
||
fail: guard((error) => {
|
||
console.error("[GenerationResult] OSS上传失败:", error);
|
||
reject(error);
|
||
}),
|
||
});
|
||
};
|
||
|
||
const getSelectedImageUrl = () => {
|
||
if (isLenticularDisplay.value) {
|
||
const mid = lenticularLayers.value.find((l) => l.id === "mid");
|
||
return mid?.src || craftFormData.value?.lenticularSubjectImage || "";
|
||
}
|
||
if (selectedIndex.value < 0) return "";
|
||
return generatedImages.value[selectedIndex.value] || "";
|
||
};
|
||
|
||
// 选择藏品
|
||
const selectAsset = async () => {
|
||
const imagePath = isLenticularDisplay.value
|
||
? lenticularLayers.value.find((l) => l.id === "mid")?.src || ""
|
||
: "";
|
||
const bgImagePath = isLenticularDisplay.value
|
||
? lenticularLayers.value.find((l) => l.id === "base")?.src || ""
|
||
: undefined;
|
||
|
||
console.log("[selectAsset] start", {
|
||
isLenticularDisplay: isLenticularDisplay.value,
|
||
imagePath: imagePath ? "has" : "empty",
|
||
bgImagePath: bgImagePath ? "has" : "empty",
|
||
lenticularLayers: lenticularLayers.value.map((l) => ({
|
||
id: l.id,
|
||
src: l.src ? l.src.substring(0, 50) : "",
|
||
})),
|
||
});
|
||
|
||
if (!imagePath) {
|
||
uni.showToast({ title: "缺少作品图", icon: "none" });
|
||
return;
|
||
}
|
||
|
||
// 获取铸造费用估算
|
||
try {
|
||
uni.showLoading({ title: "加载中…", mask: true });
|
||
const costRes = await estimateMintCostApi();
|
||
uni.hideLoading();
|
||
|
||
if (costRes.code === 0 && costRes.data) {
|
||
confirmCostInfo.value = {
|
||
costCrystal: costRes.data.cost_crystal || 0,
|
||
currentBalance: costRes.data.current_balance || 0,
|
||
mintCount: costRes.data.mint_count || 0,
|
||
nextTierCost: costRes.data.next_tier_cost || 0,
|
||
};
|
||
} else {
|
||
confirmCostInfo.value = {
|
||
costCrystal: 100,
|
||
currentBalance: 0,
|
||
mintCount: 0,
|
||
nextTierCost: 0,
|
||
};
|
||
}
|
||
} catch (e) {
|
||
uni.hideLoading();
|
||
console.error("[selectAsset] 获取铸造费用失败:", e);
|
||
confirmCostInfo.value = {
|
||
costCrystal: 100,
|
||
currentBalance: 0,
|
||
mintCount: 0,
|
||
nextTierCost: 0,
|
||
};
|
||
}
|
||
|
||
// 显示确认弹窗
|
||
showConfirmModal.value = true;
|
||
};
|
||
|
||
// 确认铸造
|
||
const handleConfirmMint = async () => {
|
||
showConfirmModal.value = false;
|
||
|
||
// 检查余额是否充足
|
||
if (
|
||
confirmCostInfo.value.currentBalance < confirmCostInfo.value.costCrystal
|
||
) {
|
||
uni.showToast({ title: "水晶余额不足,无法铸造", icon: "none" });
|
||
return;
|
||
}
|
||
|
||
const imagePath = isLenticularDisplay.value
|
||
? lenticularLayers.value.find((l) => l.id === "mid")?.src || ""
|
||
: "";
|
||
const bgImagePath = isLenticularDisplay.value
|
||
? lenticularLayers.value.find((l) => l.id === "base")?.src || ""
|
||
: undefined;
|
||
|
||
uni.showLoading({ title: "铸造中…", mask: true });
|
||
try {
|
||
await submitCraftMintFromPath({
|
||
imagePath,
|
||
bgImagePath,
|
||
formData: craftFormData.value,
|
||
});
|
||
// 只有铸造成功后才扣除本地余额
|
||
updateLocalBalance(confirmCostInfo.value.balanceAfter);
|
||
uni.hideLoading();
|
||
uni.navigateTo({ url: "/pages/castlove/success" });
|
||
} catch (e) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: e.message || "铸造失败", icon: "none" });
|
||
}
|
||
};
|
||
|
||
// 取消铸造
|
||
const handleCancelMint = () => {
|
||
showConfirmModal.value = false;
|
||
};
|
||
|
||
const initLenticularPreview = () => {
|
||
try {
|
||
const raw = readDraft("lenticular_studio_payload");
|
||
if (!raw) {
|
||
return;
|
||
}
|
||
const p = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||
const bgPath = p.bgPath || "";
|
||
const subjectPath = p.subjectPath || "";
|
||
lenticularLayers.value = buildLenticularLayersTwo(bgPath, subjectPath);
|
||
selectedIndex.value = 0;
|
||
} catch (e) {
|
||
console.error("[GenerationResult] lenticular init", e);
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
try {
|
||
const formDataStr = uni.getStorageSync(CASTLOVE_FORM_KEY);
|
||
if (formDataStr) {
|
||
craftFormData.value = JSON.parse(formDataStr);
|
||
}
|
||
const metaStr = readDraft("generation_result_meta");
|
||
if (metaStr) {
|
||
resultMeta.value = JSON.parse(metaStr);
|
||
removeDraft("generation_result_meta");
|
||
}
|
||
} catch (e) {
|
||
console.error("[GenerationResult] 读取表单失败:", e);
|
||
}
|
||
|
||
try {
|
||
const imagesData = readDraft("generated_images");
|
||
if (!imagesData) {
|
||
uni.showToast({ title: "未找到生成的图片", icon: "none" });
|
||
setTimeout(
|
||
() =>
|
||
uni.navigateTo({
|
||
url: "/pages/castlove/lenticular/lenticular-create",
|
||
}),
|
||
1500,
|
||
);
|
||
return;
|
||
}
|
||
const parsed = JSON.parse(imagesData);
|
||
removeDraft("generated_images");
|
||
|
||
if (
|
||
isLenticularDisplay.value ||
|
||
(parsed[0] && parsed[0].type === "lenticular")
|
||
) {
|
||
initLenticularPreview();
|
||
} else {
|
||
generatedImages.value = parsed;
|
||
if (generatedImages.value.length > 0) {
|
||
selectedIndex.value = 0;
|
||
}
|
||
}
|
||
|
||
setTimeout(() => {
|
||
isGiftOpened.value = true;
|
||
}, 800);
|
||
} catch (e) {
|
||
console.error("[GenerationResult] 读取图片数据失败:", e);
|
||
uni.showToast({ title: "数据错误", icon: "none" });
|
||
setTimeout(
|
||
() =>
|
||
uni.navigateTo({
|
||
url: "/pages/castlove/lenticular/lenticular-create",
|
||
}),
|
||
1500,
|
||
);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.generation-result {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
background: #000;
|
||
}
|
||
|
||
.background-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
}
|
||
|
||
.stars-container {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.star {
|
||
position: absolute;
|
||
background: #ffffff;
|
||
border-radius: 50%;
|
||
animation: starTwinkle 2s ease-in-out infinite;
|
||
box-shadow: 0 0 10rpx rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
@keyframes starTwinkle {
|
||
0%,
|
||
100% {
|
||
opacity: 0.3;
|
||
transform: scale(1);
|
||
}
|
||
|
||
50% {
|
||
opacity: 1;
|
||
transform: scale(1.2);
|
||
}
|
||
}
|
||
|
||
.reward-tips {
|
||
position: absolute;
|
||
top: 80rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
z-index: 11;
|
||
padding: 0 40rpx;
|
||
}
|
||
|
||
.reward-item {
|
||
background: transparent;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.reward-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.reward-text {
|
||
font-size: 28rpx;
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
text-shadow:
|
||
0 2rpx 8rpx rgba(0, 0, 0, 0.6),
|
||
0 0 20rpx rgba(255, 107, 157, 0.5);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.cards-container {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 10;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.5s ease-in-out;
|
||
}
|
||
|
||
.cards-container.cards-visible {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.card-item {
|
||
position: absolute;
|
||
width: 200rpx;
|
||
height: 260rpx;
|
||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||
cursor: pointer;
|
||
filter: drop-shadow(0 15rpx 40rpx rgba(0, 0, 0, 0.5));
|
||
transform: scale(0) translateY(100rpx);
|
||
opacity: 0;
|
||
}
|
||
|
||
.cards-visible .card-item {
|
||
animation: cardAppear 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||
opacity: 1;
|
||
}
|
||
|
||
@keyframes cardAppear {
|
||
0% {
|
||
transform: scale(0) translateY(100rpx);
|
||
opacity: 0;
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1) translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.card-item:active:not(.card-selected) {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.cards-visible .card-item:nth-child(1) {
|
||
animation-delay: 0s, 0.8s;
|
||
}
|
||
|
||
.cards-visible .card-item:nth-child(2) {
|
||
animation-delay: 0.15s, 0.95s;
|
||
}
|
||
|
||
.cards-visible .card-item:nth-child(3) {
|
||
animation-delay: 0.3s, 1.1s;
|
||
}
|
||
|
||
.cards-visible .card-item:nth-child(4) {
|
||
animation-delay: 0.45s, 1.25s;
|
||
}
|
||
|
||
.cards-visible .card-item:nth-child(5) {
|
||
animation-delay: 0.6s, 1.4s;
|
||
}
|
||
|
||
@keyframes cardFloat {
|
||
0%,
|
||
100% {
|
||
transform: translateY(0) rotate(var(--rotate, 0deg));
|
||
}
|
||
|
||
50% {
|
||
transform: translateY(-20rpx) rotate(var(--rotate, 0deg));
|
||
}
|
||
}
|
||
|
||
.card-frame {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: visible;
|
||
}
|
||
|
||
.topfans-label {
|
||
position: absolute;
|
||
top: 15rpx;
|
||
left: 15rpx;
|
||
background: linear-gradient(135deg, #a855ff 0%, #d946ef 100%);
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 30rpx;
|
||
z-index: 10;
|
||
box-shadow: 0 4rpx 15rpx rgba(168, 85, 255, 0.5);
|
||
}
|
||
|
||
.topfans-text {
|
||
font-size: 22rpx;
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
letter-spacing: 1rpx;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.card-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 14rpx;
|
||
display: block;
|
||
position: relative;
|
||
z-index: 1;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.card-shine {
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(
|
||
90deg,
|
||
transparent 0%,
|
||
rgba(255, 255, 255, 0.5) 50%,
|
||
transparent 100%
|
||
);
|
||
animation: shine 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes shine {
|
||
0% {
|
||
left: -100%;
|
||
}
|
||
|
||
50%,
|
||
100% {
|
||
left: 100%;
|
||
}
|
||
}
|
||
|
||
.topfans-badge {
|
||
position: absolute;
|
||
top: 20rpx;
|
||
left: 20rpx;
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
filter: drop-shadow(0 2rpx 8rpx rgba(255, 107, 157, 0.5));
|
||
}
|
||
|
||
.card-label {
|
||
position: absolute;
|
||
top: 24rpx;
|
||
right: 20rpx;
|
||
padding: 8rpx 16rpx;
|
||
background: rgba(255, 107, 157, 0.9);
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.label-text {
|
||
font-size: 20rpx;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.card-title {
|
||
position: absolute;
|
||
bottom: 20rpx;
|
||
left: 20rpx;
|
||
right: 20rpx;
|
||
padding: 12rpx;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
border-radius: 12rpx;
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.title-text {
|
||
font-size: 22rpx;
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
display: block;
|
||
}
|
||
|
||
.heart-icon {
|
||
position: absolute;
|
||
top: 20rpx;
|
||
right: 20rpx;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.selected-mark {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
background: linear-gradient(135deg, #00d4ff 0%, #0099ff 100%);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
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;
|
||
border: 4rpx solid rgba(255, 255, 255, 0.9);
|
||
z-index: 10;
|
||
}
|
||
|
||
@keyframes checkPop {
|
||
0% {
|
||
transform: translate(-50%, -50%) scale(0);
|
||
}
|
||
|
||
50% {
|
||
transform: translate(-50%, -50%) scale(1.2);
|
||
}
|
||
|
||
100% {
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
}
|
||
|
||
.check-icon {
|
||
font-size: 70rpx;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.card-selected {
|
||
z-index: 100 !important;
|
||
}
|
||
|
||
.gift-box {
|
||
position: absolute;
|
||
bottom: 20%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 480rpx;
|
||
height: 480rpx;
|
||
z-index: 3;
|
||
animation: giftFloat 3s ease-in-out infinite;
|
||
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.gift-box.gift-opened {
|
||
width: 17rem;
|
||
bottom: 15%;
|
||
animation: none;
|
||
}
|
||
|
||
@keyframes giftFloat {
|
||
0%,
|
||
100% {
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
|
||
50% {
|
||
transform: translateX(-50%) translateY(-20rpx);
|
||
}
|
||
}
|
||
|
||
.gift-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
transition: opacity 0.5s ease-in-out;
|
||
}
|
||
|
||
.gift-image-opened {
|
||
width: 17rem;
|
||
animation: giftOpenPop 0.5s ease-out;
|
||
}
|
||
|
||
@keyframes giftOpenPop {
|
||
0% {
|
||
transform: scale(0.8);
|
||
opacity: 0;
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.lenticular-result-wrap {
|
||
position: absolute;
|
||
top: 200rpx;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
z-index: 8;
|
||
opacity: 0;
|
||
transform: translateY(40rpx);
|
||
transition:
|
||
opacity 0.6s ease,
|
||
transform 0.6s ease;
|
||
}
|
||
|
||
.lenticular-result-wrap.cards-visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.lenticular-result-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.lenticular-result-card .card-wrapper {
|
||
position: relative;
|
||
width: 352rpx;
|
||
height: 520rpx;
|
||
}
|
||
|
||
.lenticular-result-card .craft-card-wrapper {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.lenticular-result-card .card-frame {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 2;
|
||
transform: rotate(-10deg);
|
||
}
|
||
|
||
.lenticular-result-card .craft-lenticular-slot {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 87%;
|
||
height: 96%;
|
||
border-radius: 48rpx;
|
||
transform: translate(-50%, -50%) rotate(-10deg);
|
||
z-index: 2;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.lenticular-result-card .craft-lenticular-card {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.bottom-action {
|
||
position: absolute;
|
||
bottom: 80rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 11;
|
||
}
|
||
|
||
.bottom-action--row {
|
||
flex-direction: row;
|
||
gap: 24rpx;
|
||
width: 90%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.action-button {
|
||
padding: 24rpx 80rpx;
|
||
background: linear-gradient(90deg, #ff6b9d 0%, #ffa8c5 100%);
|
||
border-radius: 50rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.5);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.action-button:active {
|
||
transform: scale(1.05);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.action-button--secondary {
|
||
background: linear-gradient(90deg, #8e9eab 0%, #b8c6db 100%);
|
||
box-shadow: 0 8rpx 24rpx rgba(120, 130, 150, 0.45);
|
||
}
|
||
|
||
.button-text {
|
||
font-size: 32rpx;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||
white-space: nowrap;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.hint-text {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
</style>
|