Files
shoot-miniprograms/src/pages/point-book.vue
2025-09-25 15:18:24 +08:00

437 lines
11 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.

<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import PointRecord from "@/components/PointRecord.vue";
import RingBarChart from "@/components/RingBarChart.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import ScreenHint2 from "@/components/ScreenHint2.vue";
import RewardUs from "@/components/RewardUs.vue";
import {
getHomeData,
getPointBookConfigAPI,
getPointBookListAPI,
getPointBookStatisticsAPI,
} from "@/apis";
import { getElementRect } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { updateUser } = store;
const { user } = storeToRefs(store);
const isIOS = computed(() => {
const systemInfo = uni.getDeviceInfo();
return systemInfo.osName === "ios";
});
const showModal = ref(false);
const showTip = ref(false);
const data = ref({
yellowRate: 0,
weeksCheckIn: [],
});
const list = ref([]);
const bowTargetSrc = ref("");
const toListPage = () => {
uni.navigateTo({
url: "/pages/point-book-list",
});
};
// 绘制热力图
const drawHeatMap = (width, height, arrows) => {
try {
const ctx = uni.createCanvasContext("heatMapCanvas");
let minCount = 0;
let maxCount = 0;
// 计算最大和最小频次
arrows.forEach((point) => {
if (point.count > maxCount) {
maxCount = point.count;
}
if (point.count < minCount) {
minCount = point.count;
}
});
// 绘制热力点
arrows.forEach((point) => {
if (point.x === 0 && point.y === 0) return;
// 数量越多,半径越小(反比关系)
const radius = 20;
// 根据频次设置同一种颜色的5个深浅度
let color;
if (point.count >= maxCount * 0.5) {
color = "rgba(255, 232, 143, 0.7)"; // 最深 - 频次5次及以上
} else if (point.count >= maxCount * 0.4) {
color = "rgba(255, 232, 143, 0.5)"; // 较深 - 频次4次
} else if (point.count >= maxCount * 0.3) {
color = "rgba(255, 232, 143, 0.3)"; // 中等 - 频次3次
} else if (point.count >= maxCount * 0.2) {
color = "rgba(255, 232, 143, 0.2)"; // 较浅 - 频次2次
} else {
color = "rgba(255, 232, 143, 0.1)"; // 最浅 - 频次1次
}
// 绘制圆形热力点
ctx.setFillStyle(color);
ctx.beginPath();
ctx.arc(point.x * width, point.y * height, radius, 0, 2 * Math.PI);
ctx.fill();
});
ctx.draw();
} catch (error) {
console.error("绘制热力图失败:", error);
}
};
onShow(async () => {
const result = await getPointBookListAPI(1);
list.value = result.slice(0, 3);
const result2 = await getPointBookStatisticsAPI();
data.value = result2;
let hot = 0;
if (result2.checkInCount > -3 && result2.checkInCount < 3) hot = 1;
else if (result2.checkInCount >= 3) hot = 2;
else if (result2.checkInCount >= 5) hot = 3;
else if (result2.checkInCount === 7) hot = 4;
uni.$emit("update-hot", hot);
const rect = await getElementRect(".heat-map");
// 延迟绘制热力图确保Canvas准备就绪
setTimeout(() => {
drawHeatMap(rect.width, rect.height, result2.weekArrows);
}, 500);
});
const onSignin = () => {
showModal.value = true;
};
const startScoring = () => {
if (user.value.id) {
uni.navigateTo({
url: "/pages/point-book-create",
});
} else {
showModal.value = true;
}
};
onMounted(async () => {
uni.$on("point-book-signin", onSignin);
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (!user.value.id && token) {
const data = await getHomeData();
if (data.user) updateUser(data.user);
}
const config = await getPointBookConfigAPI();
uni.setStorageSync("point-book-config", config);
if (config.targetOption && config.targetOption[0]) {
bowTargetSrc.value = config.targetOption[0].icon;
}
});
onBeforeUnmount(() => {
uni.$off("point-book-signin", onSignin);
});
onShareAppMessage(() => {
return {
title: "高效记录每一次射箭,深度分析助你提升!",
path: "pages/point-book-create",
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-22/dcz4m4nbgycqqwknrv.png",
};
});
onShareTimeline(() => {
return {
title: "高效记录每一次射箭,深度分析助你提升!",
query: "from=timeline",
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-22/dcz4m4nbgycqqwknrv.png",
};
});
</script>
<template>
<Container :bgType="4" bgColor="#F5F5F5" :whiteBackArrow="false" title="">
<view class="container">
<view class="daily-signin">
<view>
<image src="../static/week-check.png" mode="widthFix" />
</view>
<view>
<image
v-if="data.weeksCheckIn[0]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周一</text>
</view>
<view>
<image
v-if="data.weeksCheckIn[1]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周二</text>
</view>
<view>
<image
v-if="data.weeksCheckIn[2]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周三</text>
</view>
<view>
<image
v-if="data.weeksCheckIn[3]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周四</text>
</view>
<view>
<image
v-if="data.weeksCheckIn[4]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周五</text>
</view>
<view>
<image
v-if="data.weeksCheckIn[5]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周六</text>
</view>
<view>
<image
v-if="data.weeksCheckIn[6]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周日</text>
</view>
</view>
<view class="statistics">
<view>
<text>{{ data.todayTotalArrow }}</text>
<text>今日射箭</text>
</view>
<view>
<text>{{ data.totalArrow }}</text>
<text>累计射箭</text>
</view>
<view>
<text>{{ data.totalDay }}</text>
<text>已训练天数</text>
</view>
<view>
<text>{{ data.averageRing }}</text>
<text>平均环数</text>
</view>
<view>
<text>{{ Number(data.yellowRate * 100).toFixed(2) }}</text>
<text>黄心率%</text>
</view>
<view>
<button hover-class="none" @click="startScoring">
<image src="../static/start-scoring.png" mode="widthFix" />
</button>
</view>
</view>
<view class="title">
<image src="../static/point-book-title1.png" mode="widthFix" />
</view>
<view class="heat-map">
<image
:src="bowTargetSrc || '../static/bow-target.png'"
mode="widthFix"
/>
<canvas canvas-id="heatMapCanvas" style="width: 100%; height: 100%" />
</view>
<view class="reward">
<button hover-class="none" @click="showTip = true">
<image src="../static/reward-us.png" mode="widthFix" />
</button>
</view>
<RingBarChart :data="data.ringRate" />
<view class="title">
<image src="../static/point-book-title2.png" mode="widthFix" />
</view>
<block v-for="(item, index) in list" :key="index">
<PointRecord :data="item" />
</block>
<view
class="see-more"
@click="toListPage"
v-if="list.length"
:style="{ marginBottom: isIOS ? '10rpx' : 0 }"
>
<text>查看所有记录</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</view>
</view>
<SModal :show="showModal" :onClose="() => (showModal = false)" :noBg="true">
<Signin :onClose="() => (showModal = false)" :noBg="true" />
</SModal>
<ScreenHint2 :show="showTip" :onClose="() => (showTip = false)">
<RewardUs />
</ScreenHint2>
</Container>
</template>
<style scoped>
.container {
width: calc(100% - 50rpx);
padding: 25rpx;
}
.statistics {
border-radius: 25rpx;
border-bottom-left-radius: 50rpx;
border-bottom-right-radius: 50rpx;
border: 1rpx solid #fed847;
background: #fff;
font-size: 22rpx;
display: flex;
flex-wrap: wrap;
padding: 25rpx 0;
margin-bottom: 10rpx;
}
.statistics > view {
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.statistics > view:nth-child(-n + 3) {
margin-bottom: 25rpx;
}
.statistics > view:nth-child(2),
.statistics > view:nth-child(5) {
border-left: 1rpx solid #eeeeee;
border-right: 1rpx solid #eeeeee;
box-sizing: border-box;
}
.statistics > view > text {
text-align: center;
font-size: 22rpx;
color: #333333;
}
.statistics > view > text:first-child {
font-weight: 500;
font-size: 40rpx;
margin-bottom: 10rpx;
}
.statistics > view:last-child > button > image {
width: 164rpx;
}
.daily-signin {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 12rpx;
border-radius: 20rpx;
margin-bottom: 25rpx;
}
.daily-signin > view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 12rpx;
box-sizing: border-box;
}
.daily-signin > view:not(:first-child) {
background: #f8f8f8;
padding: 15rpx 8rpx;
}
.daily-signin > view:not(:first-child) > image {
width: 32rpx;
height: 32rpx;
}
.daily-signin > view:not(:first-child) > view {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
box-sizing: border-box;
border: 1rpx solid #333;
}
.daily-signin > view > text {
font-size: 24rpx;
/* color: #333; */
color: #999999;
font-weight: 500;
text-align: center;
margin-top: 12rpx;
}
.daily-signin > view:first-child > image {
width: 100%;
}
.title {
width: 100%;
display: flex;
justify-content: center;
margin: 25rpx 0;
}
.title > image {
width: 566rpx;
}
.heat-map {
position: relative;
margin: 10rpx;
width: calc(100vw - 70rpx);
height: calc(100vw - 70rpx);
}
.heat-map > image {
width: 100%;
}
.heat-map > canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
.reward {
width: 100%;
display: flex;
justify-content: flex-end;
margin-top: -100rpx;
position: relative;
z-index: 10;
}
.reward > button {
width: 100rpx;
}
.reward > button > image {
width: 100%;
height: 100%;
}
</style>