472 lines
12 KiB
Vue
472 lines
12 KiB
Vue
<script setup>
|
||
import { ref, computed, onMounted, onBeforeUnmount, watch } 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 { generateHeatmapImage } from "@/heatmap";
|
||
|
||
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({
|
||
weeksCheckIn: [],
|
||
});
|
||
|
||
const list = ref([]);
|
||
const bowTargetSrc = ref("");
|
||
const heatMapImageSrc = ref(""); // 存储热力图图片地址
|
||
const canvasVisible = ref(false); // 控制canvas显示状态
|
||
|
||
const toListPage = () => {
|
||
uni.navigateTo({
|
||
url: "/pages/point-book-list",
|
||
});
|
||
};
|
||
|
||
const onSignin = () => {
|
||
showModal.value = true;
|
||
};
|
||
|
||
const startScoring = () => {
|
||
if (user.value.id) {
|
||
uni.navigateTo({
|
||
url: "/pages/point-book-create",
|
||
});
|
||
} else {
|
||
showModal.value = true;
|
||
}
|
||
};
|
||
|
||
// 生成热力图测试数据
|
||
const generateHeatMapData = (width, height, amount = 100) => {
|
||
const data = [];
|
||
const centerX = 0.5; // 中心点X坐标
|
||
const centerY = 0.5; // 中心点Y坐标
|
||
|
||
// 生成500条记录
|
||
for (let i = 0; i < amount; i++) {
|
||
let x, y, count;
|
||
|
||
// 30%的数据集中在中心区域(高斯分布)
|
||
if (Math.random() < 0.3) {
|
||
// 使用正态分布生成中心区域的数据
|
||
const angle = Math.random() * 2 * Math.PI;
|
||
const radius = Math.sqrt(-2 * Math.log(Math.random())) * 0.15; // 标准差0.15
|
||
x = centerX + radius * Math.cos(angle);
|
||
y = centerY + radius * Math.sin(angle);
|
||
count = Math.floor(Math.random() * 20);
|
||
} else {
|
||
x = Math.random() * 0.8 + 0.1; // 0.1-0.9范围
|
||
y = Math.random() * 0.8 + 0.1;
|
||
count = Math.floor(Math.random() * 20);
|
||
}
|
||
|
||
// 确保坐标在0-1范围内
|
||
x = Math.max(0.05, Math.min(0.95, x));
|
||
y = Math.max(0.05, Math.min(0.95, y));
|
||
|
||
data.push({
|
||
x: parseFloat(x.toFixed(3)),
|
||
y: parseFloat(y.toFixed(3)),
|
||
ring: Math.floor(Math.random() * 5) + 6, // 6-10环
|
||
count: count,
|
||
});
|
||
}
|
||
|
||
return data;
|
||
};
|
||
|
||
const loadData = async () => {
|
||
const result = await getPointBookListAPI(1);
|
||
list.value = result.slice(0, 3);
|
||
const result2 = await getPointBookStatisticsAPI();
|
||
|
||
const rect = await getElementRect(".heat-map");
|
||
// const testWeekArrows = generateHeatMapData(rect.width, rect.height);
|
||
// result2.weekArrows = testWeekArrows;
|
||
// console.log(result2.weekArrows)
|
||
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);
|
||
setTimeout(async () => {
|
||
try {
|
||
const imagePath = await generateHeatmapImage(
|
||
"heatMapCanvas",
|
||
rect.width,
|
||
rect.height,
|
||
result2.weekArrows.filter((item) => item.x && item.y)
|
||
);
|
||
heatMapImageSrc.value = imagePath; // 存储生成的图片地址
|
||
console.log("热力图图片地址:", imagePath);
|
||
} catch (error) {
|
||
console.error("生成热力图图片失败:", error);
|
||
}
|
||
}, 500);
|
||
};
|
||
|
||
watch(
|
||
() => user.value.id,
|
||
(id) => {
|
||
if (id) loadData();
|
||
}
|
||
);
|
||
|
||
onShow(async () => {
|
||
if (user.value.id) loadData();
|
||
});
|
||
|
||
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>{{
|
||
data.yellowRate !== undefined
|
||
? 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"
|
||
/>
|
||
<image v-if="heatMapImageSrc" :src="heatMapImageSrc" mode="widthFix" />
|
||
<canvas
|
||
canvas-id="heatMapCanvas"
|
||
style="
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 2;
|
||
"
|
||
/>
|
||
</view>
|
||
<view class="reward" v-if="data.totalArrow">
|
||
<button hover-class="none" @click="showTip = true">
|
||
<image src="../static/reward-us.png" mode="widthFix" />
|
||
</button>
|
||
</view>
|
||
<RingBarChart :data="data.ringRate" />
|
||
<view class="title" v-if="user.id">
|
||
<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 :show="showTip" :onClose="() => (showTip = false)" />
|
||
</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%;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
}
|
||
|
||
.heat-map > canvas {
|
||
position: absolute;
|
||
top: -1000px;
|
||
left: 0px;
|
||
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>
|