Files
likedream-web/src/views/dashboard/DashboardView.vue
T
sugarBecomer 18fcae8030 🎨 主题 v2:渐变按钮 + 4层语义色 + 降低饱和度
1. Primary 按钮改为渐变背景 linear-gradient(135deg, #6b8cff, #88a4ff)
2. 全局 primary 饱和度从 0.19 降至 0.12,更柔和
3. destructive 改为珊瑚色 oklch(0.70 0.14 25)
4. 新增 success/warning/info 三个语义色变量
5. Badge 新增 success/warning/info variant
6. 视图层硬编码 text-green/text-red/text-yellow 全部替换为语义 token
7. chart 颜色同步降低饱和度
2026-04-27 10:32:59 +08:00

254 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { cpTaskApi } from '@/api/cpdaily'
import { authApi } from '@/api/auth'
import { Card, CardContent } from '@/components/ui/card'
import { Wallet, TrendingUp, TrendingDown, BarChart3, CalendarCheck, Minus } from 'lucide-vue-next'
import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/preview.css'
import type { TaskStatisticsV2Item, DashboardSummary } from '@/types/cpdaily'
const userStore = useUserStore()
const router = useRouter()
const statistics = ref<TaskStatisticsV2Item[]>([])
const summary = ref<DashboardSummary | null>(null)
const loading = ref(false)
const announcement = ref('')
const announcementLoading = ref(true)
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 6) return '夜深了'
if (hour < 12) return '早上好'
if (hour < 14) return '中午好'
if (hour < 18) return '下午好'
return '晚上好'
})
const maxTaskCount = computed(() => {
if (statistics.value.length === 0) return 1
return Math.max(...statistics.value.map(s => Math.max(s.successTask + s.failTask + s.pendingTask, 1)))
})
const todayCostDiff = computed(() => {
if (!summary.value) return 0
return parseFloat(summary.value.todayCost) - parseFloat(summary.value.yesterdayCost)
})
const weekCostDiff = computed(() => {
if (!summary.value) return 0
return parseFloat(summary.value.week1Cost) - parseFloat(summary.value.week2Cost)
})
const taskCountDiff = computed(() => {
if (!summary.value) return 0
return summary.value.todayTaskCount - summary.value.ydayTaskCount
})
function formatDiff(val: number, isPrice = false) {
const abs = Math.abs(val)
const str = isPrice ? abs.toFixed(2) : String(abs)
if (val > 0) return `+${str}`
if (val < 0) return `-${str}`
return '0'
}
function formatDate(dateStr: string) {
const d = dateStr.slice(5) // "MM-DD"
return d
}
async function fetchStatistics() {
loading.value = true
try {
const res = await cpTaskApi.getStatisticsV2()
if (res.code === 200) statistics.value = res.data ?? []
} finally {
loading.value = false
}
}
async function fetchSummary() {
try {
const res = await cpTaskApi.getDashboardSummary()
if (res.code === 200) summary.value = res.data ?? null
} catch { /* ignore */ }
}
async function fetchAnnouncement() {
announcementLoading.value = true
try {
const res = await authApi.getPublicConfig('system.announcement')
if (res.code === 200 && res.data) announcement.value = res.data.value || ''
} finally {
announcementLoading.value = false
}
}
function goToTaskList(ids: number[] | undefined) {
if (!ids || ids.length === 0) return
router.push({ path: '/cpdaily/task/index', query: { ids: ids.join(',') } })
}
onMounted(() => {
fetchStatistics()
fetchSummary()
fetchAnnouncement()
})
</script>
<template>
<div class="space-y-4 sm:space-y-6">
<!-- 公告 -->
<Card v-if="announcementLoading">
<CardContent class="space-y-2">
<div class="h-4 w-3/4 rounded bg-muted animate-pulse" />
<div class="h-4 w-1/2 rounded bg-muted animate-pulse" />
</CardContent>
</Card>
<Card v-else-if="announcement">
<CardContent>
<MdPreview :modelValue="announcement" previewTheme="github" class="announcement-preview" />
</CardContent>
</Card>
<!-- 问候 -->
<div class="flex items-center gap-2">
<span class="text-lg sm:text-xl font-semibold">{{ greeting }},{{ userStore.profile?.username ?? '' }}</span>
</div>
<!-- 摘要卡片 -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-4">
<!-- 当前余额 -->
<Card>
<CardContent class="p-3 sm:p-4">
<div class="flex items-center gap-2 text-muted-foreground text-xs sm:text-sm mb-1">
<Wallet class="w-4 h-4" />
<span>当前余额</span>
</div>
<div class="text-lg sm:text-2xl font-bold">{{ summary?.balance ?? '--' }}</div>
</CardContent>
</Card>
<!-- 今日消费 -->
<Card>
<CardContent class="p-3 sm:p-4">
<div class="flex items-center gap-2 text-muted-foreground text-xs sm:text-sm mb-1">
<BarChart3 class="w-4 h-4" />
<span>今日消费</span>
</div>
<div class="text-lg sm:text-2xl font-bold">{{ summary?.todayCost ?? '--' }}</div>
<div v-if="summary" class="flex items-center gap-1 text-xs mt-1" :class="todayCostDiff > 0 ? 'text-destructive' : todayCostDiff < 0 ? 'text-success' : 'text-muted-foreground'">
<TrendingUp v-if="todayCostDiff > 0" class="w-3 h-3" />
<TrendingDown v-else-if="todayCostDiff < 0" class="w-3 h-3" />
<Minus v-else class="w-3 h-3" />
<span>较昨日 {{ formatDiff(todayCostDiff, true) }}</span>
</div>
</CardContent>
</Card>
<!-- 近7日消费 -->
<Card>
<CardContent class="p-3 sm:p-4">
<div class="flex items-center gap-2 text-muted-foreground text-xs sm:text-sm mb-1">
<BarChart3 class="w-4 h-4" />
<span>近7日消费</span>
</div>
<div class="text-lg sm:text-2xl font-bold">{{ summary?.week1Cost ?? '--' }}</div>
<div v-if="summary" class="flex items-center gap-1 text-xs mt-1" :class="weekCostDiff > 0 ? 'text-destructive' : weekCostDiff < 0 ? 'text-success' : 'text-muted-foreground'">
<TrendingUp v-if="weekCostDiff > 0" class="w-3 h-3" />
<TrendingDown v-else-if="weekCostDiff < 0" class="w-3 h-3" />
<Minus v-else class="w-3 h-3" />
<span>较上7日 {{ formatDiff(weekCostDiff, true) }}</span>
</div>
</CardContent>
</Card>
<!-- 今日任务数 -->
<Card>
<CardContent class="p-3 sm:p-4">
<div class="flex items-center gap-2 text-muted-foreground text-xs sm:text-sm mb-1">
<CalendarCheck class="w-4 h-4" />
<span>今日任务数</span>
</div>
<div class="text-lg sm:text-2xl font-bold">{{ summary?.todayTaskCount ?? '--' }}</div>
<div v-if="summary" class="flex items-center gap-1 text-xs mt-1" :class="taskCountDiff > 0 ? 'text-destructive' : taskCountDiff < 0 ? 'text-success' : 'text-muted-foreground'">
<TrendingUp v-if="taskCountDiff > 0" class="w-3 h-3" />
<TrendingDown v-else-if="taskCountDiff < 0" class="w-3 h-3" />
<Minus v-else class="w-3 h-3" />
<span>较昨日 {{ formatDiff(taskCountDiff) }}</span>
</div>
</CardContent>
</Card>
</div>
<!-- 近7日任务统计 -->
<Card>
<CardContent class="p-3 sm:p-6">
<div class="flex items-center justify-between mb-4">
<span class="font-semibold text-sm sm:text-base">近7日任务统计</span>
<div class="flex items-center gap-3 text-xs">
<span class="flex items-center gap-1"><span class="w-2.5 h-2.5 rounded-sm bg-primary inline-block"></span>成功</span>
<span class="flex items-center gap-1"><span class="w-2.5 h-2.5 rounded-sm bg-destructive inline-block"></span>失败</span>
<span class="flex items-center gap-1"><span class="w-2.5 h-2.5 rounded-sm bg-muted-foreground/30 inline-block"></span>待执行</span>
</div>
</div>
<div v-if="loading" class="flex items-center justify-center h-40 text-muted-foreground text-sm">加载中...</div>
<div v-else-if="statistics.length === 0" class="flex items-center justify-center h-40 text-muted-foreground text-sm">暂无数据</div>
<div v-else class="space-y-2">
<div v-for="item in statistics" :key="item.date" class="flex items-center gap-2 sm:gap-3">
<span class="text-xs text-muted-foreground w-12 sm:w-14 shrink-0 text-right">{{ formatDate(item.date) }}</span>
<div class="flex-1 flex h-5 sm:h-6 rounded overflow-hidden bg-muted/30">
<div
v-if="item.successTask > 0"
class="bg-primary transition-all cursor-pointer hover:opacity-80"
:style="{ width: (item.successTask / maxTaskCount * 100) + '%' }"
:title="`成功: ${item.successTask} — 点击查看`"
@click="goToTaskList(item.successTaskIds)"
></div>
<div
v-if="item.failTask > 0"
class="bg-destructive transition-all cursor-pointer hover:opacity-80"
:style="{ width: (item.failTask / maxTaskCount * 100) + '%' }"
:title="`失败: ${item.failTask} — 点击查看`"
@click="goToTaskList(item.failTaskIds)"
></div>
<div
v-if="item.pendingTask > 0"
class="bg-muted-foreground/30 transition-all cursor-pointer hover:opacity-80"
:style="{ width: (item.pendingTask / maxTaskCount * 100) + '%' }"
:title="`待执行: ${item.pendingTask} — 点击查看`"
@click="goToTaskList(item.pendingTaskIds)"
></div>
</div>
<span class="text-xs text-muted-foreground w-24 sm:w-28 shrink-0 flex gap-0.5">
<span class="cursor-pointer text-primary hover:underline" @click="goToTaskList(item.successTaskIds)">{{ item.successTask }}</span>
<span>/</span>
<span class="cursor-pointer text-destructive hover:underline" @click="goToTaskList(item.failTaskIds)">{{ item.failTask }}</span>
<span>/</span>
<span class="cursor-pointer hover:underline" @click="goToTaskList(item.pendingTaskIds)">{{ item.pendingTask }}</span>
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</template>
<style scoped>
.announcement-preview :deep(.md-editor-preview-wrapper) {
padding: 0;
background: transparent;
}
.announcement-preview :deep(.md-editor-preview) {
font-size: 0.875rem;
background: transparent;
}
.announcement-preview :deep(.md-editor-preview .md-editor-admonition) {
margin: 0;
}
</style>