mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
fix(frontend): complete i18n coverage and responsive layouts
This commit is contained in:
@@ -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(' · ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user