fix(frontend): complete i18n coverage and responsive layouts

This commit is contained in:
elky
2026-09-07 08:54:19 +08:00
parent 14f96c9fa0
commit b599fb7354
78 changed files with 4809 additions and 1485 deletions
@@ -4,26 +4,27 @@
<Teleport to="body">
<div
v-if="tooltip.visible && tooltip.day"
class="fixed z-50 rounded-lg border border-border/70 bg-background px-3 py-2 text-xs shadow-lg backdrop-blur pointer-events-none"
ref="tooltipRef"
class="fixed z-50 w-[200px] max-w-[calc(100vw-1rem)] break-words rounded-lg border border-border/70 bg-background px-3 py-2 text-xs shadow-lg backdrop-blur pointer-events-none"
:style="tooltipStyle"
>
<p class="font-medium">
{{ tooltip.day.date }}
{{ formatDay(tooltip.day.date) }}
</p>
<p class="mt-0.5">
{{ tooltip.day.requests }} 次请求 · {{ formatTokens(tooltip.day.total_tokens) }}
{{ t('heatmap.requests', { count: tooltip.day.requests }) }} · {{ formatTokens(tooltip.day.total_tokens) }}
</p>
<p class="text-[11px] text-muted-foreground">
成本 {{ formatCurrency(tooltip.day.total_cost) }}
{{ t('heatmap.cost', { value: formatCurrency(tooltip.day.total_cost) }) }}
</p>
</div>
</Teleport>
<div
v-if="showHeader"
class="flex items-center justify-between gap-4"
class="flex flex-wrap items-center justify-between gap-4"
>
<div class="flex-shrink-0">
<div class="min-w-0 break-words">
<p class="text-sm font-semibold">
{{ title }}
</p>
@@ -38,20 +39,20 @@
v-if="weekColumns.length > 0"
class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0"
>
<span class="flex-shrink-0">少</span>
<span class="flex-shrink-0">{{ t('heatmap.less') }}</span>
<div
v-for="(level, index) in legendLevels"
:key="index"
class="w-3 h-3 rounded-[3px] flex-shrink-0"
:style="getLegendStyle(level)"
/>
<span class="flex-shrink-0">多</span>
<span class="flex-shrink-0">{{ t('heatmap.more') }}</span>
</div>
</div>
<div
v-if="weekColumns.length > 0"
class="flex w-full gap-3"
class="flex w-full gap-3 overflow-x-auto"
>
<div
class="flex flex-col text-[10px] text-muted-foreground flex-shrink-0"
@@ -62,33 +63,12 @@
M
</div>
<span
:style="dayLabelStyle"
class="flex items-center invisible"
>周日</span>
<span
v-for="(weekday, index) in weekdayLabels"
:key="index"
:style="dayLabelStyle"
class="flex items-center"
>一</span>
<span
:style="dayLabelStyle"
class="flex items-center invisible"
>周二</span>
<span
:style="dayLabelStyle"
class="flex items-center"
>三</span>
<span
:style="dayLabelStyle"
class="flex items-center invisible"
>周四</span>
<span
:style="dayLabelStyle"
class="flex items-center"
>五</span>
<span
:style="dayLabelStyle"
class="flex items-center invisible"
>周六</span>
:class="{ invisible: index % 2 === 0 }"
>{{ weekday }}</span>
</div>
<div class="flex-1 min-w-[200px]">
<div
@@ -103,7 +83,7 @@
v-for="(week, weekIndex) in weekColumns"
:key="`month-${weekIndex}`"
:style="monthCellStyle"
class="text-center"
class="whitespace-nowrap text-left"
>
<span v-if="monthMarkers[weekIndex]">{{ monthMarkers[weekIndex] }}</span>
</div>
@@ -146,15 +126,16 @@
v-else
class="text-xs text-muted-foreground"
>
暂无活跃数据
{{ t('heatmap.empty') }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ActivityHeatmap, ActivityHeatmapDay } from '@/types/activity'
import { formatCurrency, formatTokens } from '@/utils/format'
import { useI18n } from '@/i18n'
const props = withDefaults(defineProps<{
data?: ActivityHeatmap | null
@@ -169,12 +150,23 @@ const props = withDefaults(defineProps<{
})
const legendLevels = [0.08, 0.25, 0.45, 0.65, 0.85]
const { locale, t } = useI18n()
const weekdayLabels = computed(() => {
const formatter = new Intl.DateTimeFormat(locale.value, { weekday: 'short', timeZone: 'UTC' })
return Array.from({ length: 7 }, (_, day) => formatter.format(new Date(Date.UTC(2024, 0, 7 + day))))
})
function formatDay(value: string): string {
return new Intl.DateTimeFormat(locale.value, { dateStyle: 'medium', timeZone: 'UTC' })
.format(new Date(`${value}T00:00:00Z`))
}
type DayWithMeta = ActivityHeatmapDay & { dateObj: Date }
const heatmapWrapper = ref<HTMLElement | null>(null)
const heatmapWidth = ref(0)
const cellSize = ref(10)
const cellGap = ref(4)
const tooltipRef = ref<HTMLElement | null>(null)
const tooltip = ref<{ day: ActivityHeatmapDay | null; x: number; y: number; visible: boolean; below: boolean }>({
day: null,
x: 0,
@@ -259,6 +251,7 @@ const weekColumns = computed(() => {
const monthMarkers = computed(() => {
const markers: Record<number, string> = {}
const columns = weekColumns.value
const formatter = new Intl.DateTimeFormat(locale.value, { month: 'short', timeZone: 'UTC' })
let lastMonth: number | null = null
columns.forEach((week, index) => {
@@ -270,7 +263,7 @@ const monthMarkers = computed(() => {
if (month === lastMonth) {
return
}
markers[index] = `${month + 1}月`
markers[index] = formatter.format(firstValid.dateObj)
lastMonth = month
})
@@ -343,10 +336,14 @@ onBeforeUnmount(() => {
}
})
function handleHover(day: ActivityHeatmapDay, event: MouseEvent) {
async function handleHover(day: ActivityHeatmapDay, event: MouseEvent) {
const cellRect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const tooltipWidth = 200
const tooltipHeight = 72
tooltip.value = { day, x: cellRect.left, y: cellRect.top, visible: true, below: false }
await nextTick()
if (!tooltip.value.visible || tooltip.value.day?.date !== day.date) return
const tooltipWidth = tooltipRef.value?.offsetWidth || 200
const tooltipHeight = tooltipRef.value?.offsetHeight || 72
// Calculate horizontal position (centered on cell)
let left = cellRect.left + cellRect.width / 2
@@ -401,11 +398,11 @@ function getCellStyle(requests: number) {
}
function buildTooltip(day: ActivityHeatmapDay): string {
const dateLabel = day.date
const dateLabel = formatDay(day.date)
const costLabel = formatCurrency(day.total_cost || 0)
const parts = [`${dateLabel}`, `${day.requests} 次请求`, `${formatTokens(day.total_tokens)} tokens`, costLabel]
const parts = [dateLabel, t('heatmap.requests', { count: day.requests }), `${formatTokens(day.total_tokens)} tokens`, costLabel]
if (day.actual_total_cost !== undefined) {
parts.push(`倍率: ${formatCurrency(day.actual_total_cost)}`)
parts.push(t('heatmap.actualCost', { value: formatCurrency(day.actual_total_cost) }))
}
return parts.join(' · ')
}