Compare commits
18 Commits
633bc1c814
...
6c1084ef32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1084ef32 | ||
|
|
818e947513 | ||
|
|
19c18b478f | ||
|
|
e5a8f3ca3f | ||
|
|
90e7f000c8 | ||
|
|
59fa05c341 | ||
|
|
39f8b88715 | ||
|
|
aa09652069 | ||
|
|
74679b0407 | ||
|
|
806878fa54 | ||
|
|
5e49b247db | ||
|
|
28f0f83531 | ||
|
|
66e483c315 | ||
|
|
5e8a3db6d9 | ||
|
|
1d84e34cb3 | ||
|
|
5944f8d011 | ||
|
|
6c23726e09 | ||
|
|
b3165a56cf |
45
App.vue
45
App.vue
@@ -1,21 +1,40 @@
|
||||
<script>
|
||||
import { useUserStore } from './stores/user'
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
const userStore = useUserStore()
|
||||
userStore.loadFromStorage()
|
||||
}
|
||||
}
|
||||
|
||||
<script>
|
||||
import { useUserStore } from "./stores/user";
|
||||
import { userOpenApp } from "./api/auth";
|
||||
|
||||
const openApp = async () => {
|
||||
try {
|
||||
const res = await userOpenApp();
|
||||
if (res?.points && res.points > 0) {
|
||||
uni.showToast({
|
||||
title: `每日登录 +${res.points} 积分`,
|
||||
icon: "none",
|
||||
});
|
||||
const userStore = useUserStore();
|
||||
await userStore.fetchUserAssets();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("userOpenApp error", e);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
const userStore = useUserStore();
|
||||
userStore.loadFromStorage();
|
||||
if (userStore.userInfo.id) {
|
||||
openApp();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import url("common/style/common-style.scss");
|
||||
wx-swiper .wx-swiper-dot {
|
||||
position: relative;
|
||||
right: -260rpx;
|
||||
bottom: 110rpx;
|
||||
position: relative;
|
||||
right: -260rpx;
|
||||
bottom: 110rpx;
|
||||
}
|
||||
/* tabBar */
|
||||
.customtabbar {
|
||||
|
||||
14
api/auth.js
14
api/auth.js
@@ -23,6 +23,20 @@ export const updateUserInfo = async (body) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const userOpenApp = async () => {
|
||||
return request({
|
||||
url: "/api/user/open-app",
|
||||
method: "POST",
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserAsset = async () => {
|
||||
return request({
|
||||
url: "/api/user/asset",
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const reportPrivacy = async () => {
|
||||
// return request({
|
||||
// url: "/api/common/privacy/report",
|
||||
|
||||
@@ -7,6 +7,20 @@ export const getAvatarSystemList = async (page = 1) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getAvatarSystemCategoryList = async () => {
|
||||
return request({
|
||||
url: `/api/blessing/avatar/category/list`,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getAvatarSystemListByCategory = async (categoryId, page = 1) => {
|
||||
return request({
|
||||
url: `/api/blessing/avatar/system/list?categoryId=${categoryId}&page=${page}`,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getAvatarDecorList = async (page = 1) => {
|
||||
return request({
|
||||
url: `/api/blessing/avatar/decor/list?page=${page}`,
|
||||
|
||||
@@ -81,3 +81,10 @@ export const createTracking = async (data) => {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const watchAdReward = async () => {
|
||||
return request({
|
||||
url: "/api/blessing/ad/reward",
|
||||
method: "POST",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { getPlatformProvider, isSinglePageMode } from "@/utils/system";
|
||||
import { uploadImage } from "@/utils/common";
|
||||
@@ -112,6 +112,19 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(["logind"]);
|
||||
|
||||
// 监听全局事件
|
||||
const handleGlobalShow = () => {
|
||||
open();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
uni.$on("show-login-popup", handleGlobalShow);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off("show-login-popup", handleGlobalShow);
|
||||
});
|
||||
|
||||
// 是否处于单页模式(朋友圈打开)
|
||||
const isSinglePage = computed(() => isSinglePageMode());
|
||||
|
||||
@@ -197,6 +210,9 @@ const handleAlipayLogin = async () => {
|
||||
id: loginRes?.user?.id,
|
||||
isVip: loginRes?.isVip || false,
|
||||
vipExpireAt: loginRes?.vipExpireAt || null,
|
||||
points: loginRes?.points || 0,
|
||||
exp: loginRes?.exp || 0,
|
||||
level: loginRes?.level || 0,
|
||||
});
|
||||
|
||||
userStore.setToken(loginRes.token);
|
||||
@@ -268,6 +284,9 @@ const confirmLogin = async () => {
|
||||
id: loginRes?.user?.id,
|
||||
isVip: loginRes?.isVip || false,
|
||||
vipExpireAt: loginRes?.vipExpireAt || null,
|
||||
points: loginRes?.points || 0,
|
||||
exp: loginRes?.exp || 0,
|
||||
level: loginRes?.level || 0,
|
||||
});
|
||||
|
||||
userStore.setToken(loginRes.token);
|
||||
|
||||
861
components/LuckyPopup/LuckyPopup.vue
Normal file
861
components/LuckyPopup/LuckyPopup.vue
Normal file
@@ -0,0 +1,861 @@
|
||||
<template>
|
||||
<view class="lucky-popup-container">
|
||||
<uni-popup ref="popup" type="center" :mask-click="false">
|
||||
<!-- 动画阶段 -->
|
||||
<view v-if="isAnimating" class="animation-container">
|
||||
<view class="card-flip-wrapper" :class="{ flipped: isFlipping }">
|
||||
<view class="card-front">
|
||||
<view class="loading-circle">
|
||||
<view class="particle p1"></view>
|
||||
<view class="particle p2"></view>
|
||||
<view class="particle p3"></view>
|
||||
<view class="particle p4"></view>
|
||||
</view>
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
<view class="card-back"></view>
|
||||
</view>
|
||||
<view class="light-effect" v-if="showLight"></view>
|
||||
</view>
|
||||
|
||||
<!-- 结果阶段 -->
|
||||
<view v-else class="result-container">
|
||||
<view class="lucky-card" id="lucky-card">
|
||||
<!-- 头部渐变区 -->
|
||||
<view class="card-header">
|
||||
<view class="header-decor left">福</view>
|
||||
<view class="header-decor right">禧</view>
|
||||
|
||||
<text class="header-label">今日好运指数</text>
|
||||
<view class="score-wrap">
|
||||
<text class="score">{{ resultData.score }}</text>
|
||||
<text class="percent">%</text>
|
||||
</view>
|
||||
<text class="lucky-word">{{ resultData.luckyWord }}</text>
|
||||
|
||||
<view class="tag-year">{{ currentDateStr }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<view class="card-body">
|
||||
<!-- 宜忌 -->
|
||||
<view class="yi-ji-grid">
|
||||
<view class="grid-item yi">
|
||||
<view class="item-title">
|
||||
<uni-icons type="checkmarkempty" size="16" color="#d81e06" />
|
||||
<text>今日宜</text>
|
||||
</view>
|
||||
<text class="item-content">{{ resultData.yi }}</text>
|
||||
</view>
|
||||
<view class="grid-item ji">
|
||||
<view class="item-title">
|
||||
<uni-icons type="closeempty" size="16" color="#666" />
|
||||
<text>今日忌</text>
|
||||
</view>
|
||||
<text class="item-content">{{ resultData.ji }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 幸运元素 -->
|
||||
<view class="lucky-elements">
|
||||
<view class="elements-title">
|
||||
<uni-icons type="star-filled" size="16" color="#ffca28" />
|
||||
<text>幸运元素</text>
|
||||
</view>
|
||||
<view class="elements-row">
|
||||
<view class="el-item">
|
||||
<text class="label">颜色</text>
|
||||
<text class="value color-val">{{
|
||||
resultData.luckyColor
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="el-item">
|
||||
<text class="label">数字</text>
|
||||
<text class="value">{{ resultData.luckyNumber }}</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="el-item">
|
||||
<text class="label">方向</text>
|
||||
<text class="value">{{ resultData.luckyDirection }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="quote-text">“{{ resultData.quote }}”</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<view class="action-btn" @tap="onSaveImage">
|
||||
<uni-icons type="download" size="24" color="#fff" />
|
||||
<text class="btn-label">保存</text>
|
||||
</view>
|
||||
|
||||
<button class="action-btn share-btn" open-type="share">
|
||||
<uni-icons type="paperplane" size="24" color="#fff" />
|
||||
<text class="btn-label">好友</text>
|
||||
</button>
|
||||
|
||||
<view class="action-btn" @tap="onShareMoments">
|
||||
<uni-icons type="camera" size="24" color="#fff" />
|
||||
<text class="btn-label">朋友圈</text>
|
||||
</view>
|
||||
|
||||
<view class="action-btn close-btn" @tap="close">
|
||||
<uni-icons type="closeempty" size="24" color="#fff" />
|
||||
<text class="btn-label">关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
|
||||
<!-- 画布用于生成图片 -->
|
||||
<canvas
|
||||
canvas-id="luckyCanvas"
|
||||
id="luckyCanvas"
|
||||
class="lucky-canvas"
|
||||
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||||
></canvas>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from "vue";
|
||||
import calendar from "@/utils/lunar.js";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const popup = ref(null);
|
||||
const isAnimating = ref(true);
|
||||
const isFlipping = ref(false);
|
||||
const showLight = ref(false);
|
||||
const loadingText = ref("好运加载中...");
|
||||
const currentDateStr = ref("");
|
||||
|
||||
// 画布相关
|
||||
const canvasWidth = ref(600);
|
||||
const canvasHeight = ref(1000);
|
||||
|
||||
const resultData = ref({
|
||||
score: 88,
|
||||
luckyWord: "鸿运当头",
|
||||
yi: "沟通合作、尝试新事物",
|
||||
ji: "熬夜、冲动消费",
|
||||
luckyColor: "如意金",
|
||||
luckyNumber: "6",
|
||||
luckyDirection: "东南",
|
||||
quote: "今天适合向前一步,好运正在回应你的努力。",
|
||||
});
|
||||
|
||||
const texts = ["好运加载中...", "今日能量汇集中 ✨", "正在计算你的幸运指数..."];
|
||||
|
||||
const open = () => {
|
||||
isAnimating.value = true;
|
||||
isFlipping.value = false;
|
||||
showLight.value = false;
|
||||
loadingText.value = texts[0];
|
||||
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = (now.getMonth() + 1).toString().padStart(2, "0");
|
||||
const d = now.getDate().toString().padStart(2, "0");
|
||||
const lunar = calendar.solar2lunar(now);
|
||||
currentDateStr.value = `${y}.${m}.${d} ${lunar.lunarDateStr}`;
|
||||
|
||||
popup.value.open();
|
||||
|
||||
startAnimation();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
popup.value.close();
|
||||
};
|
||||
|
||||
const startAnimation = () => {
|
||||
// 文字轮播
|
||||
let step = 0;
|
||||
const timer = setInterval(() => {
|
||||
step++;
|
||||
if (step < texts.length) {
|
||||
loadingText.value = texts[step];
|
||||
}
|
||||
}, 600);
|
||||
|
||||
// 1.5s 后翻转
|
||||
setTimeout(() => {
|
||||
clearInterval(timer);
|
||||
isFlipping.value = true;
|
||||
showLight.value = true;
|
||||
|
||||
// 动画结束后显示结果
|
||||
setTimeout(() => {
|
||||
isAnimating.value = false;
|
||||
}, 600);
|
||||
}, 1800);
|
||||
};
|
||||
|
||||
const onSaveImage = () => {
|
||||
uni.showLoading({ title: "生成图片中..." });
|
||||
const ctx = uni.createCanvasContext("luckyCanvas", proxy);
|
||||
const W = canvasWidth.value;
|
||||
const H = canvasHeight.value;
|
||||
const cardH = 850; // 卡片主体高度
|
||||
|
||||
// 1. 绘制背景
|
||||
ctx.setFillStyle("#ffffff");
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// 2. 绘制卡片头部渐变
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, 360);
|
||||
grd.addColorStop(0, "#d84315");
|
||||
grd.addColorStop(1, "#ffca28");
|
||||
ctx.setFillStyle(grd);
|
||||
ctx.fillRect(0, 0, W, 360);
|
||||
|
||||
// 3. 头部装饰文字
|
||||
ctx.setFillStyle("rgba(255, 255, 255, 0.6)");
|
||||
ctx.setFontSize(24);
|
||||
ctx.fillText("福", 30, 40);
|
||||
ctx.fillText("禧", W - 50, 40);
|
||||
|
||||
// 4. 头部内容
|
||||
ctx.setTextAlign("center");
|
||||
ctx.setFillStyle("rgba(255, 255, 255, 0.9)");
|
||||
ctx.setFontSize(24);
|
||||
ctx.fillText("今日好运指数", W / 2, 80);
|
||||
|
||||
// 分数
|
||||
ctx.setFillStyle("#ffffff");
|
||||
ctx.setFontSize(120);
|
||||
ctx.font = "bold 120px sans-serif";
|
||||
ctx.fillText(resultData.value.score + "%", W / 2, 200);
|
||||
|
||||
// 幸运词
|
||||
ctx.setFontSize(48);
|
||||
ctx.font = "bold 48px sans-serif";
|
||||
ctx.fillText(resultData.value.luckyWord, W / 2, 280);
|
||||
|
||||
// 日期标签背景
|
||||
const dateStr = currentDateStr.value || "2026 CNY SPECIAL";
|
||||
ctx.setFillStyle("rgba(0, 0, 0, 0.15)");
|
||||
const dateWidth = ctx.measureText(dateStr).width + 40;
|
||||
roundRect(ctx, W / 2 - dateWidth / 2, 310, dateWidth, 34, 17);
|
||||
ctx.fill();
|
||||
|
||||
// 日期文字
|
||||
ctx.setFillStyle("#ffffff");
|
||||
ctx.setFontSize(20);
|
||||
ctx.fillText(dateStr, W / 2, 334);
|
||||
|
||||
// 5. 绘制内容区 (宜/忌)
|
||||
const gridY = 400;
|
||||
const boxW = (W - 64 - 24) / 2; // (600 - padding*2 - gap)/2
|
||||
const gridH = 140; // 增加高度防止内容溢出
|
||||
|
||||
// 宜
|
||||
drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
|
||||
ctx.setTextAlign("left");
|
||||
ctx.setFontSize(24);
|
||||
ctx.setFillStyle("#d81e06");
|
||||
ctx.font = "bold 24px sans-serif";
|
||||
// 图标模拟
|
||||
ctx.fillText("✔ 今日宜", 56, gridY + 44);
|
||||
|
||||
ctx.setFontSize(22);
|
||||
ctx.setFillStyle("#666666");
|
||||
ctx.font = "normal 22px sans-serif";
|
||||
wrapText(ctx, resultData.value.yi, 56, gridY + 80, boxW - 48, 30);
|
||||
|
||||
// 忌
|
||||
drawBox(ctx, 32 + boxW + 24, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
|
||||
ctx.setFontSize(24);
|
||||
ctx.setFillStyle("#666666");
|
||||
ctx.font = "bold 24px sans-serif";
|
||||
ctx.fillText("✖ 今日忌", 32 + boxW + 24 + 24, gridY + 44);
|
||||
|
||||
ctx.setFontSize(22);
|
||||
ctx.font = "normal 22px sans-serif";
|
||||
wrapText(
|
||||
ctx,
|
||||
resultData.value.ji,
|
||||
32 + boxW + 24 + 24,
|
||||
gridY + 80,
|
||||
boxW - 48,
|
||||
30,
|
||||
);
|
||||
|
||||
// 6. 幸运元素
|
||||
const elY = 570; // 下移,避免与上面重叠
|
||||
const elH = 160; // 增加高度
|
||||
drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5");
|
||||
|
||||
// 标题
|
||||
ctx.setFontSize(26);
|
||||
ctx.setFillStyle("#333333");
|
||||
ctx.font = "bold 26px sans-serif";
|
||||
ctx.fillText("★ 幸运元素", 56, elY + 46);
|
||||
|
||||
// 元素内容
|
||||
const contentW = W - 64; // 内容区域总宽度
|
||||
const colW = contentW / 3; // 三等分
|
||||
const startX = 32; // 起始X坐标
|
||||
|
||||
// 调整Y坐标,确保不重叠
|
||||
const labelY = elY + 90;
|
||||
const valY = elY + 126;
|
||||
|
||||
// 颜色 (第一列)
|
||||
ctx.setTextAlign("center");
|
||||
ctx.setFontSize(20);
|
||||
ctx.setFillStyle("#999999");
|
||||
ctx.font = "normal 20px sans-serif";
|
||||
ctx.fillText("颜色", startX + colW * 0.5, labelY);
|
||||
|
||||
ctx.setFontSize(26);
|
||||
ctx.setFillStyle("#d84315");
|
||||
ctx.font = "bold 26px sans-serif";
|
||||
ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY);
|
||||
|
||||
// 数字 (第二列)
|
||||
ctx.setFontSize(20);
|
||||
ctx.setFillStyle("#999999");
|
||||
ctx.font = "normal 20px sans-serif";
|
||||
ctx.fillText("数字", startX + colW * 1.5, labelY);
|
||||
|
||||
ctx.setFontSize(26);
|
||||
ctx.setFillStyle("#333333");
|
||||
ctx.font = "bold 26px sans-serif";
|
||||
ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY);
|
||||
|
||||
// 方向 (第三列)
|
||||
ctx.setFontSize(20);
|
||||
ctx.setFillStyle("#999999");
|
||||
ctx.font = "normal 20px sans-serif";
|
||||
ctx.fillText("方向", startX + colW * 2.5, labelY);
|
||||
|
||||
ctx.setFontSize(26);
|
||||
ctx.setFillStyle("#333333");
|
||||
ctx.font = "bold 26px sans-serif";
|
||||
ctx.fillText(resultData.value.luckyDirection, startX + colW * 2.5, valY);
|
||||
|
||||
// 分隔线 1
|
||||
ctx.setStrokeStyle("#eeeeee");
|
||||
ctx.setLineWidth(2);
|
||||
ctx.beginPath();
|
||||
const lineTop = elY + 70;
|
||||
const lineBottom = elY + 130;
|
||||
ctx.moveTo(startX + colW, lineTop);
|
||||
ctx.lineTo(startX + colW, lineBottom);
|
||||
ctx.stroke();
|
||||
|
||||
// 分隔线 2
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX + colW * 2, lineTop);
|
||||
ctx.lineTo(startX + colW * 2, lineBottom);
|
||||
ctx.stroke();
|
||||
|
||||
// 7. 语录
|
||||
ctx.setTextAlign("center");
|
||||
ctx.setFontSize(22);
|
||||
ctx.setFillStyle("#999999");
|
||||
ctx.font = "italic 22px sans-serif";
|
||||
// 语录换行处理,下移坐标
|
||||
wrapTextCentered(ctx, `“${resultData.value.quote}”`, W / 2, 780, W - 80, 30);
|
||||
|
||||
// 8. 底部区域 (Footer)
|
||||
const footerY = 850;
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle("#f0f0f0");
|
||||
ctx.setLineWidth(1);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(40, footerY);
|
||||
ctx.lineTo(W - 40, footerY);
|
||||
ctx.stroke();
|
||||
|
||||
// 底部左侧文字
|
||||
ctx.setTextAlign("left");
|
||||
ctx.setFontSize(32);
|
||||
ctx.setFillStyle("#333333");
|
||||
ctx.font = "bold 32px sans-serif";
|
||||
ctx.fillText("扫码开启今日好运", 40, footerY + 60);
|
||||
|
||||
ctx.setFontSize(20);
|
||||
ctx.setFillStyle("#999999");
|
||||
ctx.font = "normal 20px sans-serif";
|
||||
ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100);
|
||||
|
||||
// 底部右侧二维码 (占位图)
|
||||
// 假设二维码在 static/icon/yunshi.png 或 logo.png
|
||||
// 实际开发中应替换为小程序码
|
||||
ctx.drawImage("/static/logo.png", W - 140, footerY + 25, 100, 100);
|
||||
|
||||
// 绘制
|
||||
ctx.draw(false, () => {
|
||||
setTimeout(() => {
|
||||
uni.canvasToTempFilePath(
|
||||
{
|
||||
canvasId: "luckyCanvas",
|
||||
success: (res) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "已保存到相册", icon: "success" });
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "保存失败,请授权", icon: "none" });
|
||||
},
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "生成图片失败", icon: "none" });
|
||||
console.error(err);
|
||||
},
|
||||
},
|
||||
proxy,
|
||||
);
|
||||
}, 200);
|
||||
});
|
||||
};
|
||||
|
||||
// 辅助函数:绘制圆角矩形
|
||||
function roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + w - r, y);
|
||||
ctx.arc(x + w - r, y + r, r, 1.5 * Math.PI, 2 * Math.PI);
|
||||
ctx.lineTo(x + w, y + h - r);
|
||||
ctx.arc(x + w - r, y + h - r, r, 0, 0.5 * Math.PI);
|
||||
ctx.lineTo(x + r, y + h);
|
||||
ctx.arc(x + r, y + h - r, r, 0.5 * Math.PI, Math.PI);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.arc(x + r, y + r, r, Math.PI, 1.5 * Math.PI);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// 辅助函数:绘制带背景边框的盒子
|
||||
function drawBox(ctx, x, y, w, h, bg, border) {
|
||||
ctx.setFillStyle(bg);
|
||||
ctx.setStrokeStyle(border);
|
||||
ctx.setLineWidth(2);
|
||||
roundRect(ctx, x, y, w, h, 20);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 辅助函数:文字换行
|
||||
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
|
||||
let words = text.split("");
|
||||
let line = "";
|
||||
for (let n = 0; n < words.length; n++) {
|
||||
let testLine = line + words[n];
|
||||
let metrics = ctx.measureText(testLine);
|
||||
let testWidth = metrics.width;
|
||||
if (testWidth > maxWidth && n > 0) {
|
||||
ctx.fillText(line, x, y);
|
||||
line = words[n];
|
||||
y += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
ctx.fillText(line, x, y);
|
||||
}
|
||||
|
||||
// 辅助函数:文字换行(居中)
|
||||
function wrapTextCentered(ctx, text, x, y, maxWidth, lineHeight) {
|
||||
let words = text.split("");
|
||||
let line = "";
|
||||
for (let n = 0; n < words.length; n++) {
|
||||
let testLine = line + words[n];
|
||||
let metrics = ctx.measureText(testLine);
|
||||
let testWidth = metrics.width;
|
||||
if (testWidth > maxWidth && n > 0) {
|
||||
ctx.fillText(line, x, y);
|
||||
line = words[n];
|
||||
y += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
ctx.fillText(line, x, y);
|
||||
}
|
||||
|
||||
const onShareMoments = () => {
|
||||
uni.showToast({ title: "请点击右上角分享", icon: "none" });
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lucky-popup-container {
|
||||
/* 动画容器 */
|
||||
.animation-container {
|
||||
width: 600rpx;
|
||||
height: 850rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.card-flip-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.card-front,
|
||||
.card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 0 40rpx rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-front {
|
||||
background: linear-gradient(135deg, #fffcf5 0%, #fff4e6 100%);
|
||||
z-index: 2;
|
||||
|
||||
.loading-circle {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
position: relative;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #ff8f00;
|
||||
border-radius: 50%;
|
||||
animation: orbit 1.5s linear infinite;
|
||||
|
||||
/* 绝对居中 */
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -10rpx;
|
||||
margin-left: -10rpx;
|
||||
|
||||
&.p1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
&.p2 {
|
||||
animation-delay: -0.375s;
|
||||
}
|
||||
&.p3 {
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
&.p4 {
|
||||
animation-delay: -1.125s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #d81e06;
|
||||
font-size: 28rpx;
|
||||
letter-spacing: 2rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.card-back {
|
||||
background: #fff;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.light-effect {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 215, 0, 0.8) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
opacity: 0;
|
||||
animation: flash 0.6s ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 结果展示 */
|
||||
.result-container {
|
||||
width: 600rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lucky-card {
|
||||
width: 100%;
|
||||
height: 850rpx;
|
||||
background: #fff;
|
||||
border-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.card-header {
|
||||
height: 360rpx;
|
||||
background: linear-gradient(180deg, #d84315 0%, #ffca28 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
|
||||
.header-decor {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.6;
|
||||
|
||||
&.left {
|
||||
left: 30rpx;
|
||||
}
|
||||
&.right {
|
||||
right: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.header-label {
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.score-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
line-height: 1;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.score {
|
||||
font-size: 120rpx;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.percent {
|
||||
font-size: 40rpx;
|
||||
margin-left: 4rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.lucky-word {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4rpx;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.tag-year {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 20rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 40rpx 32rpx;
|
||||
|
||||
.yi-ji-grid {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.grid-item {
|
||||
flex: 1;
|
||||
background: #fbfbfb;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid #f5f5f5;
|
||||
|
||||
.item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lucky-elements {
|
||||
background: #fbfbfb;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid #f5f5f5;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.elements-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
text {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.elements-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.el-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
|
||||
&.color-val {
|
||||
color: #d84315;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2rpx;
|
||||
height: 40rpx;
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0 40rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.uni-icons) {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(10rpx);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: 12rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&:active :deep(.uni-icons) {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-label {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
0% {
|
||||
transform: rotate(0deg) translateX(40rpx) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg) translateX(40rpx) rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
.lucky-canvas {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -89,6 +89,14 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/avatar/download",
|
||||
"style": {
|
||||
"navigationBarTitleText": "精选头像",
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/creation/index",
|
||||
"style": {
|
||||
|
||||
609
pages/avatar/download.vue
Normal file
609
pages/avatar/download.vue
Normal file
@@ -0,0 +1,609 @@
|
||||
<template>
|
||||
<view class="avatar-download-page">
|
||||
<NavBar title="精选头像" />
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<view class="category-tabs">
|
||||
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
|
||||
<view class="tabs-content">
|
||||
<view
|
||||
v-for="(item, index) in categories"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: currentCategoryId === item.id }"
|
||||
@tap="switchCategory(item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Points Info -->
|
||||
<view class="points-bar">
|
||||
<view class="points-left">
|
||||
<uni-icons type="download" size="14" color="#ff9800" />
|
||||
<text>每次下载消耗 {{ downloadCost }} 积分</text>
|
||||
</view>
|
||||
<view class="points-right">
|
||||
<text>当前积分:</text>
|
||||
<text class="score-val">{{ userPoints }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Avatar Grid -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="avatar-scroll"
|
||||
@scrolltolower="loadMore"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<view class="grid-container">
|
||||
<view class="grid-item" v-for="(item, index) in avatars" :key="index">
|
||||
<image
|
||||
:src="getThumbUrl(item.imageUrl)"
|
||||
mode="aspectFill"
|
||||
class="avatar-img"
|
||||
@tap="previewImage(index)"
|
||||
/>
|
||||
<view class="action-overlay">
|
||||
<button
|
||||
class="action-btn share"
|
||||
open-type="share"
|
||||
:data-item="item"
|
||||
@tap.stop="shareAvatar(item)"
|
||||
>
|
||||
<text class="icon">➦</text>
|
||||
</button>
|
||||
<view class="action-btn download" @tap.stop="downloadAvatar(item)">
|
||||
<text class="icon">↓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Loading State -->
|
||||
<view class="loading-state" v-if="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view class="empty-state" v-if="!loading && avatars.length === 0">
|
||||
<text>暂无内容</text>
|
||||
</view>
|
||||
<view class="no-more" v-if="!loading && !hasMore && avatars.length > 0">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
|
||||
<!-- Spacer for bottom button -->
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- Bottom Button -->
|
||||
<view class="bottom-action-bar">
|
||||
<button class="create-btn" @tap="goToMake">
|
||||
<uni-icons
|
||||
type="compose"
|
||||
size="20"
|
||||
color="#fff"
|
||||
style="margin-right: 8rpx"
|
||||
/>
|
||||
去制作专属头像
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<LoginPopup
|
||||
ref="loginPopupRef"
|
||||
@logind="handleLogind"
|
||||
:share-token="shareToken"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import {
|
||||
getAvatarSystemCategoryList,
|
||||
getAvatarSystemListByCategory,
|
||||
avatarDownloadRecord,
|
||||
} from "@/api/avatar.js";
|
||||
import {
|
||||
saveRemoteImageToLocal,
|
||||
saveRecordRequest,
|
||||
getShareToken,
|
||||
trackRecord,
|
||||
saveViewRequest,
|
||||
} from "@/utils/common.js";
|
||||
import { onShareAppMessage, onShareTimeline, onLoad } from "@dcloudio/uni-app";
|
||||
import { getShareReward, abilityCheck, watchAdReward } from "@/api/system.js";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import NavBar from "@/components/NavBar/NavBar.vue";
|
||||
|
||||
let videoAd = null;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loginPopupRef = ref(null);
|
||||
|
||||
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
|
||||
const userPoints = computed(() => userStore.userInfo.points || 0);
|
||||
const downloadCost = ref(20);
|
||||
|
||||
const categories = ref([]);
|
||||
const currentCategoryId = ref(null);
|
||||
const avatars = ref([]);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const isRefreshing = ref(false);
|
||||
const shareToken = ref("");
|
||||
|
||||
onShareAppMessage(async (options) => {
|
||||
if (!isLoggedIn.value) {
|
||||
const shareToken = await getShareToken("avatar_download_index", "");
|
||||
return {
|
||||
title: "新年好运已送达 🎊|祝福卡·头像·壁纸",
|
||||
path: `/pages/index/index?shareToken=${shareToken}`,
|
||||
imageUrl:
|
||||
"https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png",
|
||||
};
|
||||
}
|
||||
getShareReward({ scene: "avatar_download" });
|
||||
if (options.from === "button") {
|
||||
const shareToken = await getShareToken(
|
||||
"avatar_download",
|
||||
options?.target?.dataset?.item?.id,
|
||||
);
|
||||
return {
|
||||
title: "快来挑选喜欢的新春头像吧",
|
||||
path: `/pages/avatar/download?shareToken=${shareToken}`,
|
||||
};
|
||||
} else {
|
||||
const shareToken = await getShareToken("avatar_download_index", "");
|
||||
return {
|
||||
title: "新年好运已送达 🎊|祝福卡·头像·壁纸",
|
||||
path: `/pages/index/index?shareToken=${shareToken}`,
|
||||
imageUrl:
|
||||
"https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onShareTimeline(async () => {
|
||||
const shareToken = await getShareToken("avatar_timeline");
|
||||
return {
|
||||
title: "精选新年头像,定制专属祝福 🧧",
|
||||
query: `shareToken=${shareToken}`,
|
||||
imageUrl:
|
||||
"https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png",
|
||||
};
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.shareToken) {
|
||||
shareToken.value = options.shareToken;
|
||||
saveViewRequest(options.shareToken, "avatar_download");
|
||||
}
|
||||
fetchCategories();
|
||||
trackRecord({
|
||||
eventName: "avatar_download_page_visit",
|
||||
eventType: `visit`,
|
||||
});
|
||||
|
||||
// Initialize Rewarded Video Ad
|
||||
if (uni.createRewardedVideoAd) {
|
||||
videoAd = uni.createRewardedVideoAd({
|
||||
adUnitId: "adunit-d7a28e0357d98947",
|
||||
});
|
||||
videoAd.onLoad(() => {
|
||||
console.log("ad loaded");
|
||||
});
|
||||
videoAd.onError((err) => {
|
||||
console.error("ad load error", err);
|
||||
});
|
||||
videoAd.onClose((res) => {
|
||||
console.log(1212121212, res);
|
||||
|
||||
if (res && res.isEnded) {
|
||||
handleAdReward();
|
||||
} else {
|
||||
// Playback not completed
|
||||
uni.showToast({ title: "观看完整广告才能获取积分哦", icon: "none" });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const getThumbUrl = (url) => {
|
||||
// Use square thumbnail for avatars
|
||||
return `${url}?imageView2/1/w/340/h/340/q/80`;
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await getAvatarSystemCategoryList();
|
||||
const list = Array.isArray(res) ? res : res?.list || [];
|
||||
if (list.length > 0) {
|
||||
categories.value = list;
|
||||
currentCategoryId.value = list[0].id;
|
||||
loadAvatars(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch categories", e);
|
||||
uni.showToast({ title: "获取分类失败", icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
const switchCategory = (id) => {
|
||||
if (currentCategoryId.value === id) return;
|
||||
currentCategoryId.value = id;
|
||||
loadAvatars(true);
|
||||
trackRecord({
|
||||
eventName: "avatar_category_click",
|
||||
eventType: `select`,
|
||||
elementId: id || "",
|
||||
});
|
||||
};
|
||||
|
||||
const loadAvatars = async (reset = false) => {
|
||||
if (loading.value) return;
|
||||
if (!currentCategoryId.value) return;
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
avatars.value = [];
|
||||
}
|
||||
if (!hasMore.value) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAvatarSystemListByCategory(
|
||||
currentCategoryId.value,
|
||||
page.value,
|
||||
);
|
||||
const list = res?.list || [];
|
||||
hasMore.value = !!res?.hasNext;
|
||||
|
||||
if (reset) {
|
||||
avatars.value = list;
|
||||
} else {
|
||||
avatars.value = [...avatars.value, ...list];
|
||||
}
|
||||
|
||||
if (hasMore.value) {
|
||||
page.value++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch avatars", e);
|
||||
uni.showToast({ title: "获取列表失败", icon: "none" });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
loadAvatars();
|
||||
};
|
||||
|
||||
const handleLogind = async () => {
|
||||
// Logic after successful login if needed
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
isRefreshing.value = true;
|
||||
loadAvatars(true);
|
||||
};
|
||||
|
||||
const previewImage = (index) => {
|
||||
const urls = avatars.value.map((item) => item.imageUrl);
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current: index,
|
||||
});
|
||||
const item = avatars.value[index];
|
||||
trackRecord({
|
||||
eventName: "avatar_preview_click",
|
||||
eventType: `select`,
|
||||
elementId: item?.id || "",
|
||||
});
|
||||
};
|
||||
|
||||
const showRewardAd = () => {
|
||||
if (videoAd) {
|
||||
videoAd.show().catch(() => {
|
||||
// Failed to load, try loading again
|
||||
videoAd
|
||||
.load()
|
||||
.then(() => videoAd.show())
|
||||
.catch((err) => {
|
||||
console.error("Ad show failed", err);
|
||||
uni.showToast({ title: "广告加载失败,请稍后再试", icon: "none" });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
uni.showToast({ title: "当前环境不支持广告", icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdReward = async () => {
|
||||
try {
|
||||
const res = await watchAdReward();
|
||||
if (res) {
|
||||
uni.showToast({
|
||||
title: "获得50积分",
|
||||
icon: "success",
|
||||
});
|
||||
await userStore.fetchUserAssets();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Reward claim failed", e);
|
||||
uni.showToast({ title: "奖励发放失败", icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAvatar = async (item) => {
|
||||
trackRecord({
|
||||
eventName: "avatar_download_click",
|
||||
eventType: `click`,
|
||||
elementId: item?.id || "",
|
||||
});
|
||||
if (!isLoggedIn.value) {
|
||||
loginPopupRef.value.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use avatar specific ability check if exists, otherwise reuse generic or skip?
|
||||
// Assuming 'avatar_download' ability check exists or similar
|
||||
const abilityRes = await abilityCheck("avatar_download");
|
||||
if (!abilityRes.canUse) {
|
||||
if (
|
||||
abilityRes?.blockType === "need_share" &&
|
||||
abilityRes?.message === "分享可继续"
|
||||
) {
|
||||
uni.showToast({
|
||||
title: "分享给好友即可下载",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
abilityRes?.blockType === "need_ad" &&
|
||||
abilityRes?.message === "观看广告可继续"
|
||||
) {
|
||||
uni.showModal({
|
||||
title: "积分不足",
|
||||
content: "观看广告可获得50积分,继续下载",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
showRewardAd();
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showToast({
|
||||
title: "您今日下载次数已用完,明日再试",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showLoading({ title: "下载中..." });
|
||||
try {
|
||||
await Promise.all([
|
||||
saveRemoteImageToLocal(item.imageUrl),
|
||||
avatarDownloadRecord({ id: item.id, url: item.imageUrl }),
|
||||
]);
|
||||
|
||||
await userStore.fetchUserAssets();
|
||||
uni.showToast({ title: "保存成功 消耗 20 积分", icon: "success" });
|
||||
} catch (e) {
|
||||
console.error("Download failed", e);
|
||||
// saveRemoteImageToLocal handles its own error toast usually, but let's be safe
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const shareAvatar = (item) => {};
|
||||
|
||||
const goToMake = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/avatar/index",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-download-page {
|
||||
height: 100vh;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
padding: 0;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #eeeeee;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.points-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 30rpx;
|
||||
background-color: #fffbf0;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.points-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.points-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.score-val {
|
||||
color: #d81e06;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
display: inline-flex;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 24rpx 30rpx;
|
||||
font-size: 30rpx;
|
||||
color: #999999;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #e60012;
|
||||
font-weight: bold;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80rpx;
|
||||
height: 4rpx;
|
||||
background-color: #e60012;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-scroll {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 30rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
aspect-ratio: 1; /* Make it square */
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.action-overlay {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn.share .icon {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.action-btn .icon {
|
||||
color: #fff;
|
||||
font-size: 36rpx;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #999999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 140rpx; /* Space for fixed bottom bar */
|
||||
}
|
||||
|
||||
.bottom-action-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx calc(20rpx + constant(safe-area-inset-bottom));
|
||||
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background: linear-gradient(135deg, #ff3b30, #ff1744);
|
||||
color: #fff;
|
||||
border-radius: 50rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,18 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Points Info -->
|
||||
<view class="points-bar">
|
||||
<view class="points-left">
|
||||
<uni-icons type="download" size="14" color="#ff9800" />
|
||||
<text>每次下载消耗 {{ downloadCost }} 积分</text>
|
||||
</view>
|
||||
<view class="points-right">
|
||||
<text>当前积分:</text>
|
||||
<text class="score-val">{{ userScore }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Wallpaper Grid -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
@@ -100,6 +112,9 @@ const userStore = useUserStore();
|
||||
const loginPopupRef = ref(null);
|
||||
|
||||
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
|
||||
const userScore = computed(() => userStore.userInfo.score || 0);
|
||||
const downloadCost = ref(20);
|
||||
|
||||
const categories = ref([]);
|
||||
const currentCategoryId = ref(null);
|
||||
const wallpapers = ref([]);
|
||||
@@ -308,6 +323,35 @@ const shareWallpaper = (item) => {};
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.points-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 30rpx;
|
||||
background-color: #fffbf0;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.points-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.points-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.score-val {
|
||||
color: #d81e06;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { wxLogin, wxGetUserProfile } from "@/utils/login.js";
|
||||
import { getPlatformProvider } from "@/utils/system";
|
||||
import { getUserInfo } from "@/api/auth.js";
|
||||
import { getUserInfo, getUserAsset } from "@/api/auth.js";
|
||||
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
@@ -55,6 +55,28 @@ export const useUserStore = defineStore("user", {
|
||||
console.error("fetchUserInfo error", e);
|
||||
}
|
||||
},
|
||||
async fetchUserAssets() {
|
||||
try {
|
||||
const res = await getUserAsset();
|
||||
if (res) {
|
||||
const newInfo = { ...this.userInfo, ...res };
|
||||
if (res.points !== undefined) {
|
||||
newInfo.points = res.points;
|
||||
}
|
||||
|
||||
if (res.exp !== undefined) {
|
||||
newInfo.exp = res.exp;
|
||||
}
|
||||
|
||||
if (res.level !== undefined) {
|
||||
newInfo.level = res.level;
|
||||
}
|
||||
this.setUserInfo(newInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("fetchUserAssets error", e);
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.userInfo = {};
|
||||
this.token = "";
|
||||
|
||||
178
utils/lunar.js
Normal file
178
utils/lunar.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 农历转换工具
|
||||
* 仅包含简单的公历转农历功能
|
||||
*/
|
||||
|
||||
const calendar = {
|
||||
/**
|
||||
* 农历 1900-2049 的润大小信息表
|
||||
* @Array Of Property
|
||||
* @return Hex
|
||||
*/
|
||||
lunarInfo: [
|
||||
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0,
|
||||
0x09ad0, 0x055d2, 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540,
|
||||
0x0d6a0, 0x0ada2, 0x095b0, 0x14977, 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50,
|
||||
0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, 0x06566, 0x0d4a0,
|
||||
0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
|
||||
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2,
|
||||
0x0a950, 0x0b557, 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573,
|
||||
0x052d0, 0x0a9a8, 0x0e950, 0x06aa0, 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4,
|
||||
0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, 0x096d0, 0x04dd5,
|
||||
0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6,
|
||||
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46,
|
||||
0x0ab60, 0x09570, 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58,
|
||||
0x055c0, 0x0ab60, 0x096d5, 0x092e0, 0x0c960, 0x0d954, 0x0d4a0, 0x0da50,
|
||||
0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, 0x0a950, 0x0b4a0,
|
||||
0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
|
||||
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260,
|
||||
0x0ea65, 0x0d530, 0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0,
|
||||
0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, 0x0b5a0, 0x056d0, 0x055b2, 0x049b0,
|
||||
0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
|
||||
],
|
||||
|
||||
/**
|
||||
* 传回农历 y年的总天数
|
||||
* @param y
|
||||
* @return Number
|
||||
*/
|
||||
lYearDays: function (y) {
|
||||
var i,
|
||||
sum = 348;
|
||||
for (i = 0x8000; i > 0x8; i >>= 1)
|
||||
sum += this.lunarInfo[y - 1900] & i ? 1 : 0;
|
||||
return sum + this.leapDays(y);
|
||||
},
|
||||
|
||||
/**
|
||||
* 传回农历 y年闰月的天数
|
||||
* @param y
|
||||
* @return Number
|
||||
*/
|
||||
leapDays: function (y) {
|
||||
if (this.leapMonth(y)) {
|
||||
return this.lunarInfo[y - 1900] & 0x10000 ? 30 : 29;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 传回农历 y年闰哪个月 1-12 , 没闰传回 0
|
||||
* @param y
|
||||
* @return Number
|
||||
*/
|
||||
leapMonth: function (y) {
|
||||
return this.lunarInfo[y - 1900] & 0xf;
|
||||
},
|
||||
|
||||
/**
|
||||
* 传回农历 y年m月的总天数
|
||||
* @param y
|
||||
* @param m
|
||||
* @return Number
|
||||
*/
|
||||
monthDays: function (y, m) {
|
||||
return this.lunarInfo[y - 1900] & (0x10000 >> m) ? 30 : 29;
|
||||
},
|
||||
|
||||
/**
|
||||
* 算出农历, 传入日期控件, 传回农历日期对象
|
||||
* @param objDate
|
||||
* @return Object
|
||||
*/
|
||||
solar2lunar: function (objDate) {
|
||||
var i,
|
||||
temp = 0;
|
||||
var baseDate = new Date(1900, 0, 31);
|
||||
var offset = Math.floor(
|
||||
(objDate.getTime() - baseDate.getTime()) / 86400000,
|
||||
);
|
||||
|
||||
var year = 1900;
|
||||
for (i = 1900; i < 2050 && offset > 0; i++) {
|
||||
temp = this.lYearDays(i);
|
||||
if (offset < temp) break;
|
||||
offset -= temp;
|
||||
year = i;
|
||||
}
|
||||
|
||||
// 如果循环结束时 offset 仍大于0 (超出范围),则年份最后一次增加未被撤销?
|
||||
// 不,break时 year=i。 如果没break,year会一直增加。
|
||||
// 修正:循环里 year = i 是对的。
|
||||
// 但是 calendar.js 原版通常是 year++ 在 check 之后?
|
||||
// 这里的逻辑:
|
||||
// offset 是总天数。
|
||||
// 减去1900年的天数,如果 offset > 0,说明在1900之后。
|
||||
// year 变成 1901。
|
||||
// 正确。
|
||||
|
||||
var leapMonth = this.leapMonth(year);
|
||||
var isLeap = false;
|
||||
var month = 1;
|
||||
|
||||
for (i = 1; i < 13; i++) {
|
||||
// 闰月
|
||||
temp = this.monthDays(year, i);
|
||||
if (offset < temp) {
|
||||
month = i;
|
||||
break;
|
||||
}
|
||||
offset -= temp;
|
||||
|
||||
if (leapMonth > 0 && i == leapMonth) {
|
||||
temp = this.leapDays(year);
|
||||
if (offset < temp) {
|
||||
isLeap = true;
|
||||
month = i;
|
||||
break;
|
||||
}
|
||||
offset -= temp;
|
||||
}
|
||||
}
|
||||
|
||||
var day = offset + 1;
|
||||
|
||||
// 格式化输出
|
||||
const monthCn = this.toChinaMonth(month);
|
||||
const dayCn = this.toChinaDay(day);
|
||||
|
||||
return {
|
||||
lYear: year,
|
||||
lMonth: month,
|
||||
lDay: day,
|
||||
isLeap: isLeap,
|
||||
monthCn: monthCn,
|
||||
dayCn: dayCn,
|
||||
lunarDateStr: (isLeap ? "闰" : "") + monthCn + dayCn,
|
||||
};
|
||||
},
|
||||
|
||||
toChinaMonth: function (m) {
|
||||
var s = "正二三四五六七八九十冬腊";
|
||||
var str = s.substring(m - 1, m);
|
||||
return str + "月";
|
||||
},
|
||||
|
||||
toChinaDay: function (d) {
|
||||
var s = "初十廿三";
|
||||
var arr = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
|
||||
var str = "";
|
||||
switch (Math.floor(d / 10)) {
|
||||
case 0:
|
||||
str = "初" + arr[d - 1];
|
||||
break;
|
||||
case 1:
|
||||
str = d == 10 ? "初十" : "十" + arr[d - 11];
|
||||
break;
|
||||
case 2:
|
||||
str = d == 20 ? "二十" : "廿" + arr[d - 21];
|
||||
break;
|
||||
case 3:
|
||||
str = d == 30 ? "三十" : "三" + arr[d - 31];
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
},
|
||||
};
|
||||
|
||||
export default calendar;
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE_URL = "https://api.ai-meng.com";
|
||||
// const BASE_URL = "https://api.ai-meng.com";
|
||||
// const BASE_URL = 'http://127.0.0.1:3999'
|
||||
// const BASE_URL = "http://192.168.1.3:3999";
|
||||
const BASE_URL = "http://192.168.1.2:3999";
|
||||
// const BASE_URL = "http://192.168.31.253:3999";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { getPlatform } from "./system.js";
|
||||
|
||||
Reference in New Issue
Block a user