18fcae8030
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 颜色同步降低饱和度
254 lines
10 KiB
Vue
254 lines
10 KiB
Vue
<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>
|