优化
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 39s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 39s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -1,446 +0,0 @@
|
||||
<template>
|
||||
<div class="heatmap-card">
|
||||
<div class="grid-row">
|
||||
<!-- Weekday Labels (Fixed Left) -->
|
||||
<div class="weekday-col-fixed">
|
||||
<div class="weekday-label">
|
||||
二
|
||||
</div>
|
||||
<div class="weekday-label">
|
||||
四
|
||||
</div>
|
||||
<div class="weekday-label">
|
||||
六
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Heatmap Area -->
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="heatmap-scroll-container"
|
||||
>
|
||||
<div class="heatmap-content">
|
||||
<!-- Month Labels -->
|
||||
<div class="month-row">
|
||||
<div
|
||||
v-for="(month, index) in monthLabels"
|
||||
:key="index"
|
||||
class="month-label"
|
||||
:style="{ left: month.left + 'px' }"
|
||||
>
|
||||
{{ month.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heatmap Grid -->
|
||||
<div class="heatmap-grid">
|
||||
<div
|
||||
v-for="(week, wIndex) in weeks"
|
||||
:key="wIndex"
|
||||
class="heatmap-week"
|
||||
>
|
||||
<div
|
||||
v-for="(day, dIndex) in week"
|
||||
:key="dIndex"
|
||||
class="heatmap-cell"
|
||||
:class="getLevelClass(day)"
|
||||
@click="onCellClick(day)"
|
||||
>
|
||||
<!-- Tooltip could be implemented here or using title -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="heatmap-footer">
|
||||
<div
|
||||
v-if="totalCount > 0"
|
||||
class="summary-text"
|
||||
>
|
||||
过去一年共 {{ totalCount }} 笔交易
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span>少</span>
|
||||
<div class="legend-item level-0" />
|
||||
<div class="legend-item level-1" />
|
||||
<div class="legend-item level-2" />
|
||||
<div class="legend-item level-3" />
|
||||
<div class="legend-item level-4" />
|
||||
<span>多</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { getDailyStatisticsRange } from '@/api/statistics'
|
||||
|
||||
const stats = ref({})
|
||||
const weeks = ref([])
|
||||
const monthLabels = ref([])
|
||||
const totalCount = ref(0)
|
||||
const scrollContainer = ref(null)
|
||||
const thresholds = ref([2, 4, 7]) // Default thresholds
|
||||
|
||||
const CELL_SIZE = 15
|
||||
const CELL_GAP = 3
|
||||
const WEEK_WIDTH = CELL_SIZE + CELL_GAP
|
||||
|
||||
const formatDate = (d) => {
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
const endDate = new Date()
|
||||
const startDate = new Date()
|
||||
startDate.setFullYear(endDate.getFullYear() - 1)
|
||||
|
||||
try {
|
||||
const res = await getDailyStatisticsRange({
|
||||
startDate: formatDate(startDate),
|
||||
endDate: formatDate(endDate)
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
const map = {}
|
||||
let count = 0
|
||||
res.data.forEach((item) => {
|
||||
map[item.date] = item
|
||||
count += item.count
|
||||
})
|
||||
stats.value = map
|
||||
totalCount.value = count
|
||||
|
||||
// Calculate thresholds based on last 15 days average
|
||||
const today = new Date()
|
||||
let last15DaysSum = 0
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const d = new Date(today)
|
||||
d.setDate(d.getDate() - i)
|
||||
const dateStr = formatDate(d)
|
||||
last15DaysSum += map[dateStr]?.count || 0
|
||||
}
|
||||
|
||||
const avg = last15DaysSum / 15
|
||||
// Step size calculation: ensure at least 1, roughly avg/2 to create spread
|
||||
// Level 1: 1 ~ step
|
||||
// Level 2: step+1 ~ step*2
|
||||
// Level 3: step*2+1 ~ step*3
|
||||
// Level 4: > step*3
|
||||
const step = Math.max(Math.ceil(avg / 2), 1)
|
||||
thresholds.value = [step, step * 2, step * 3]
|
||||
|
||||
generateHeatmapData(startDate, endDate)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch heatmap data', e)
|
||||
}
|
||||
}
|
||||
|
||||
const generateHeatmapData = (startDate, endDate) => {
|
||||
const current = new Date(startDate)
|
||||
|
||||
const allDays = []
|
||||
|
||||
// Adjust start date to be Monday to align weeks
|
||||
// 0 = Sunday, 1 = Monday
|
||||
const startDay = current.getDay()
|
||||
// If startDay is 0 (Sunday), we need to go back 6 days to Monday
|
||||
// If startDay is 1 (Monday), we are good
|
||||
// If startDay is 2 (Tuesday), we need to go back 1 day
|
||||
// Formula: (day + 6) % 7 days back?
|
||||
// Monday (1) -> 0 days back
|
||||
// Sunday (0) -> 6 days back
|
||||
// Tuesday (2) -> 1 day back
|
||||
|
||||
// We don't necessarily need to subtract from startDate for data fetching,
|
||||
// but for grid alignment we want the first column to start on Monday.
|
||||
|
||||
const alignStart = new Date(startDate)
|
||||
// alignStart.setDate(alignStart.getDate() - daysToSubtract);
|
||||
|
||||
const tempDate = new Date(alignStart)
|
||||
while (tempDate <= endDate) {
|
||||
const dateStr = formatDate(tempDate)
|
||||
allDays.push({
|
||||
date: dateStr,
|
||||
count: stats.value[dateStr]?.count || 0,
|
||||
obj: new Date(tempDate)
|
||||
})
|
||||
tempDate.setDate(tempDate.getDate() + 1)
|
||||
}
|
||||
|
||||
// Now group into weeks
|
||||
const resultWeeks = []
|
||||
let currentWeek = []
|
||||
|
||||
// Pad first week if start date is not Monday
|
||||
// allDays[0] is startDate
|
||||
const firstDayObj = new Date(allDays[0].date)
|
||||
const firstDay = firstDayObj.getDay() // 0-6 (Sun-Sat)
|
||||
|
||||
// We want Monday (1) to be index 0
|
||||
// Mon(1)->0, Tue(2)->1, ..., Sun(0)->6
|
||||
const padCount = (firstDay + 6) % 7
|
||||
|
||||
for (let i = 0; i < padCount; i++) {
|
||||
currentWeek.push(null)
|
||||
}
|
||||
|
||||
allDays.forEach((day) => {
|
||||
currentWeek.push(day)
|
||||
if (currentWeek.length === 7) {
|
||||
resultWeeks.push(currentWeek)
|
||||
currentWeek = []
|
||||
}
|
||||
})
|
||||
|
||||
// Push last partial week
|
||||
if (currentWeek.length > 0) {
|
||||
while (currentWeek.length < 7) {
|
||||
currentWeek.push(null)
|
||||
}
|
||||
resultWeeks.push(currentWeek)
|
||||
}
|
||||
|
||||
weeks.value = resultWeeks
|
||||
|
||||
// Generate Month Labels
|
||||
const labels = []
|
||||
let lastMonth = -1
|
||||
|
||||
resultWeeks.forEach((week, index) => {
|
||||
// Check the first valid day in the week
|
||||
const day = week.find((d) => d !== null)
|
||||
if (day) {
|
||||
const d = new Date(day.date)
|
||||
const month = d.getMonth()
|
||||
if (month !== lastMonth) {
|
||||
labels.push({
|
||||
text: d.toLocaleString('zh-CN', { month: 'short' }),
|
||||
left: index * WEEK_WIDTH
|
||||
})
|
||||
lastMonth = month
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
monthLabels.value = labels
|
||||
|
||||
// Scroll to end
|
||||
nextTick(() => {
|
||||
if (scrollContainer.value) {
|
||||
scrollContainer.value.scrollLeft = scrollContainer.value.scrollWidth
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getLevelClass = (day) => {
|
||||
if (!day) {
|
||||
return 'invisible'
|
||||
}
|
||||
const count = day.count
|
||||
if (count === 0) {
|
||||
return 'level-0'
|
||||
}
|
||||
if (count <= thresholds.value[0]) {
|
||||
return 'level-1'
|
||||
}
|
||||
if (count <= thresholds.value[1]) {
|
||||
return 'level-2'
|
||||
}
|
||||
if (count <= thresholds.value[2]) {
|
||||
return 'level-3'
|
||||
}
|
||||
return 'level-4'
|
||||
}
|
||||
|
||||
const onCellClick = (day) => {
|
||||
if (day) {
|
||||
// Emit event or show toast
|
||||
// console.log(day);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh: fetchData
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.heatmap-card {
|
||||
background: var(--van-background-2);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
color: var(--van-text-color);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
margin: 0 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--van-border-color);
|
||||
}
|
||||
|
||||
.heatmap-scroll-container {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
scrollbar-width: none;
|
||||
flex: 1; /* Take remaining space */
|
||||
}
|
||||
.heatmap-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.heatmap-content {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.month-row {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.months-container {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
top: 0;
|
||||
color: var(--van-text-color-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.weekday-col-fixed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 19px; /* Align with cells (month row height 15px + margin 4px) */
|
||||
margin-right: 6px;
|
||||
font-size: 9px;
|
||||
height: 142px; /* Total height: 15 (month) + 4 (margin) + 123 (grid) */
|
||||
color: var(--van-text-color-2);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
background-color: var(--van-background-2); /* Match card background */
|
||||
}
|
||||
|
||||
.weekday-label {
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
margin-top: 15px; /* (15 cell + 3 gap)*2 - 15 height - previous margin? No. */
|
||||
/*
|
||||
Row 0: 0px top
|
||||
Row 1: 18px top (15+3) - Label "二" aligns here? No, "二" is usually row 1 (index 1, 2nd row)
|
||||
If we want to align with 2nd, 4th, 6th rows (indices 1, 3, 5):
|
||||
|
||||
Row 0: y=0
|
||||
Row 1: y=18
|
||||
Row 2: y=36
|
||||
Row 3: y=54
|
||||
Row 4: y=72
|
||||
Row 5: y=90
|
||||
Row 6: y=108
|
||||
|
||||
Label 1 ("二") at Row 1 (y=18)
|
||||
Label 2 ("四") at Row 3 (y=54)
|
||||
Label 3 ("六") at Row 5 (y=90)
|
||||
|
||||
Padding-top of container is 19px.
|
||||
First label margin-top: 18px
|
||||
Second label margin-top: (54 - (18+15)) = 21px
|
||||
Third label margin-top: (90 - (54+15)) = 21px
|
||||
|
||||
Let's try standard spacing.
|
||||
Gap between tops is 36px (2 rows).
|
||||
Height of label is 15px.
|
||||
Margin needed is 36 - 15 = 21px.
|
||||
|
||||
First label top needs to be at 18px relative to grid start.
|
||||
Container padding-top aligns with grid start (row 0 top).
|
||||
So first label margin-top should be 18px.
|
||||
*/
|
||||
margin-top: 21px;
|
||||
}
|
||||
.weekday-label:first-child {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.heatmap-grid {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.heatmap-week {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.heatmap-cell {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--van-gray-2);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.heatmap-cell.invisible {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.level-0 {
|
||||
background-color: var(--heatmap-level-0);
|
||||
}
|
||||
.level-1 {
|
||||
background-color: var(--heatmap-level-1);
|
||||
}
|
||||
.level-2 {
|
||||
background-color: var(--heatmap-level-2);
|
||||
}
|
||||
.level-3 {
|
||||
background-color: var(--heatmap-level-3);
|
||||
}
|
||||
.level-4 {
|
||||
background-color: var(--heatmap-level-4);
|
||||
}
|
||||
|
||||
.heatmap-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
color: var(--van-text-color-2);
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user