@@ -18,21 +18,35 @@
class = "decor-img"
:src = "selectedDecor"
mode = "aspectFit"
:style = "decorStyle"
@touchstart.stop ="onTouchStart"
@touchmove.stop ="onTouchMove"
@touchend.stop ="onTouchEnd"
/ >
< / view >
< text class = "preview-tip" > 实时预览效果 < / text >
< view v-if = "selectedDecor" class="interaction-tip" >
< text > 👆 单指拖动 < / text >
< text class = "tip-divider" > | < / text >
< text > ✌ ️ 双指缩放 / 旋转 < / text >
< / view >
< text v-else class = "preview-tip" > 实时预览效果 < / text >
< / view >
< view class = "quick-acti ons" >
< button class = "btn w echat " @tap ="useWeChatAvatar" > 使用微信头像 < / button >
< view class = "action-butt ons" >
< button class = "btn s econdary " @tap ="saveAndUse" > 保存 < / button >
< button class = "btn primary" open -type = " share " > 分享给朋友 < / button >
< / view >
< view class = "section" >
< view class = "section-header" >
< text class = "section-title" > 系统推荐头像 < / text >
< text class = "section-title" > 选择底图 < / text >
< / view >
< scroll-view scroll -x class = "avatar-scroll" show -scrollbar = " false " >
< view class = "avatar-list" >
< view class = "avatar-card upload-card" @tap ="useWeChatAvatar" >
< view class = "upload-icon" > 📷 < / view >
< text class = "upload-text" > 微信头像 < / text >
< / view >
< view
v-for = "(item, i) in systemAvatars"
:key = "i"
@@ -68,7 +82,7 @@
:key = "i"
class = "grid-item"
: class = "{ active: selectedFrame === frame }"
@tap ="selected Frame = frame"
@tap ="toggle Frame( frame) "
>
< image :src = "frame" class = "grid-img" mode = "aspectFill" / >
< view v-if = "selectedFrame === frame" class="check" > ✔ < / view >
@@ -88,11 +102,6 @@
< / 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"
@@ -106,8 +115,15 @@
< script setup >
import { ref , computed } from "vue" ;
import { getBavBarHeight } from "@/utils/system ";
import { onShareAppMessage } from "@dcloudio/uni-app ";
import { getBavBarHeight , getDeviceInfo } from "@/utils/system" ;
import { useUserStore } from "@/stores/user" ;
import {
createShareToken ,
getShareReward ,
abilityCheck ,
} from "@/api/system.js" ;
import { avatarDownloadRecord } from "@/api/avatar.js" ;
const userStore = useUserStore ( ) ;
const loginPopupRef = ref ( null ) ;
@@ -135,6 +151,89 @@ const selectedFrame = ref("");
const selectedDecor = ref ( "" ) ;
const activeTab = ref ( "frame" ) ;
const toggleFrame = ( frame ) => {
if ( selectedFrame . value === frame ) {
selectedFrame . value = "" ;
} else {
selectedFrame . value = frame ;
}
} ;
// 挂饰状态
const decorState = ref ( {
x : 300 , // 初始中心 X (rpx)
y : 80 , // 初始中心 Y (rpx)
scale : 1 ,
rotate : 0 ,
} ) ;
const decorStyle = computed ( ( ) => {
return {
transform : ` translate( ${ decorState . value . x - 120 } rpx, ${
decorState . value . y - 120
} rpx) rotate( ${ decorState . value . rotate } deg) scale( ${
decorState . value . scale
} ) ` ,
} ;
} ) ;
// 触摸状态
let startTouches = [ ] ;
let initialDecorState = { } ;
const getDistance = ( p1 , p2 ) => {
const x = p1 . clientX - p2 . clientX ;
const y = p1 . clientY - p2 . clientY ;
return Math . sqrt ( x * x + y * y ) ;
} ;
const getAngle = ( p1 , p2 ) => {
const x = p1 . clientX - p2 . clientX ;
const y = p1 . clientY - p2 . clientY ;
return ( Math . atan2 ( y , x ) * 180 ) / Math . PI ;
} ;
const onTouchStart = ( e ) => {
startTouches = e . touches ;
initialDecorState = { ... decorState . value } ;
} ;
const onTouchMove = ( e ) => {
if ( e . touches . length === 1 && startTouches . length === 1 ) {
// 单指移动
const dx = e . touches [ 0 ] . clientX - startTouches [ 0 ] . clientX ;
const dy = e . touches [ 0 ] . clientY - startTouches [ 0 ] . clientY ;
// px 转 rpx
const systemInfo = uni . getSystemInfoSync ( ) ;
const ratio = 750 / systemInfo . windowWidth ;
decorState . value . x = initialDecorState . x + dx * ratio ;
decorState . value . y = initialDecorState . y + dy * ratio ;
} else if ( e . touches . length === 2 && startTouches . length === 2 ) {
// 双指缩放/旋转
const p1 = e . touches [ 0 ] ;
const p2 = e . touches [ 1 ] ;
const startP1 = startTouches [ 0 ] ;
const startP2 = startTouches [ 1 ] ;
const currentDist = getDistance ( p1 , p2 ) ;
const startDist = getDistance ( startP1 , startP2 ) ;
const currentAngle = getAngle ( p1 , p2 ) ;
const startAngle = getAngle ( startP1 , startP2 ) ;
decorState . value . scale =
initialDecorState . scale * ( currentDist / startDist ) ;
decorState . value . rotate =
initialDecorState . rotate + ( currentAngle - startAngle ) ;
}
} ;
const onTouchEnd = ( ) => {
// 可以在这里做边界检查等
} ;
const handleLogind = async ( ) => {
// Logic after successful login if needed
} ;
@@ -152,6 +251,28 @@ const goBack = () => {
} ;
const saveAndUse = async ( ) => {
const abilityRes = await abilityCheck ( "avatar_download" ) ;
if ( ! abilityRes . canUse ) {
if (
abilityRes ? . blockType === "need_share" &&
abilityRes ? . message === "分享可继续"
) {
uni . showToast ( {
title : "分享到群聊可继续使用" ,
icon : "none" ,
} ) ;
return ;
}
uni . showToast ( {
title : "您今日头像下载次数已用完,明日再试" ,
icon : "none" ,
} ) ;
return ;
}
// 调用avatarDownloadRecord API记录下载次数
await avatarDownloadRecord ( {
avatarUrl : currentAvatar . value ,
} ) ;
const ctx = uni . createCanvasContext ( "avatarCanvas" ) ;
const size = 600 ;
const avatarPath = await loadImage ( currentAvatar . value ) ;
@@ -163,7 +284,21 @@ const saveAndUse = async () => {
}
if ( selectedDecor . value ) {
const decorPath = await loadImage ( selectedDecor . value ) ;
ctx . drawImage ( decorPath , 0 , 0 , size , size ) ;
ctx . save ( ) ;
// 映射 rpx 坐标到 Canvas 坐标 (假设 1rpx = 1 unit for 600x600 canvas logic)
// Canvas size is 600, Preview is 600rpx. Ratio is 1:1 in logical space.
ctx . translate ( decorState . value . x , decorState . value . y ) ;
ctx . rotate ( ( decorState . value . rotate * Math . PI ) / 180 ) ;
const scale = decorState . value . scale ;
// 绘制图片,宽高 240
ctx . drawImage (
decorPath ,
- 120 * scale ,
- 120 * scale ,
240 * scale ,
240 * scale ,
) ;
ctx . restore ( ) ;
}
ctx . draw ( false , ( ) => {
uni . canvasToTempFilePath ( {
@@ -172,7 +307,7 @@ const saveAndUse = async () => {
uni . saveImageToPhotosAlbum ( {
filePath : res . tempFilePath ,
success : ( ) => {
uni . showToast ( { title : "已保存并设置 " , icon : "success" } ) ;
uni . showToast ( { title : "已保存到相册 " , icon : "success" } ) ;
} ,
} ) ;
} ,
@@ -184,6 +319,39 @@ const share = () => {
uni . showToast ( { title : "已生成,可在相册分享" , icon : "none" } ) ;
} ;
onShareAppMessage ( async ( ) => {
const deviceInfo = getDeviceInfo ( ) ;
const shareTokenRes = await createShareToken ( {
targetId : "" ,
scene : "avatar_download" ,
... deviceInfo ,
} ) ;
getRewardByShare ( ) ;
return {
title : "制作我的新春头像" ,
path : ` /pages/avatar/index?shareToken= ${ shareTokenRes . shareToken } ` ,
imageUrl :
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png" , // 使用默认封面或 popularCards 的封面
} ;
} ) ;
const getRewardByShare = async ( ) => {
const res = await getShareReward ( { scene : "avatar_download" } ) ;
if ( res . success ) {
uni . showToast ( { title : "分享成功,可下载头像" } ) ;
checkDrawStatus ( ) ;
}
} ;
// onShareTimeline(() => {
// return {
// title: "制作我的新春头像",
// imageUrl:
// "https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
// };
// });
const loadImage = ( url ) => {
return new Promise ( ( resolve , reject ) => {
uni . getImageInfo ( {
@@ -245,11 +413,11 @@ const loadImage = (url) => {
}
. decor - img {
position : absolute ;
top : - 40 rpx ;
left : 50 % ;
transform : translateX ( - 50 % ) ;
top : 0 ;
left : 0 ;
width : 240 rpx ;
height : 240 rpx ;
/* 移除原有的 transform 和 center positioning, 改为由 inline style 控制 */
}
. preview - tip {
display : block ;
@@ -258,21 +426,49 @@ const loadImage = (url) => {
font - size : 22 rpx ;
margin - top : 12 rpx ;
}
. quick - actions {
. interaction - tip {
display : flex ;
gap : 16 rpx ;
padding : 0 24 rpx ;
margin - to p: 8 rpx ;
}
. btn {
height : 80 rpx ;
align - items : center ;
justify - content : center ;
ga p: 12 rpx ;
margin - top : 20 rpx ;
font - size : 24 rpx ;
color : # ff3b30 ;
background : # fff0f0 ;
padding : 12 rpx 32 rpx ;
border - radius : 999 rpx ;
padding : 0 32 rpx ;
font - size : 28 rpx ;
width : fit - content ;
margin - left : auto ;
margin - right : auto ;
}
. btn . wechat {
background : # ff3b30 ;
color : # fff ;
. tip - divider {
color : # ffccc7 ;
margin : 0 4 rpx ;
}
. action - buttons {
display : flex ;
padding : 0 48 rpx ;
gap : 32 rpx ;
margin - top : 32 rpx ;
}
. btn {
flex : 1 ;
height : 88 rpx ;
border - radius : 999 rpx ;
display : flex ;
align - items : center ;
justify - content : center ;
font - size : 30 rpx ;
font - weight : 600 ;
margin : 0 ;
padding : 0 ;
border : none ;
line - height : normal ;
}
. btn : : after {
border : none ;
}
. section {
@@ -307,6 +503,26 @@ const loadImage = (url) => {
. avatar - card . active {
outline : 4 rpx solid # ff3b30 ;
}
. upload - card {
display : flex ;
flex - direction : column ;
align - items : center ;
justify - content : center ;
background : # fff5f5 ;
border : 2 rpx dashed # ffccc7 ;
box - sizing : border - box ;
}
. upload - icon {
font - size : 48 rpx ;
margin - bottom : 8 rpx ;
}
. upload - text {
font - size : 22 rpx ;
color : # ff3b30 ;
font - weight : 500 ;
}
. avatar - thumb {
width : 100 % ;
height : 100 % ;
@@ -368,12 +584,6 @@ const loadImage = (url) => {
height : 100 % ;
}
. bottom - actions {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 20 rpx 24 rpx 40 rpx ;
}
. btn . primary {
background : # ff3b30 ;
color : # fff ;
@@ -383,7 +593,6 @@ const loadImage = (url) => {
background : # f5f5f5 ;
color : # 333 ;
}
. hidden - canvas {
position : fixed ;
left : - 9999 px ;