Files
shoot-miniprograms/src/pages/point-book.vue

483 lines
12 KiB
Vue
Raw Normal View History

2025-09-24 21:05:06 +08:00
<script setup>
2025-09-25 17:07:37 +08:00
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
2025-09-24 21:05:06 +08:00
import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import PointRecord from "@/components/PointRecord.vue";
2025-09-25 10:16:23 +08:00
import RingBarChart from "@/components/RingBarChart.vue";
2025-09-24 21:05:06 +08:00
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
2025-09-25 15:18:24 +08:00
import ScreenHint2 from "@/components/ScreenHint2.vue";
import RewardUs from "@/components/RewardUs.vue";
2025-09-24 21:05:06 +08:00
import {
getHomeData,
getPointBookConfigAPI,
getPointBookListAPI,
getPointBookStatisticsAPI,
} from "@/apis";
2025-09-28 18:28:49 +08:00
import { getElementRect } from "@/util";
2025-09-29 11:05:42 +08:00
import { generateKDEHeatmapImage } from "@/kde-heatmap";
2025-09-25 11:53:47 +08:00
2025-09-24 21:05:06 +08:00
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";
});
2025-10-11 09:06:56 +08:00
const loadImage = ref(false);
2025-09-24 21:05:06 +08:00
const showModal = ref(false);
2025-09-25 15:18:24 +08:00
const showTip = ref(false);
2025-09-24 21:05:06 +08:00
const data = ref({
weeksCheckIn: [],
});
const list = ref([]);
2025-09-25 10:59:49 +08:00
const bowTargetSrc = ref("");
2025-09-27 10:09:02 +08:00
const heatMapImageSrc = ref(""); // 存储热力图图片地址
2025-09-28 18:28:49 +08:00
const canvasVisible = ref(false); // 控制canvas显示状态
2025-09-24 21:05:06 +08:00
const toListPage = () => {
uni.navigateTo({
url: "/pages/point-book-list",
});
};
2025-09-25 17:07:37 +08:00
const onSignin = () => {
showModal.value = true;
};
const startScoring = () => {
if (user.value.id) {
uni.navigateTo({
url: "/pages/point-book-create",
});
} else {
showModal.value = true;
}
};
const loadData = async () => {
2025-09-24 21:05:06 +08:00
const result = await getPointBookListAPI(1);
2025-09-25 11:53:47 +08:00
list.value = result.slice(0, 3);
2025-09-24 21:05:06 +08:00
const result2 = await getPointBookStatisticsAPI();
2025-09-29 11:05:42 +08:00
data.value = result2;
2025-09-28 18:28:49 +08:00
const rect = await getElementRect(".heat-map");
2025-09-25 14:22:03 +08:00
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);
2025-10-11 09:06:56 +08:00
loadImage.value = true;
2025-09-30 16:47:00 +08:00
const generateHeatmapAsync = async () => {
const weekArrows = result2.weekArrows
.filter((item) => item.x && item.y)
.map((item) => [item.x, item.y]);
2025-09-30 10:53:06 +08:00
try {
2025-09-30 16:47:00 +08:00
// 渐进式渲染:数据量大时先快速渲染粗略版本
if (weekArrows.length > 1000) {
const quickPath = await generateKDEHeatmapImage(
"heatMapCanvas",
rect.width,
rect.height,
2025-10-03 16:40:28 +08:00
weekArrows
2025-09-30 16:47:00 +08:00
);
heatMapImageSrc.value = quickPath;
// 延迟后再渲染精细版本
await new Promise((resolve) => setTimeout(resolve, 500));
}
// 渲染最终精细版本
const finalPath = await generateKDEHeatmapImage(
2025-09-30 10:53:06 +08:00
"heatMapCanvas",
rect.width,
rect.height,
2025-09-30 16:47:00 +08:00
weekArrows,
2025-09-30 10:53:06 +08:00
{
2025-09-30 16:47:00 +08:00
range: [0, 1],
2025-09-30 10:53:06 +08:00
gridSize: 120, // 更高的网格密度,减少锯齿
bandwidth: 0.15, // 稍小的带宽,让热力图更细腻
2025-10-05 11:52:32 +08:00
showPoints: false,
2025-09-30 10:53:06 +08:00
}
);
2025-09-30 16:47:00 +08:00
heatMapImageSrc.value = finalPath;
2025-10-11 09:06:56 +08:00
loadImage.value = false;
2025-09-30 16:47:00 +08:00
console.log("热力图图片地址:", finalPath);
2025-09-30 10:53:06 +08:00
} catch (error) {
console.error("生成热力图图片失败:", error);
2025-10-11 09:06:56 +08:00
loadImage.value = false;
2025-09-30 10:53:06 +08:00
}
2025-09-30 16:47:00 +08:00
};
// 异步生成热力图不阻塞UI
generateHeatmapAsync();
2025-09-24 21:05:06 +08:00
};
2025-09-25 17:07:37 +08:00
watch(
() => user.value.id,
(id) => {
if (id) loadData();
2025-09-24 21:05:06 +08:00
}
2025-09-25 17:07:37 +08:00
);
onShow(async () => {
if (user.value.id) loadData();
});
2025-09-24 21:05:06 +08:00
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);
2025-09-25 10:59:49 +08:00
if (config.targetOption && config.targetOption[0]) {
bowTargetSrc.value = config.targetOption[0].icon;
}
2025-09-24 21:05:06 +08:00
});
onBeforeUnmount(() => {
uni.$off("point-book-signin", onSignin);
});
onShareAppMessage(() => {
return {
title: "高效记录每一次射箭,深度分析助你提升!",
2025-10-01 11:58:24 +08:00
path: "pages/point-book",
2025-09-24 21:05:06 +08:00
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>
2025-09-25 10:16:23 +08:00
<Container :bgType="4" bgColor="#F5F5F5" :whiteBackArrow="false" title="">
2025-09-24 21:05:06 +08:00
<view class="container">
<view class="daily-signin">
<view>
2025-09-30 18:29:08 +08:00
<image src="../static/week-check.png" />
2025-09-24 21:05:06 +08:00
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[0] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<image
v-if="data.weeksCheckIn[0]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周一</text>
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[1] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<image
v-if="data.weeksCheckIn[1]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周二</text>
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[2] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<image
v-if="data.weeksCheckIn[2]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周三</text>
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[3] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<image
v-if="data.weeksCheckIn[3]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周四</text>
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[4] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<image
v-if="data.weeksCheckIn[4]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周五</text>
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[5] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<image
v-if="data.weeksCheckIn[5]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>周六</text>
</view>
2025-09-30 10:53:06 +08:00
<view :class="data.weeksCheckIn[6] ? 'checked' : ''">
2025-09-24 21:05:06 +08:00
<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>
2025-09-25 17:07:37 +08:00
<text>{{ data.todayTotalArrow || "-" }}</text>
2025-09-30 16:47:00 +08:00
<text>今日射箭()</text>
2025-09-24 21:05:06 +08:00
</view>
<view>
2025-09-25 17:07:37 +08:00
<text>{{ data.totalArrow || "-" }}</text>
2025-09-30 16:47:00 +08:00
<text>累计射箭()</text>
2025-09-24 21:05:06 +08:00
</view>
<view>
2025-09-25 17:07:37 +08:00
<text>{{ data.totalDay || "-" }}</text>
2025-09-30 16:47:00 +08:00
<text>已训练天数()</text>
2025-09-24 21:05:06 +08:00
</view>
<view>
2025-09-25 17:07:37 +08:00
<text>{{ data.averageRing || "-" }}</text>
2025-09-30 16:47:00 +08:00
<text>平均环数()</text>
2025-09-24 21:05:06 +08:00
</view>
<view>
2025-09-25 17:07:37 +08:00
<text>{{
data.yellowRate !== undefined
2025-10-05 11:52:32 +08:00
? Number((data.yellowRate * 100).toFixed(2)) + "%"
2025-09-25 17:07:37 +08:00
: "-"
}}</text>
2025-10-05 11:52:32 +08:00
<text>黄心率</text>
2025-09-24 21:05:06 +08:00
</view>
<view>
<button hover-class="none" @click="startScoring">
<image src="../static/start-scoring.png" mode="widthFix" />
</button>
</view>
</view>
2025-10-21 15:13:22 +08:00
<view class="title" :style="{ marginBottom: 0 }">
2025-09-24 21:05:06 +08:00
<image src="../static/point-book-title1.png" mode="widthFix" />
</view>
2025-09-25 10:59:49 +08:00
<view class="heat-map">
<image
:src="bowTargetSrc || '../static/bow-target.png'"
mode="widthFix"
/>
2025-10-01 11:58:24 +08:00
<image
v-if="heatMapImageSrc"
:src="heatMapImageSrc"
mode="aspectFill"
/>
2025-10-11 09:06:56 +08:00
<view v-if="loadImage" class="load-image">
<text>生成中...</text>
</view>
2025-09-28 18:28:49 +08:00
<canvas
2025-10-03 16:40:28 +08:00
id="heatMapCanvas"
2025-09-28 18:28:49 +08:00
canvas-id="heatMapCanvas"
2025-10-03 16:40:28 +08:00
type="2d"
2025-09-28 18:28:49 +08:00
style="
width: 100%;
height: 100%;
position: absolute;
2025-09-29 11:14:49 +08:00
top: -1000px;
2025-09-28 18:28:49 +08:00
left: 0;
z-index: 2;
"
/>
2025-09-25 10:59:49 +08:00
</view>
2025-09-30 16:47:00 +08:00
<view class="reward" v-if="data.totalArrow">
2025-09-25 15:18:24 +08:00
<button hover-class="none" @click="showTip = true">
2025-09-25 14:22:03 +08:00
<image src="../static/reward-us.png" mode="widthFix" />
</button>
2025-09-25 15:18:24 +08:00
</view>
2025-10-01 11:58:24 +08:00
<RingBarChart :data="data.ringRate" v-if="user.id" />
2025-09-25 17:07:37 +08:00
<view class="title" v-if="user.id">
2025-09-24 21:05:06 +08:00
<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>
2025-09-25 15:18:24 +08:00
<ScreenHint2 :show="showTip" :onClose="() => (showTip = false)">
2025-09-25 17:07:37 +08:00
<RewardUs :show="showTip" :onClose="() => (showTip = false)" />
2025-09-25 15:18:24 +08:00
</ScreenHint2>
2025-09-24 21:05:06 +08:00
</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;
2025-09-30 18:29:08 +08:00
border: 4rpx solid #fed848;
2025-09-24 21:05:06 +08:00
background: #fff;
font-size: 22rpx;
display: flex;
flex-wrap: wrap;
2025-09-25 14:22:03 +08:00
padding: 25rpx 0;
2025-09-25 10:59:49 +08:00
margin-bottom: 10rpx;
2025-09-24 21:05:06 +08:00
}
.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);
2025-09-30 18:29:08 +08:00
gap: 10rpx;
2025-09-24 21:05:06 +08:00
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;
2025-09-30 10:53:06 +08:00
box-sizing: border-box;
2025-09-30 18:29:08 +08:00
width: 78rpx;
height: 94rpx;
padding-top: 10rpx;
2025-09-24 21:05:06 +08:00
}
.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;
2025-09-30 18:29:08 +08:00
border: 2rpx solid #333;
2025-09-24 21:05:06 +08:00
}
.daily-signin > view > text {
2025-09-30 18:29:08 +08:00
font-size: 20rpx;
2025-09-24 21:05:06 +08:00
color: #999999;
font-weight: 500;
text-align: center;
2025-09-30 18:29:08 +08:00
margin-top: 10rpx;
2025-09-24 21:05:06 +08:00
}
.daily-signin > view:first-child > image {
2025-09-30 18:29:08 +08:00
width: 72rpx;
height: 94rpx;
2025-09-24 21:05:06 +08:00
}
2025-09-30 10:53:06 +08:00
.checked {
2025-09-30 18:29:08 +08:00
border: 2rpx solid #000;
2025-09-30 10:53:06 +08:00
}
.checked > text {
color: #333 !important;
}
2025-09-24 21:05:06 +08:00
.title {
width: 100%;
display: flex;
justify-content: center;
2025-09-25 10:59:49 +08:00
margin: 25rpx 0;
2025-09-24 21:05:06 +08:00
}
.title > image {
width: 566rpx;
}
2025-09-25 10:59:49 +08:00
.heat-map {
position: relative;
2025-10-21 15:13:22 +08:00
margin: 0 auto;
2025-09-25 10:59:49 +08:00
width: calc(100vw - 70rpx);
height: calc(100vw - 70rpx);
2025-10-21 15:13:22 +08:00
transform: scale(0.9);
2025-09-25 10:59:49 +08:00
}
.heat-map > image {
width: 100%;
2025-09-30 10:53:06 +08:00
height: 100%;
2025-09-27 10:09:02 +08:00
position: absolute;
top: 0;
left: 0;
2025-09-25 10:59:49 +08:00
}
2025-10-11 09:06:56 +08:00
.load-image {
position: absolute;
width: 160rpx;
top: calc(50% - 65rpx);
left: calc(50% - 75rpx);
color: #525252;
font-size: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
2025-09-25 14:22:03 +08:00
.reward {
width: 100%;
display: flex;
justify-content: flex-end;
2025-10-21 15:13:22 +08:00
margin-top: -120rpx;
2025-09-25 15:18:24 +08:00
position: relative;
z-index: 10;
2025-09-25 14:22:03 +08:00
}
.reward > button {
width: 100rpx;
}
.reward > button > image {
width: 100%;
height: 100%;
}
2025-09-24 21:05:06 +08:00
</style>