437 lines
11 KiB
Vue
437 lines
11 KiB
Vue
<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>
|