backend/src/views/dashboard/index.vue
2026-01-27 18:01:54 +08:00

791 lines
18 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 paginatedTasks"
: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 }}
<el-tag
:type="task.priorityTagType || getPriorityType(task.priority)"
size="small"
effect="plain"
:class="'priority-' + task.priority"
>
{{ task.priorityLabel || task.priority }}
</el-tag>
</div>
<div class="task-meta">
<span class="task-date">
<el-icon><Clock /></el-icon>
{{ formatDate(task.date) }}
</span>
</div>
</div>
</div>
<el-empty
description="暂无待办任务"
:image-size="80"
/>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="taskCurrentPage"
:page-size="taskPageSize"
layout="prev, pager, next"
size="small"
@current-change="handleTaskPageChange"
/>
</div>
</div>
</div>
<div class="list-card">
<div class="card-header">
<h3 class="card-title">最新动态</h3>
<!-- <el-button type="primary" link size="small" @click="goToActivityLogs">
查看全部
</el-button> -->
</div>
<div class="list-content">
<div
v-for="(activity, idx) in paginatedActivityLogs"
:key="idx"
class="activity-item"
>
<div class="activity-icon" :class="activity.type">
<el-icon>
<component :is="getActivityIcon(activity.type)" />
</el-icon>
</div>
<div class="activity-info">
<div class="activity-text">
<span class="activity-module">{{ activity.operation }}</span>
<span class="activity-action">{{ activity.description }}</span>
</div>
<div class="activity-time">
{{ formatTime(activity.timestamp) }}
</div>
</div>
</div>
<el-empty
description="暂无动态"
:image-size="80"
/>
<div v-if="totalActivityLogs > pageSize" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="totalActivityLogs"
layout="prev, pager, next"
size="small"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, markRaw } from "vue";
import { Chart, registerables } from "chart.js";
import {
Money,
User,
ShoppingCart,
TrendCharts,
ArrowUp,
ArrowDown,
MoreFilled,
Plus,
Document,
Edit,
View,
} from "@element-plus/icons-vue";
import { getMenus } from "@/api/menu";
import { useAuthStore } from "@/stores/auth";
import { useDictStore } from "@/stores/dict";
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 formatDate = (date: string) => {
const d = new Date(date);
const month = d.getMonth() + 1;
const day = d.getDate();
return `${month}${day}`;
};
// 统计数据(使用 markRaw 避免图标组件被响应式化)
const stats = ref([
{
label: "知识库",
value: "0",
change: 0,
icon: markRaw(Document),
type: "knowledge",
},
{
label: "新用户",
value: "0",
change: 0,
icon: markRaw(User),
type: "users",
},
{
label: "员工数",
value: "0",
change: 0,
icon: markRaw(ShoppingCart),
type: "employees",
},
{
label: "租户数",
value: "0",
change: 0,
icon: markRaw(TrendCharts),
type: "tenants",
},
]);
// 任务相关数据
const tasks = ref([]);
const taskCurrentPage = ref(1);
const taskPageSize = ref(5);
// 活动日志相关数据
const activityLogs = ref([]);
const currentPage = ref(1);
const pageSize = ref(5);
const totalActivityLogs = ref(0);
// 计算属性
const paginatedTasks = computed(() => {
const start = (taskCurrentPage.value - 1) * taskPageSize.value;
const end = start + taskPageSize.value;
return tasks.value.slice(start, end);
});
const paginatedActivityLogs = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return activityLogs.value.slice(start, end);
});
// 事件处理函数
const handleTaskPageChange = (page) => {
taskCurrentPage.value = page;
};
const handlePageChange = (page) => {
currentPage.value = page;
};
const getPriorityType = (priority) => {
const types = {
high: 'danger',
medium: 'warning',
low: 'info'
};
return types[priority] || 'info';
};
const getActivityIcon = (type) => {
const icons = {
operation: Edit,
access: View
};
return icons[type] || Document;
};
const authStore = useAuthStore();
// 格式化时间
const formatTime = (timestamp: string | Date) => {
if (!timestamp) return "-";
const date = new Date(timestamp);
if (isNaN(date.getTime())) return String(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days < 1) {
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours < 1) {
const minutes = Math.floor(diff / (1000 * 60));
return `${minutes}分钟前`;
}
return `${hours}小时前`;
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
onMounted(async () => {
try {
// 从auth store获取用户信息
const id = authStore.user.id;
console.log('用户ID:', id);
if (!id) {
console.error('用户ID不存在');
return;
}
const response = await getMenus(id);
} catch (error) {
console.error('获取菜单数据失败:', error);
}
});
</script>
<style lang="less" scoped>
.dashboard {
min-height: 100%;
background-color: var(--el-bg-color-page);
}
// 欢迎区域
.welcome-section {
margin-bottom: 16px;
padding: 32px;
background: linear-gradient(135deg, #3973ff 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: 16px;
margin-bottom: 16px;
.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, #3973ff 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%);
}
&.knowledge .stat-icon-wrapper {
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
}
&.employees .stat-icon-wrapper {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
}
&.tenants .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: 16px;
.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 {
min-height: 400px;
display: flex;
flex-direction: column;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
.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 {
margin-left: 20px;
.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: 4px;
font-weight: 500;
display: flex;
align-items: center;
}
.el-tag {
margin-left: 8px !important;
}
.task-meta {
display: flex;
align-items: center;
gap: 8px;
.task-date {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
.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-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
&.operation {
background-color: rgba(79, 132, 255, 0.2);
color: #4f84ff;
}
&.access {
background-color: rgba(85, 190, 130, 0.2);
color: #55be82;
}
}
.activity-info {
flex: 1;
min-width: 0;
.activity-text {
font-size: 14px;
color: var(--el-text-color-primary);
margin-bottom: 4px;
.activity-module {
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) {
.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;
}
}
.priority-0 {
color: #e6a23c;
background-color: #fdf6ec;
border-color: #f5dab1;
}
.priority-1 {
color: #67c23a;
background-color: rgb(240, 249, 235);
border-color: rgb(225, 243, 216);
}
.priority-2 {
color: #f56c6c;
background-color: #fef0f0;
border-color: #fbc4c4;
}
</style>