Files
spring-festival-greetings/pages/avatar/index.vue
2026-01-15 10:27:14 +08:00

379 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="avatar-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
<view class="nav-bar">
<view class="back" @tap="goBack"></view>
<text class="nav-title-left">新春头像挂饰</text>
</view>
<view class="preview-card">
<view class="preview-square">
<image class="avatar-img" :src="currentAvatar" mode="aspectFill" />
<image
v-if="selectedFrame"
class="frame-img"
:src="selectedFrame"
mode="aspectFill"
/>
<image
v-if="selectedDecor"
class="decor-img"
:src="selectedDecor"
mode="aspectFit"
/>
</view>
<text class="preview-tip">实时预览效果</text>
</view>
<view class="quick-actions">
<button class="btn wechat" @tap="useWeChatAvatar">使用微信头像</button>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">系统推荐头像</text>
</view>
<scroll-view scroll-x class="avatar-scroll" show-scrollbar="false">
<view class="avatar-list">
<view
v-for="(item, i) in systemAvatars"
:key="i"
class="avatar-card"
:class="{ active: currentAvatar === item }"
@tap="currentAvatar = item"
>
<image :src="item" class="avatar-thumb" mode="aspectFill" />
<view v-if="currentAvatar === item" class="check"></view>
</view>
</view>
</scroll-view>
</view>
<view class="tabs">
<view
class="tab"
:class="{ active: activeTab === 'frame' }"
@tap="activeTab = 'frame'"
>头像框</view
>
<view
class="tab"
:class="{ active: activeTab === 'decor' }"
@tap="activeTab = 'decor'"
>挂饰配件</view
>
</view>
<view v-if="activeTab === 'frame'" class="grid">
<view
v-for="(frame, i) in frames"
:key="i"
class="grid-item"
:class="{ active: selectedFrame === frame }"
@tap="selectedFrame = frame"
>
<image :src="frame" class="grid-img" mode="aspectFill" />
<view v-if="selectedFrame === frame" class="check"></view>
</view>
</view>
<view v-else class="grid">
<view
v-for="(decor, i) in decors"
:key="i"
class="grid-item"
:class="{ active: selectedDecor === decor }"
@tap="selectedDecor = decor"
>
<image :src="decor" class="grid-img" mode="aspectFit" />
<view v-if="selectedDecor === decor" class="check"></view>
</view>
</view>
<view class="bottom-actions">
<button class="btn primary" @tap="saveAndUse">保存并使用</button>
<button class="btn secondary" @tap="share">分享</button>
</view>
<canvas
canvas-id="avatarCanvas"
class="hidden-canvas"
style="width: 600px; height: 600px"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import { getBavBarHeight } from "@/utils/system";
const systemAvatars = [
"https://file.lihailezzc.com/resource/1463f294244c11cf274a5eaae115872a.jpeg",
"https://file.lihailezzc.com/20260109082842_666_1.jpg",
"https://file.lihailezzc.com/20260108222141_644_1.jpg",
"https://file.lihailezzc.com/9a929a32-439f-453b-b603-fda7b04cbe08.png",
];
const frames = [
"https://file.lihailezzc.com/resource/0f8e6a78c1b04a4fb3a6fa1c66f1f7b3.png",
"https://file.lihailezzc.com/resource/4a4f0c9e6e1b4c0b9f6f7a1b2c3d4e5f.png",
"https://file.lihailezzc.com/resource/aa77f0b45b2e4a0f91c4fda74f3f1123.png",
"https://file.lihailezzc.com/resource/bb88f0b45b2e4a0f91c4fda74f3f1199.png",
];
const decors = [
"https://file.lihailezzc.com/resource/horse_decor_01.png",
"https://file.lihailezzc.com/resource/horse_decor_02.png",
"https://file.lihailezzc.com/resource/horse_decor_03.png",
"https://file.lihailezzc.com/resource/horse_decor_04.png",
];
const currentAvatar = ref(systemAvatars[0]);
const selectedFrame = ref("");
const selectedDecor = ref("");
const activeTab = ref("frame");
const useWeChatAvatar = () => {
uni.showToast({ title: "已应用微信头像", icon: "none" });
};
const goBack = () => {
uni.navigateBack();
};
const saveAndUse = async () => {
const ctx = uni.createCanvasContext("avatarCanvas");
const size = 600;
const avatarPath = await loadImage(currentAvatar.value);
ctx.clearRect(0, 0, size, size);
ctx.drawImage(avatarPath, 0, 0, size, size);
if (selectedFrame.value) {
const framePath = await loadImage(selectedFrame.value);
ctx.drawImage(framePath, 0, 0, size, size);
}
if (selectedDecor.value) {
const decorPath = await loadImage(selectedDecor.value);
ctx.drawImage(decorPath, 0, 0, size, size);
}
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "avatarCanvas",
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({ title: "已保存并设置", icon: "success" });
},
});
},
});
});
};
const share = () => {
uni.showToast({ title: "已生成,可在相册分享", icon: "none" });
};
const loadImage = (url) => {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: url,
success: (res) => resolve(res.path),
fail: reject,
});
});
};
</script>
<style lang="scss" scoped>
.avatar-page {
min-height: 100vh;
background: #fff;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
}
.nav-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
}
.back {
font-size: 40rpx;
margin-right: 12rpx;
}
.nav-title-left {
font-size: 32rpx;
font-weight: 600;
}
.preview-card {
margin: 20rpx 24rpx;
background: #fff;
border-radius: 24rpx;
padding: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.06);
}
.preview-square {
width: 600rpx;
height: 600rpx;
margin: 0 auto;
border-radius: 24rpx;
background: #f5dfc9;
position: relative;
overflow: hidden;
}
.avatar-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.frame-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.decor-img {
position: absolute;
top: -40rpx;
left: 50%;
transform: translateX(-50%);
width: 240rpx;
height: 240rpx;
}
.preview-tip {
display: block;
text-align: center;
color: #999;
font-size: 22rpx;
margin-top: 12rpx;
}
.quick-actions {
display: flex;
gap: 16rpx;
padding: 0 24rpx;
margin-top: 8rpx;
}
.btn {
height: 80rpx;
border-radius: 999rpx;
padding: 0 32rpx;
font-size: 28rpx;
}
.btn.wechat {
background: #ff3b30;
color: #fff;
}
.section {
margin-top: 24rpx;
padding: 0 24rpx;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.section-title {
font-size: 26rpx;
font-weight: 600;
}
.avatar-scroll {
width: 100%;
}
.avatar-list {
display: flex;
}
.avatar-card {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
margin-right: 16rpx;
position: relative;
background: #fff;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
}
.avatar-card.active {
outline: 4rpx solid #ff3b30;
}
.avatar-thumb {
width: 100%;
height: 100%;
}
.check {
position: absolute;
right: 8rpx;
top: 8rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #ff3b30;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
}
.tabs {
display: flex;
padding: 16rpx 24rpx 8rpx;
gap: 12rpx;
}
.tab {
flex: 1;
text-align: center;
background: #f7f7f7;
border-radius: 999rpx;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
color: #666;
}
.tab.active {
background: #fff0f0;
color: #ff3b30;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
padding: 8rpx 24rpx 24rpx;
}
.grid-item {
height: 200rpx;
border-radius: 16rpx;
background: #fff;
overflow: hidden;
position: relative;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
}
.grid-item.active {
outline: 4rpx solid #ff3b30;
}
.grid-img {
width: 100%;
height: 100%;
}
.bottom-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx 40rpx;
}
.btn.primary {
background: #ff3b30;
color: #fff;
box-shadow: 0 12rpx 24rpx rgba(255, 59, 48, 0.35);
}
.btn.secondary {
background: #f5f5f5;
color: #333;
}
.hidden-canvas {
position: fixed;
left: -9999px;
top: -9999px;
}
</style>