Files
spring-festival-greetings/pages/make/index.vue
2026-01-22 00:01:30 +08:00

1021 lines
24 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="make-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
<!-- 预览卡片 -->
<view class="card-preview">
<image
class="card-bg"
:src="currentTemplate?.imageUrl"
mode="aspectFill"
/>
<view class="card-overlay">
<view class="title">
<text class="main">新春快乐</text>
<text class="sub">2026 YEAR OF THE HORSE</text>
</view>
<view class="bubble" @tap="activeTool = 'text'">
<text class="bubble-text" :style="{ color: selectedColor }">{{
targetName + "\n " + blessingText
}}</text>
</view>
<view class="user">
<image class="avatar" :src="userAvatar" mode="aspectFill" />
<view class="user-info">
<text class="user-name">{{ signatureName }}</text>
<text class="user-desc">送上祝福</text>
</view>
</view>
</view>
</view>
<view class="tip-line">
<text>点击卡片内容即可编辑</text>
</view>
<!-- 编辑工具区 -->
<view class="editor-panel">
<view class="drag-handle"></view>
<!-- 底部操作 -->
<view class="bottom-actions">
<button class="btn secondary" @tap="preview">
<uni-icons type="cloud-download" size="20" color="#888"></uni-icons>
<view>保存</view>
</button>
<button open-type="share" class="btn primary" @tap="shareOrSave">
<uni-icons
type="paperplane-filled"
size="20"
color="#fff"
></uni-icons>
<view>分享给好友</view>
</button>
</view>
<!-- 功能入口 -->
<view class="tools">
<view
v-for="(tool, idx) in tools"
:key="idx"
class="tool-item"
:class="{ active: activeTool === tool.type }"
@tap="activeTool = tool.type"
>
<view class="tool-icon">{{ tool.icon }}</view>
<text class="tool-text">{{ tool.text }}</text>
</view>
</view>
<!-- 模板区 -->
<view v-if="activeTool === 'template'" class="section">
<view class="section-title">
<text>热门模板</text>
</view>
<view class="tpl-scroll">
<view class="tpl-grid">
<view
v-for="(tpl, i) in templates"
:key="i"
class="tpl-card"
:class="{ selected: tpl?.id === currentTemplate?.id }"
@tap="applyTemplate(tpl)"
>
<image :src="tpl.imageUrl" class="tpl-cover" mode="aspectFill" />
<view class="tpl-name">{{ tpl.name }}</view>
<view v-if="tpl?.id === currentTemplate?.id" class="tpl-check"
></view
>
</view>
</view>
<view v-if="loadingTemplates" class="loading-more">加载中...</view>
<view
v-else-if="!hasMoreTemplates && templates.length > 0"
class="no-more"
>没有更多了</view
>
</view>
</view>
<!-- 文字编辑 -->
<view v-if="activeTool === 'text'" class="section text-edit-section">
<!-- 祝贺对象 -->
<view class="form-item">
<text class="label">祝贺对象</text>
<input
class="input-box"
v-model="targetName"
placeholder="请输入称呼"
placeholder-style="color:#ccc"
/>
</view>
<!-- 祝福语库 -->
<view class="form-item">
<view class="label-row">
<text class="label">祝福语库</text>
<view class="refresh-btn" @tap="refreshGreetings">
<text class="refresh-icon"></text> 换一批
</view>
</view>
<scroll-view scroll-x class="greeting-scroll" show-scrollbar="false">
<view class="greeting-list">
<view
v-for="(text, index) in displayedGreetings"
:key="index"
class="greeting-card"
:class="{ active: blessingText === text }"
@tap="selectGreeting(text)"
>
<text class="greeting-text">{{ text }}</text>
<view v-if="blessingText === text" class="check-mark"></view>
</view>
</view>
</scroll-view>
</view>
<!-- 署名 -->
<view class="form-item">
<text class="label">署名</text>
<view class="input-wrapper">
<input
class="input-box"
v-model="signatureName"
placeholder="请输入署名"
placeholder-style="color:#ccc"
/>
<text class="edit-icon"></text>
</view>
</view>
<!-- 文字颜色 -->
<view class="form-item">
<text class="label">文字颜色</text>
<view class="color-list">
<view
v-for="(color, index) in textColors"
:key="index"
class="color-item"
:style="{ background: color }"
@tap="selectedColor = color"
>
<view v-if="selectedColor === color" class="color-check"></view>
</view>
</view>
</view>
</view>
<!-- 图片/背景 -->
<view v-if="activeTool === 'image'" class="section">
<view class="section-title"><text>替换背景</text></view>
<view class="row">
<button class="btn" @tap="pickImage">从相册选择</button>
<button class="btn" @tap="resetBackground">重置为模板背景</button>
</view>
</view>
<!-- 头像挂饰 -->
<view v-if="activeTool === 'avatar'" class="section">
<view class="section-title"><text>头像挂饰</text></view>
<view class="row">
<button class="btn" @tap="toggleAvatarDecor">切换挂饰</button>
</view>
</view>
</view>
<canvas
canvas-id="cardCanvas"
class="hidden-canvas"
style="width: 540px; height: 960px"
/>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getBavBarHeight, getDeviceInfo } from "@/utils/system";
import { createCardTmp, getCardTemplateList } from "@/api/make";
import { createCardShareToken } from "@/api/card";
import { onShareAppMessage, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user";
const templatePage = ref(1);
const loadingTemplates = ref(false);
const hasMoreTemplates = ref(true);
const userStore = useUserStore();
const cardId = ref("");
const targetName = ref("祝您");
const signatureName = ref(userStore?.userInfo?.nickName || "xxx");
const userAvatar = ref(
userStore?.userInfo?.avatarUrl ||
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
);
const blessingText = ref(
"岁末将至敬颂冬绥。平安喜乐万事胜意。祝您2026年大吉大利一马当先前程似锦龙马精神阖家安康",
);
const textColors = ["#ffffff", "#ff3b30", "#F5A623", "#8B572A", "#000000"];
const selectedColor = ref("#ffffff");
const greetingLib = [
"在新的一年里身体健康,万事如意!马到成功,财源广进!",
"岁末将至敬颂冬绥。平安喜乐万事胜意。祝您2026年大吉大利一马当先前程似锦龙马精神阖家安康",
"一马当先,前程似锦!龙马精神,阖家安康!",
"骏马奔腾,福运常在!策马扬鞭,步步高升!",
"新春快乐,阖家幸福!愿您在新的一年里,所有的希望都能如愿,所有的梦想都能实现。",
"马年大吉!愿您事业如骏马奔腾,生活如春风得意!",
];
onLoad((options) => {
cardId.value = "69674f307307beac4519025f";
// createCard();
getTemplateList();
});
onReachBottom(() => {
if (activeTool.value === "template") {
loadMoreTemplates();
}
});
const createCard = async () => {
const res = await createCardTmp({
targetName: targetName.value,
signatureName: signatureName.value,
blessingText: blessingText.value,
});
if (res.id) {
cardId.value = res.id;
}
};
const getTemplateList = async (isLoadMore = false) => {
if (loadingTemplates.value || (!hasMoreTemplates.value && isLoadMore)) return;
loadingTemplates.value = true;
try {
const res = await getCardTemplateList(templatePage.value);
// 兼容数组或对象列表格式
const list = Array.isArray(res) ? res : res.list || [];
if (list.length > 0) {
if (isLoadMore) {
templates.value = [...templates.value, ...list];
} else {
templates.value = list;
// 初始加载时设置第一个为当前选中
if (list.length > 0 && !currentTemplate.value) {
currentTemplate.value = list[0];
}
}
// 判断是否还有更多
if (typeof res.hasNext !== "undefined") {
hasMoreTemplates.value = res.hasNext;
} else {
// 如果没有 hasNext 字段,根据返回数量简单判断
hasMoreTemplates.value = list.length >= 8; // 假设每页 10 条
}
if (hasMoreTemplates.value) {
templatePage.value++;
}
} else {
if (!isLoadMore) templates.value = [];
hasMoreTemplates.value = false;
}
} catch (error) {
console.error("加载模板失败:", error);
} finally {
loadingTemplates.value = false;
}
};
const loadMoreTemplates = () => {
getTemplateList(true);
};
onShareAppMessage(async () => {
const deviceInfo = getDeviceInfo();
const shareTokenRes = await createCardShareToken({
cardId: cardId.value,
...deviceInfo,
});
return {
title: "新春祝福",
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
imageUrl: "/static/images/bg.jpg",
};
});
const displayedGreetings = ref(greetingLib.slice(0, 2));
const refreshGreetings = () => {
const start = Math.floor(Math.random() * (greetingLib.length - 1));
// 简单随机逻辑,实际可优化
let next = greetingLib.slice(start, start + 2);
if (next.length < 2) {
next = [...next, ...greetingLib.slice(0, 2 - next.length)];
}
displayedGreetings.value = next;
};
const selectGreeting = (text) => {
blessingText.value = text;
};
const tools = [
{ type: "template", text: "模板", icon: "▦" },
{ type: "text", text: "文字", icon: "文" },
// { type: "image", text: "图片/背景", icon: "图" },
// { type: "avatar", text: "头像挂饰", icon: "饰" },
];
const activeTool = ref("template");
const templates = ref([]);
const currentTemplate = ref(templates.value[0]);
const applyTemplate = (tpl) => {
currentTemplate.value = tpl;
};
const pickImage = () => {
uni.chooseImage({
count: 1,
success: (res) => {
const path = res.tempFilePaths?.[0];
if (path)
currentTemplate.value = { ...currentTemplate.value, cover: path };
},
});
};
const resetBackground = () => {
currentTemplate.value = templates.value[0];
};
const toggleAvatarDecor = () => {
uni.showToast({ title: "挂饰功能即将上线~", icon: "none" });
};
const preview = () => {
saveByCanvas();
// uni.showToast({ title: '已保存到相册', icon: 'checkmarkempty' })
};
const shareOrSave = async () => {
// const tempPath = await saveByCanvas(false);
// const fileKeyRes = await uni.uploadFile({
// url: "https://api.ai-meng.com/api/common/upload",
// filePath: tempPath,
// name: "file", // 和后端接收文件字段名一致
// header: {
// "x-app-id": "69665538a49b8ae3be50fe5d",
// },
// });
// if (fileKeyRes.statusCode < 400) {
// const keyJson = JSON.parse(fileKeyRes.data);
// const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
// // const url =
// // "https://file.lihailezzc.com/resource/99c9f7e0086ed66d20bd1675b4ab22e9.png";
// updateCard({
// id: cardId.value,
// imageUrl: url,
// status: 1,
// });
// }
// createCard();
// uni.showToast({ title: '已保存到相册并可分享', icon: 'none' })
};
const showMore = () => {
uni.showToast({ title: "更多模板即将上线~", icon: "none" });
};
const saveByCanvas = async (save = true) => {
const ctx = uni.createCanvasContext("cardCanvas");
// 画布尺寸rpx 转 px
const W = 540;
const H = 960;
// 1⃣ 画背景
// ⭐ 先加载背景图
const [bgPath, avatarPath] = await Promise.all([
loadImage(currentTemplate?.value?.imageUrl),
loadImage(userAvatar.value),
]);
ctx.drawImage(bgPath, 0, 0, W, H);
// 2⃣ 半透明遮罩(和你 UI 一致)
ctx.setFillStyle("rgba(0,0,0,0.08)");
ctx.fillRect(0, 0, W, H);
// 3⃣ 标题
ctx.setFillStyle("#ffffff");
ctx.setFontSize(42);
ctx.setTextAlign("center");
ctx.fillText("新春快乐", W / 2, 120);
ctx.setFontSize(22);
ctx.setGlobalAlpha(0.9);
ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
ctx.setGlobalAlpha(1);
// 4⃣ 祝福语气泡
drawBubbleText(ctx, {
text: targetName.value + "\n " + blessingText.value,
x: 70,
y: 260,
maxWidth: 400,
fontSize: 32,
lineHeight: 46,
backgroundColor: "rgba(255,255,255,0.85)",
textColor: selectedColor.value,
});
drawUserBubble(ctx, {
x: 40,
y: H - 120,
avatarPath: avatarPath,
username: signatureName.value,
desc: "送上祝福",
});
// 6⃣ 输出
const tempPath = await new Promise((resolve, reject) => {
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "cardCanvas",
success: (res) => {
if (save) saveImage(res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (err) => reject(err),
});
});
});
return tempPath;
};
const loadImage = (url) => {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: url,
success: (res) => {
resolve(res.path); // 本地路径
},
fail: (err) => {
reject(err);
},
});
});
};
const saveImage = (path) => {
uni.saveImageToPhotosAlbum({
filePath: path,
success() {
uni.showToast({ title: "已保存到相册" });
},
fail() {
uni.showModal({
title: "提示",
content: "请授权保存到相册",
});
},
});
};
function drawBubbleText(ctx, options) {
const {
text,
x,
y,
maxWidth = 400,
padding = 24,
lineHeight = 42,
radius = 24,
backgroundColor = "rgba(255,255,255,0.9)",
textColor = "#fff",
fontSize = 32,
fontFamily = "PingFang SC",
} = options;
if (!text) return;
ctx.setFontSize(fontSize);
ctx.setFillStyle(textColor);
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.textAlign = "left";
ctx.textBaseline = "top";
// 1⃣ 文本自动换行
const paragraphs = text.split("\n");
const lines = [];
/** ② 每一段再自动换行 */
paragraphs.forEach((p) => {
let line = "";
for (let char of p) {
const testLine = line + char;
const { width } = ctx.measureText(testLine);
if (width > maxWidth) {
lines.push(line);
line = char;
} else {
line = testLine;
}
}
if (line) lines.push(line);
});
// 2⃣ 计算气泡尺寸
// const bubbleWidth = maxWidth
// const bubbleHeight = lines.length * lineHeight + padding * 2
// 3⃣ 绘制气泡(圆角矩形)
// drawRoundRect(
// ctx,
// x,
// y,
// bubbleWidth,
// bubbleHeight,
// radius,
// backgroundColor
// )
// 4⃣ 绘制文字
ctx.setFillStyle(textColor);
lines.forEach((line, index) => {
ctx.fillText(line, x + padding, y + padding + index * lineHeight);
});
}
function drawUserBubble(ctx, options) {
const {
x = 40, // 气泡起点 x
y = 860, // 气泡起点 y
avatarPath, // 头像本地路径
username = "zzc",
desc = "送上祝福",
avatarSize = 64, // 头像直径
padding = 16, // 气泡内边距
fontSizeName = 24,
fontSizeDesc = 20,
bubbleColor = "rgba(255,255,255,0.18)",
textColor = "#ffffff",
} = options;
// 设置字体
ctx.textBaseline = "top";
ctx.font = `${fontSizeName}px PingFang SC`;
// 测量文字宽度
const nameWidth = ctx.measureText(username).width;
ctx.font = `${fontSizeDesc}px PingFang SC`;
const descWidth = ctx.measureText(desc).width;
// 计算气泡宽度和高度
const textWidth = Math.max(nameWidth, descWidth);
const bubbleHeight =
Math.max(avatarSize, fontSizeName + fontSizeDesc + 4) + padding * 2;
const bubbleWidth = avatarSize + padding + textWidth + padding * 2;
// 1⃣ 绘制气泡(左右半圆)
drawRoundRect(
ctx,
x,
y,
bubbleWidth,
bubbleHeight,
bubbleHeight / 2,
bubbleColor,
);
// 2⃣ 绘制头像
const avatarX = x + padding;
const avatarY = y + (bubbleHeight - avatarSize) / 2;
ctx.save();
ctx.beginPath();
ctx.arc(
avatarX + avatarSize / 2,
avatarY + avatarSize / 2,
avatarSize / 2,
0,
Math.PI * 2,
);
ctx.clip();
ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize);
ctx.restore();
// 3⃣ 绘制文字
const textX = avatarX + avatarSize + padding;
const textY = y + padding;
ctx.setFillStyle(textColor);
ctx.font = `${fontSizeName}px PingFang SC`;
ctx.fillText(username, textX, textY);
ctx.font = `${fontSizeDesc}px PingFang SC`;
ctx.setGlobalAlpha(0.6);
ctx.fillText(desc, textX, textY + fontSizeName + 4);
ctx.setGlobalAlpha(1);
}
function drawRoundRect(ctx, x, y, w, h, r, color) {
ctx.beginPath();
// ctx.setFillStyle(color)
ctx.fillStyle = "rgba(255,255,255,0.18)";
ctx.fill();
// 描边(非常关键)
ctx.strokeStyle = "rgba(255,255,255,0.35)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
ctx.fill();
}
</script>
<style lang="scss" scoped>
.make-page {
min-height: 100vh;
background: #fff;
box-sizing: border-box;
}
/* 卡片预览 */
.card-preview {
margin: 24rpx auto;
height: 960rpx;
width: 540rpx;
border-radius: 30rpx;
overflow: hidden;
position: relative;
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
}
.card-bg {
width: 100%;
height: 100%;
}
.card-overlay {
position: absolute;
inset: 0;
padding: 30rpx;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
}
.card-overlay .title {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 60rpx;
}
.title .main {
font-size: 42rpx;
font-weight: 700;
}
.title .sub {
margin-top: 8rpx;
font-size: 22rpx;
opacity: 0.9;
}
.bubble {
margin-top: 80rpx;
padding: 40rpx;
max-width: 500rpx;
}
.bubble-text {
font-size: 26rpx;
line-height: 1.6;
white-space: pre-wrap;
text-align: left;
}
.user {
position: absolute;
bottom: 40rpx;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.18);
border: 1rpx solid rgba(255, 255, 255, 0.35);
border-radius: 9999rpx;
padding: 15rpx;
padding-left: 20rpx;
padding-right: 20rpx;
}
.avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-right: 14rpx;
border: 2rpx solid rgba(255, 255, 255, 0.6);
}
.user .user-info {
display: flex;
flex-direction: column;
}
.user-name {
font-size: 24rpx;
font-weight: 600;
}
.user-desc {
font-size: 20rpx;
opacity: 0.6;
}
/* 顶部提示 */
.tip-line {
text-align: center;
color: #999;
font-size: 22rpx;
}
/* 编辑工具区 */
.editor-panel {
margin: 20rpx 24rpx 40rpx;
border-radius: 30rpx 30rpx 0 0;
background: #fff;
box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.06);
padding-bottom: env(safe-area-inset-bottom);
}
.drag-handle {
width: 120rpx;
height: 8rpx;
border-radius: 999rpx;
background: #eee;
margin: 12rpx auto;
}
/* 工具入口 */
.tools {
display: grid;
grid-template-columns: repeat(4, 1fr);
padding: 16rpx 24rpx 8rpx;
}
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 14rpx 0;
border-radius: 18rpx;
color: #666;
}
.tool-item.active {
background: #fff6f5;
color: #ff3b30;
}
.tool-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
background: #fafafa;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
.tool-text {
font-size: 22rpx;
}
/* 模板区 */
.section {
padding: 12rpx 24rpx 0;
}
.section-title {
display: flex;
align-items: center;
}
.section-title .more {
margin-left: auto;
color: #ff3b30;
font-size: 24rpx;
}
.tpl-scroll {
margin-top: 12rpx;
}
.tpl-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
padding-bottom: 20rpx;
}
.tpl-card {
width: 100%; /* 自适应 grid 宽度 */
border-radius: 12rpx;
overflow: hidden;
background: #fff;
position: relative;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.tpl-card.selected {
outline: 4rpx solid #ff3b30;
}
.tpl-cover {
width: 100%;
height: 368rpx;
}
.tpl-name {
font-size: 20rpx;
color: #333;
padding: 6rpx 8rpx;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tpl-check {
position: absolute;
right: 6rpx;
top: 6rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #ff3b30;
color: #fff;
font-size: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.loading-more,
.no-more {
text-align: center;
font-size: 22rpx;
color: #999;
padding: 10rpx 0;
}
/* 文字编辑区 */
.text-edit-section {
padding: 10rpx 24rpx 0;
}
.form-item {
margin-bottom: 32rpx;
}
.label {
font-size: 24rpx;
color: #333;
font-weight: 600;
margin-bottom: 16rpx;
display: block;
}
.input-box {
background: #f9f9f9;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
}
.label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.refresh-btn {
font-size: 22rpx;
color: #ff3b30;
display: flex;
align-items: center;
}
.refresh-icon {
margin-right: 6rpx;
font-size: 24rpx;
}
.greeting-scroll {
width: 100%;
}
.greeting-list {
display: flex;
padding-bottom: 10rpx;
}
.greeting-card {
flex-shrink: 0;
width: 320rpx;
height: 160rpx;
background: #fff;
border: 2rpx solid #eee;
border-radius: 16rpx;
padding: 20rpx;
margin-right: 20rpx;
position: relative;
box-sizing: border-box;
}
.greeting-card.active {
border-color: #ff3b30;
background: #fff6f5;
}
.greeting-text {
font-size: 24rpx;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
}
.greeting-card.active .greeting-text {
color: #ff3b30;
}
.check-mark {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 32rpx;
height: 32rpx;
background: #ff3b30;
border-radius: 50%;
color: #fff;
font-size: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.input-wrapper {
position: relative;
}
.edit-icon {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
color: #999;
}
.color-list {
display: flex;
gap: 24rpx;
}
.color-item {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
position: relative;
border: 2rpx solid rgba(0, 0, 0, 0.05);
}
.color-check {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 32rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
/* 按钮区 */
.bottom-actions {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 24rpx 30rpx;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
border-radius: 999rpx;
padding: 0 40rpx;
font-size: 28rpx;
}
.btn view {
margin-left: 10rpx;
}
.btn.secondary {
background: #fff;
color: #000;
border: 1px solid #ccc;
}
.btn.primary {
background: #ff3b30;
color: #fff;
box-shadow: 0 12rpx 24rpx rgba(255, 59, 48, 0.35);
}
.hidden-canvas {
position: fixed;
left: -9999px;
top: -9999px;
}
</style>