mirror of
https://github.com/RhenCloud/Cloud-Home.git
synced 2026-01-22 09:29:06 +08:00
320 lines
7.3 KiB
Vue
320 lines
7.3 KiB
Vue
<template>
|
|
<section v-if="showComponent && (weeklyData || allTimeData)" class="card">
|
|
<div class="header">
|
|
<h2>Wakatime</h2>
|
|
<div class="tabs">
|
|
<button
|
|
class="tab-button"
|
|
:class="{ active: activeTab === 'weekly' }"
|
|
@click="activeTab = 'weekly'"
|
|
>
|
|
最近7天
|
|
</button>
|
|
<button
|
|
v-if="allTimeData"
|
|
class="tab-button"
|
|
:class="{ active: activeTab === 'allTime' }"
|
|
@click="activeTab = 'allTime'"
|
|
>
|
|
所有时间
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-wrap">
|
|
<h3>编码统计</h3>
|
|
<p class="muted">
|
|
{{ activeTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }}
|
|
</p>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<span class="stat-value">{{
|
|
currentData.total_seconds ? formatTime(currentData.total_seconds) : "N/A"
|
|
}}</span>
|
|
<span class="stat-label">总时间</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">{{
|
|
currentData.daily_average ? formatTime(currentData.daily_average) : "N/A"
|
|
}}</span>
|
|
<span class="stat-label">日均</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">{{ currentData.days_including_holidays || "N/A" }}</span>
|
|
<span class="stat-label">活跃天数</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="currentData.languages && currentData.languages.length" class="lang-wrap">
|
|
<h3>编程语言</h3>
|
|
<p class="muted">语言使用统计 · Languages</p>
|
|
<div class="lang-chart">
|
|
<ul class="list lang-list">
|
|
<li v-for="lang in topLanguages" :key="lang.name" class="lang-row">
|
|
<div class="lang-label">
|
|
<span class="dot" :style="{ background: colorFor(lang.name) }" />
|
|
<span class="lang-name">{{ lang.name }}</span>
|
|
<span class="lang-percent">{{ lang.percent }}%</span>
|
|
</div>
|
|
<div class="lang-bar">
|
|
<span class="lang-bar-fill" :style="barStyle(lang)" />
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="statusData" class="status-wrap">
|
|
<h3>当前状态</h3>
|
|
<p class="muted">实时状态 · Current Status</p>
|
|
<div class="status-item">
|
|
<span class="status-indicator" :class="{ active: statusData.is_coding }" />
|
|
<span class="status-text">{{ statusData.is_coding ? "正在编码" : "未在编码" }}</span>
|
|
<span v-if="statusData.project" class="status-project">{{ statusData.project }}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from "vue";
|
|
import siteConfig from "~/config/siteConfig";
|
|
|
|
const wakapi = siteConfig.wakapi;
|
|
|
|
const weeklyData = ref(null);
|
|
const allTimeData = ref(null);
|
|
const statusData = ref(null);
|
|
const showComponent = ref(true);
|
|
const activeTab = ref("weekly");
|
|
|
|
const palette = ["#7cc1ff", "#6bdba6", "#ffd166", "#201a1fff", "#9b8cfc", "#5ce1e6", "#ffa3a3"];
|
|
|
|
const currentData = computed(() => {
|
|
return activeTab.value === "weekly" ? weeklyData.value : allTimeData.value;
|
|
});
|
|
|
|
const topLanguages = computed(() => {
|
|
if (!currentData.value || !currentData.value.languages) return [];
|
|
return currentData.value.languages.slice(0, 5);
|
|
});
|
|
|
|
const colorFor = (name) => {
|
|
const idx = currentData.value.languages.findIndex((l) => l.name === name);
|
|
return palette[(idx >= 0 ? idx : 0) % palette.length];
|
|
};
|
|
|
|
const barStyle = (lang) => ({
|
|
width: `${Math.max(8, lang.percent)}%`,
|
|
background: colorFor(lang.name),
|
|
});
|
|
|
|
const formatTime = (seconds) => {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
return `${hours}h ${minutes}m`;
|
|
};
|
|
|
|
const fetchWakatimeData = async () => {
|
|
if (!wakapi.enable) {
|
|
console.warn("Wakatime is disabled in siteConfig.");
|
|
return;
|
|
}
|
|
|
|
const apiUrl = wakapi.apiUrl || "https://wakatime.com/api/v1";
|
|
const username = wakapi.username;
|
|
|
|
if (!username) {
|
|
console.error("Wakatime username is not configured.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const [weeklyStatsResponse, allTimeStatsResponse, statusResponse] = await Promise.all([
|
|
fetch(`${apiUrl}/users/${username}/stats/last_7_days`),
|
|
fetch(`${apiUrl}/users/${username}/stats`),
|
|
fetch(`${apiUrl}/users/${username}/status`),
|
|
]);
|
|
|
|
if (weeklyStatsResponse.ok) {
|
|
weeklyData.value = await weeklyStatsResponse.json();
|
|
} else {
|
|
console.error("Failed to fetch weekly stats:", weeklyStatsResponse.status);
|
|
}
|
|
|
|
if (allTimeStatsResponse.ok) {
|
|
allTimeData.value = await allTimeStatsResponse.json();
|
|
} else {
|
|
console.warn("All-time stats not available:", allTimeStatsResponse.status);
|
|
}
|
|
|
|
if (statusResponse.ok) {
|
|
statusData.value = await statusResponse.json();
|
|
} else {
|
|
console.warn("Status data not available:", statusResponse.status);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching Wakatime data:", error);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
fetchWakatimeData();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.tab-button {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid var(--border);
|
|
background: var(--card-bg);
|
|
color: var(--text);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.tab-button:hover {
|
|
background: var(--hover-bg);
|
|
}
|
|
|
|
.tab-button.active {
|
|
background: var(--accent);
|
|
color: white;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.stat-value {
|
|
display: block;
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: var(--text);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.875rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.status-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 1rem;
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.status-indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
}
|
|
|
|
.status-indicator.active {
|
|
background: #6bdba6;
|
|
}
|
|
|
|
.status-text {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-project {
|
|
color: var(--muted);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.lang-wrap {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.lang-chart {
|
|
display: block;
|
|
}
|
|
|
|
.lang-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.lang-row {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 10px;
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.lang-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.lang-name {
|
|
color: #e8eefc;
|
|
}
|
|
|
|
.lang-percent {
|
|
color: #a8b3cf;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.lang-bar {
|
|
margin-top: 6px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.lang-bar-fill {
|
|
display: block;
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.dot {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 6px;
|
|
}
|
|
</style>
|