yunzer_go/pc/src/views/dashboard/index.vue

737 lines
17 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.

<template>
<div class="dashboard">
<!-- 欢迎区域 -->
<div class="welcome-section">
<div class="welcome-content">
<h1 class="welcome-title">欢迎回来</h1>
<p class="welcome-subtitle">今天是 {{ currentDate }}祝您工作愉快</p>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div
v-for="(stat, index) in stats"
:key="index"
class="stat-card"
:class="stat.type"
>
<div class="stat-icon-wrapper">
<el-icon :size="28">
<component :is="stat.icon" />
</el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
<div
class="stat-trend"
:class="stat.change > 0 ? 'up' : stat.change < 0 ? 'down' : 'flat'"
>
<el-icon v-if="stat.change > 0" :size="14"><ArrowUp /></el-icon>
<el-icon v-else-if="stat.change < 0" :size="14"><ArrowDown /></el-icon>
<span>{{ stat.change > 0 ? '+' : '' }}{{ stat.change }}%</span>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<div class="chart-card">
<div class="card-header">
<h3 class="card-title">月收入走势</h3>
<el-dropdown trigger="click">
<el-button type="primary" link>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>查看详情</el-dropdown-item>
<el-dropdown-item>导出数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="chart-container">
<canvas id="lineChart" height="160"></canvas>
</div>
</div>
<div class="chart-card">
<div class="card-header">
<h3 class="card-title">用户活跃分布</h3>
<el-dropdown trigger="click">
<el-button type="primary" link>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>查看详情</el-dropdown-item>
<el-dropdown-item>导出数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="chart-container">
<canvas id="barChart" height="160"></canvas>
</div>
</div>
</div>
<!-- 列表区域 -->
<div class="lists-section">
<div class="list-card">
<div class="card-header">
<h3 class="card-title">待办任务</h3>
<el-button type="primary" link size="small">
<el-icon><Plus /></el-icon>
添加任务
</el-button>
</div>
<div class="list-content">
<div
v-for="(task, idx) in tasks"
:key="idx"
class="task-item"
:class="{ done: task.completed }"
>
<el-checkbox
v-model="task.completed"
@change="handleTaskChange(task)"
/>
<div class="task-info">
<div class="task-title">{{ task.title }}</div>
<div class="task-meta">
<span class="task-date">{{ task.date }}</span>
<el-tag
:type="getPriorityType(task.priority)"
size="small"
effect="plain"
>
{{ task.priority }}
</el-tag>
</div>
</div>
</div>
<el-empty
v-if="tasks.filter(t => !t.completed).length === 0"
description="暂无待办任务"
:image-size="80"
/>
</div>
</div>
<div class="list-card">
<div class="card-header">
<h3 class="card-title">最新动态</h3>
<el-button type="primary" link size="small">查看全部</el-button>
</div>
<div class="list-content">
<div
v-for="(activity, idx) in activities"
:key="idx"
class="activity-item"
>
<el-avatar :src="activity.avatar" :size="40" />
<div class="activity-info">
<div class="activity-text">
<span class="activity-user">{{ activity.user }}</span>
<span class="activity-action">{{ activity.action }}</span>
</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
<el-empty
v-if="activities.length === 0"
description="暂无动态"
:image-size="80"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { Chart, registerables } from "chart.js";
import {
Money,
User,
ShoppingCart,
TrendCharts,
ArrowUp,
ArrowDown,
MoreFilled,
Plus,
} from "@element-plus/icons-vue";
Chart.register(...registerables);
// 当前日期
const currentDate = computed(() => {
const date = new Date();
const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
const month = date.getMonth() + 1;
const day = date.getDate();
const weekday = weekdays[date.getDay()];
return `${month}${day}${weekday}`;
});
// 统计数据
const stats = ref([
{
label: "本月收入",
value: "¥ 35,800",
change: 10.4,
icon: Money,
type: "income",
},
{
label: "新用户",
value: "842",
change: 3.7,
icon: User,
type: "users",
},
{
label: "订单量",
value: "1,376",
change: -1.6,
icon: ShoppingCart,
type: "orders",
},
{
label: "活跃率",
value: "27.3%",
change: 2.2,
icon: TrendCharts,
type: "active",
},
]);
// 任务列表
const tasks = ref([
{
title: "完成Q2预算审核",
date: "2024-06-11",
priority: "High",
completed: false,
},
{
title: "发邮件通知团队",
date: "2024-06-10",
priority: "Medium",
completed: true,
},
{
title: "产品需求讨论会",
date: "2024-06-09",
priority: "Low",
completed: false,
},
]);
// 动态记录
const activities = ref([
{
user: "Emma",
action: "添加了新用户",
time: "1 小时前",
avatar: "https://picsum.photos/id/1027/40/40",
},
{
user: "John",
action: "修改了高级权限",
time: "3 小时前",
avatar: "https://picsum.photos/id/1012/40/40",
},
{
user: "Jessica",
action: "完成订单分析报表",
time: "昨天",
avatar: "https://picsum.photos/id/1000/40/40",
},
]);
// 获取优先级类型
const getPriorityType = (priority: string) => {
const map: Record<string, string> = {
High: "danger",
Medium: "warning",
Low: "success",
};
return map[priority] || "info";
};
// 任务状态改变
const handleTaskChange = (task: any) => {
console.log("Task changed:", task);
};
// 初始化图表
onMounted(() => {
// 折线图
new Chart(document.getElementById("lineChart") as HTMLCanvasElement, {
type: "line",
data: {
labels: ["1月", "2月", "3月", "4月", "5月", "6月", "7月"],
datasets: [
{
label: "收入(元)",
data: [16800, 19400, 23100, 24600, 27600, 31000, 35800],
fill: true,
borderColor: "#4f84ff",
backgroundColor: "rgba(79, 132, 255, 0.1)",
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: "#4f84ff",
pointBorderColor: "#fff",
pointBorderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: 12,
titleFont: { size: 14 },
bodyFont: { size: 13 },
cornerRadius: 6,
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: "var(--el-text-color-regular)",
font: { size: 12 },
},
grid: {
color: "var(--el-border-color-lighter)",
drawBorder: false,
},
},
x: {
ticks: {
color: "var(--el-text-color-regular)",
font: { size: 12 },
},
grid: {
display: false,
},
},
},
},
});
// 柱状图
new Chart(document.getElementById("barChart") as HTMLCanvasElement, {
type: "bar",
data: {
labels: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
datasets: [
{
label: "活跃用户",
data: [140, 162, 189, 176, 206, 238, 205],
backgroundColor: "rgba(79, 132, 255, 0.8)",
borderRadius: 6,
barPercentage: 0.6,
borderSkipped: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: 12,
titleFont: { size: 14 },
bodyFont: { size: 13 },
cornerRadius: 6,
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: "var(--el-text-color-regular)",
font: { size: 12 },
},
grid: {
color: "var(--el-border-color-lighter)",
drawBorder: false,
},
},
x: {
ticks: {
color: "var(--el-text-color-regular)",
font: { size: 12 },
},
grid: {
display: false,
},
},
},
},
});
});
</script>
<style lang="less" scoped>
.dashboard {
padding: 24px;
min-height: 100%;
background-color: var(--el-bg-color-page);
}
// 欢迎区域
.welcome-section {
margin-bottom: 24px;
padding: 32px;
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(6, 45, 163, 0.2);
.welcome-content {
.welcome-title {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: #fff;
}
.welcome-subtitle {
margin: 0;
font-size: 15px;
color: rgba(255, 255, 255, 0.9);
}
}
}
// 统计卡片
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
.stat-card {
background: var(--el-bg-color);
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.3s;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: var(--el-color-primary-light-7);
}
.stat-icon-wrapper {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.el-icon {
color: #fff;
}
}
&.income .stat-icon-wrapper {
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
}
&.users .stat-icon-wrapper {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
}
&.orders .stat-icon-wrapper {
background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
}
&.active .stat-icon-wrapper {
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
}
.stat-content {
flex: 1;
min-width: 0;
.stat-value {
font-size: 28px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: var(--el-text-color-regular);
}
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
padding: 4px 8px;
border-radius: 6px;
&.up {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
&.down {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
&.flat {
color: var(--el-text-color-placeholder);
background: var(--el-fill-color-lighter);
}
}
}
}
// 图表区域
.charts-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 24px;
.chart-card {
background: var(--el-bg-color);
border-radius: 12px;
padding: 24px;
border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.chart-container {
height: 280px;
position: relative;
}
}
}
// 列表区域
.lists-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
.list-card {
background: var(--el-bg-color);
border-radius: 12px;
padding: 24px;
border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.list-content {
.task-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--el-fill-color-lighter);
margin: 0 -24px;
padding-left: 24px;
padding-right: 24px;
border-radius: 8px;
}
&.done {
.task-info {
.task-title {
text-decoration: line-through;
color: var(--el-text-color-placeholder);
opacity: 0.7;
}
}
}
.task-info {
flex: 1;
min-width: 0;
.task-title {
font-size: 14px;
color: var(--el-text-color-primary);
margin-bottom: 6px;
font-weight: 500;
}
.task-meta {
display: flex;
align-items: center;
gap: 8px;
.task-date {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--el-fill-color-lighter);
margin: 0 -24px;
padding-left: 24px;
padding-right: 24px;
border-radius: 8px;
}
.activity-info {
flex: 1;
min-width: 0;
.activity-text {
font-size: 14px;
color: var(--el-text-color-primary);
margin-bottom: 4px;
.activity-user {
font-weight: 600;
color: var(--el-text-color-primary);
}
.activity-action {
color: var(--el-text-color-regular);
}
}
.activity-time {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
}
}
// 响应式
@media (max-width: 1200px) {
.charts-section,
.lists-section {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.dashboard {
padding: 16px;
}
.welcome-section {
padding: 24px 16px;
.welcome-title {
font-size: 24px;
}
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
padding: 16px;
.stat-value {
font-size: 24px;
}
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>