feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。
热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
This commit is contained in:
@@ -1,12 +1,36 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({ days: { type: Array, default: () => [] }, selected: String })
|
||||
const emit = defineEmits(['select'])
|
||||
const { locale } = useI18n()
|
||||
const { locale, t } = useI18n()
|
||||
const root = ref(null)
|
||||
const hover = ref(null)
|
||||
const weekMs = 7 * 24 * 60 * 60 * 1000
|
||||
const cell = ref(13)
|
||||
const gap = ref(3)
|
||||
let ro
|
||||
|
||||
function ymd(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const zhMonths = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
const enMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
|
||||
function monthText(m) {
|
||||
return locale.value === 'zh-CN' ? zhMonths[m] : enMonths[m]
|
||||
}
|
||||
|
||||
const weekdays = computed(() => [
|
||||
t('weekdays.sun'),
|
||||
t('weekdays.mon'),
|
||||
t('weekdays.tue'),
|
||||
t('weekdays.wed'),
|
||||
t('weekdays.thu'),
|
||||
t('weekdays.fri'),
|
||||
t('weekdays.sat'),
|
||||
])
|
||||
|
||||
const matrix = computed(() => {
|
||||
const end = new Date()
|
||||
@@ -16,50 +40,98 @@ const matrix = computed(() => {
|
||||
start.setDate(start.getDate() - start.getDay())
|
||||
const counts = new Map(props.days.map(x => [x.date, x]))
|
||||
const cells = []
|
||||
let week = 0
|
||||
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||
const date = d.toISOString().slice(0, 10)
|
||||
const date = ymd(d)
|
||||
const day = counts.get(date)
|
||||
const count = Number(day?.count || 0)
|
||||
cells.push({ date, count, added: Number(day?.added || 0), deleted: Number(day?.deleted || 0), week: Math.floor((d - start) / weekMs), dow: d.getDay(), level: Math.min(4, count === 0 ? 0 : count < 2 ? 1 : count < 5 ? 2 : count < 10 ? 3 : 4) })
|
||||
cells.push({ date, count, added: Number(day?.added || 0), deleted: Number(day?.deleted || 0), week, dow: d.getDay(), level: Math.min(4, count === 0 ? 0 : count < 2 ? 1 : count < 5 ? 2 : count < 10 ? 3 : 4) })
|
||||
if (d.getDay() === 6) week++
|
||||
}
|
||||
const weeks = Math.max(1, ...cells.map(x => x.week + 1))
|
||||
const labels = []
|
||||
let lastMonth = -1
|
||||
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 7)) {
|
||||
const m = d.getMonth()
|
||||
if (m !== lastMonth) {
|
||||
labels.push({ week: Math.floor((d - start) / weekMs), text: d.toLocaleString(locale.value === 'zh-CN' ? 'zh-CN' : 'en', { month: 'short' }) })
|
||||
lastMonth = m
|
||||
for (const item of cells) {
|
||||
const m = Number(item.date.slice(5, 7)) - 1
|
||||
if (m === lastMonth) continue
|
||||
const prev = labels[labels.length - 1]
|
||||
if (prev && prev.week === item.week) {
|
||||
prev.text = monthText(m)
|
||||
} else {
|
||||
if (prev) prev.span = Math.max(1, item.week - prev.week)
|
||||
labels.push({ week: item.week, span: 1, text: monthText(m) })
|
||||
}
|
||||
lastMonth = m
|
||||
}
|
||||
return { cells, labels, weeks: Math.max(53, ...cells.map(x => x.week + 1)) }
|
||||
if (labels.length) labels[labels.length - 1].span = Math.max(1, weeks - labels[labels.length - 1].week)
|
||||
return { cells, labels, weeks }
|
||||
})
|
||||
|
||||
function pick(cell) {
|
||||
if (!cell.date) return
|
||||
emit('select', cell.date === props.selected ? '' : cell.date)
|
||||
const monthLabels = computed(() => {
|
||||
const unit = cell.value + gap.value
|
||||
const total = matrix.value.weeks * unit - gap.value
|
||||
return matrix.value.labels.map((m, i, arr) => {
|
||||
const next = arr[i + 1]
|
||||
let left = m.week * unit
|
||||
let width = Math.max(24, (next ? next.week * unit : total) - left)
|
||||
if (left + width > total) {
|
||||
width = Math.min(width, total)
|
||||
left = Math.max(0, total - width)
|
||||
}
|
||||
return { week: m.week, text: m.text, left, width }
|
||||
})
|
||||
})
|
||||
|
||||
function fit() {
|
||||
const el = root.value
|
||||
if (!el) return
|
||||
const weeks = matrix.value.weeks || 53
|
||||
const avail = Math.max(0, el.clientWidth - 34)
|
||||
const nextGap = avail < 720 ? 2 : 3
|
||||
const nextCell = Math.floor((avail - (weeks - 1) * nextGap) / weeks)
|
||||
gap.value = nextGap
|
||||
cell.value = Math.max(4, Math.min(14, nextCell > 0 ? nextCell : 4))
|
||||
}
|
||||
|
||||
function pick(item) {
|
||||
if (!item.date) return
|
||||
emit('select', item.date === props.selected ? '' : item.date)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fit()
|
||||
ro = new ResizeObserver(fit)
|
||||
if (root.value) ro.observe(root.value)
|
||||
})
|
||||
onUnmounted(() => ro?.disconnect())
|
||||
watch(() => matrix.value.weeks, fit)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="heat-grid-wrap" :style="{ '--weeks': matrix.weeks }">
|
||||
<div ref="root" class="heat-grid-wrap" :style="{ '--weeks': matrix.weeks, '--cell': cell + 'px', '--gap': gap + 'px' }">
|
||||
<div class="heat-months">
|
||||
<span v-for="m in matrix.labels" :key="m.week + m.text" :style="{ gridColumnStart: m.week + 1 }">{{ m.text }}</span>
|
||||
<span v-for="m in monthLabels" :key="m.week + m.text" :style="{ left: m.left + 'px', width: m.width + 'px' }">{{ m.text }}</span>
|
||||
</div>
|
||||
<div class="heat-body">
|
||||
<div class="heat-weekdays"><span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span></div>
|
||||
<div class="heat-weekdays"><span v-for="(d, i) in weekdays" :key="i">{{ d }}</span></div>
|
||||
<div class="heat-grid">
|
||||
<button
|
||||
v-for="cell in matrix.cells"
|
||||
:key="cell.date"
|
||||
v-for="item in matrix.cells"
|
||||
:key="item.date"
|
||||
type="button"
|
||||
class="heat-cell"
|
||||
:class="['l' + cell.level, { selected: cell.date === props.selected }]"
|
||||
:style="{ gridColumnStart: cell.week + 1, gridRowStart: cell.dow + 1 }"
|
||||
@mouseenter="hover = cell"
|
||||
:class="['l' + item.level, { selected: item.date === selected }]"
|
||||
:style="{ gridColumnStart: item.week + 1, gridRowStart: item.dow + 1 }"
|
||||
:title="item.date"
|
||||
@mouseenter="hover = item"
|
||||
@mouseleave="hover = null"
|
||||
@click="pick(cell)"
|
||||
@click="pick(item)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hover" class="heat-tooltip">{{ hover.date }} · {{ hover.count }} {{ locale === 'zh-CN' ? '次提交' : 'commits' }}<template v-if="hover.count"> · <b class="positive">+{{ hover.added }}</b> <b class="negative">-{{ hover.deleted }}</b></template></div>
|
||||
</div>
|
||||
<p class="heat-tip">
|
||||
<template v-if="hover">{{ hover.date }} · {{ hover.count }} {{ t('commitsUnit') }}<template v-if="hover.count"> · <b class="positive">+{{ hover.added }}</b> <b class="negative">-{{ hover.deleted }}</b></template></template>
|
||||
<template v-else>{{ t('heatHint') }}</template>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user