982 lines
24 KiB
Vue
982 lines
24 KiB
Vue
<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="getPriorityType(task.priority)" size="small" effect="plain">
|
||
{{ task.priority }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="task-meta">
|
||
<span class="task-date">{{ task.date }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-if="tasks.length === 0" description="暂无待办任务" :image-size="80" />
|
||
<div v-if="tasks.length > taskPageSize" class="pagination-wrapper">
|
||
<el-pagination v-model:current-page="taskCurrentPage" :page-size="taskPageSize" :total="tasks.length"
|
||
layout="prev, pager, next" 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 v-if="activityLogs.length === 0" 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" 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 { getKnowledgeCount } from "@/api/knowledge";
|
||
import { getPlatformStats, getTenantStats, getActivityLogs } from "@/api/dashboard";
|
||
import { useAuthStore } from "@/stores/auth";
|
||
|
||
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}`;
|
||
});
|
||
|
||
// 统计数据(使用 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 authStore = useAuthStore();
|
||
|
||
// 判断是否为租户员工登录
|
||
const isEmployee = computed(() => {
|
||
return authStore.user?.type === 'employee';
|
||
});
|
||
|
||
// 获取平台统计数据
|
||
const fetchPlatformStats = async () => {
|
||
try {
|
||
const res = await getPlatformStats();
|
||
if (res?.code === 0 || res?.success) {
|
||
const data = res.data || {};
|
||
|
||
// 知识库
|
||
if (data.knowledgeCount) {
|
||
stats.value[0].value = data.knowledgeCount.total?.toString() || "0";
|
||
stats.value[0].change = parseFloat((data.knowledgeCount.growthRate || 0).toFixed(1));
|
||
}
|
||
|
||
// 用户数
|
||
stats.value[1].label = "用户数";
|
||
stats.value[1].value = (data.userCount || 0).toString();
|
||
stats.value[1].change = 0;
|
||
|
||
// 员工数
|
||
stats.value[2].label = "员工数";
|
||
stats.value[2].value = (data.employeeCount || 0).toString();
|
||
stats.value[2].change = 0;
|
||
|
||
// 租户数
|
||
stats.value[3].label = "租户数";
|
||
stats.value[3].value = (data.tenantCount || 0).toString();
|
||
stats.value[3].change = 0;
|
||
}
|
||
} catch (e) {
|
||
// 静默失败,不影响页面显示
|
||
}
|
||
};
|
||
|
||
// 获取租户统计数据
|
||
const fetchTenantStats = async () => {
|
||
try {
|
||
const res = await getTenantStats();
|
||
if (res?.code === 0 || res?.success) {
|
||
const data = res.data || {};
|
||
|
||
// 知识库
|
||
if (data.knowledgeCount) {
|
||
stats.value[0].value = data.knowledgeCount.total?.toString() || "0";
|
||
stats.value[0].change = parseFloat((data.knowledgeCount.growthRate || 0).toFixed(1));
|
||
}
|
||
|
||
// 员工数
|
||
stats.value[1].label = "员工数";
|
||
stats.value[1].value = (data.employeeCount || 0).toString();
|
||
stats.value[1].change = 0;
|
||
|
||
// 部门数
|
||
stats.value[2].label = "部门数";
|
||
stats.value[2].value = (data.departmentCount || 0).toString();
|
||
stats.value[2].change = 0;
|
||
|
||
// 职位数
|
||
stats.value[3].label = "职位数";
|
||
stats.value[3].value = (data.positionCount || 0).toString();
|
||
stats.value[3].change = 0;
|
||
}
|
||
} catch (e) {
|
||
// 静默失败,不影响页面显示
|
||
}
|
||
};
|
||
|
||
// 任务列表
|
||
const taskCurrentPage = ref(1);
|
||
const taskPageSize = ref(5);
|
||
|
||
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,
|
||
},
|
||
{
|
||
title: "准备季度报告",
|
||
date: "2024-06-12",
|
||
priority: "High",
|
||
completed: false,
|
||
},
|
||
{
|
||
title: "更新项目文档",
|
||
date: "2024-06-13",
|
||
priority: "Medium",
|
||
completed: false,
|
||
},
|
||
{
|
||
title: "代码审查",
|
||
date: "2024-06-14",
|
||
priority: "Low",
|
||
completed: false,
|
||
},
|
||
{
|
||
title: "客户需求沟通",
|
||
date: "2024-06-15",
|
||
priority: "High",
|
||
completed: false,
|
||
},
|
||
{
|
||
title: "测试环境部署",
|
||
date: "2024-06-16",
|
||
priority: "Medium",
|
||
completed: false,
|
||
},
|
||
]);
|
||
|
||
// 分页后的任务列表
|
||
const paginatedTasks = computed(() => {
|
||
const start = (taskCurrentPage.value - 1) * taskPageSize.value;
|
||
const end = start + taskPageSize.value;
|
||
return tasks.value.slice(start, end);
|
||
});
|
||
|
||
// 处理任务页码变化
|
||
const handleTaskPageChange = (page: number) => {
|
||
taskCurrentPage.value = page;
|
||
};
|
||
|
||
// 动态记录
|
||
const activityLogs = ref<any[]>([]);
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(5);
|
||
const totalActivityLogs = ref(0);
|
||
|
||
// 分页后的活动日志
|
||
const paginatedActivityLogs = computed(() => {
|
||
const start = (currentPage.value - 1) * pageSize.value;
|
||
const end = start + pageSize.value;
|
||
return activityLogs.value.slice(start, end);
|
||
});
|
||
|
||
// 加载活动日志
|
||
const fetchActivityLogs = async () => {
|
||
try {
|
||
const data = await getActivityLogs(1, 100); // 获取更多数据用于分页
|
||
if (data?.code === 0 && data?.data?.logs) {
|
||
activityLogs.value = data.data.logs;
|
||
totalActivityLogs.value = data.data.logs.length;
|
||
} else {
|
||
console.warn('Unexpected response format:', data);
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to fetch activity logs:', e);
|
||
}
|
||
};
|
||
|
||
// 处理页码变化
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page;
|
||
};
|
||
|
||
// 格式化时间
|
||
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}`;
|
||
};
|
||
|
||
// 获取活动图标
|
||
const getActivityIcon = (type: string) => {
|
||
if (type === 'operation') return 'Edit';
|
||
if (type === 'access') return 'View';
|
||
return 'Document';
|
||
};
|
||
|
||
// 获取优先级类型
|
||
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(() => {
|
||
// 根据登录类型加载不同的统计数据
|
||
if (isEmployee.value) {
|
||
fetchTenantStats();
|
||
} else {
|
||
fetchPlatformStats();
|
||
}
|
||
|
||
// 加载活动日志
|
||
fetchActivityLogs();
|
||
|
||
// 折线图
|
||
const lineChartEl = document.getElementById("lineChart") as HTMLCanvasElement | null;
|
||
if (!lineChartEl) {
|
||
console.error("Line chart element not found");
|
||
return;
|
||
}
|
||
|
||
new Chart(lineChartEl, {
|
||
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,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// 柱状图
|
||
const barChartEl = document.getElementById("barChart") as HTMLCanvasElement | null;
|
||
if (!barChartEl) {
|
||
console.error("Bar chart element not found");
|
||
return;
|
||
}
|
||
|
||
new Chart(barChartEl, {
|
||
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%);
|
||
}
|
||
|
||
&.knowledge .stat-icon-wrapper {
|
||
background: linear-gradient(135deg, #062da3 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: 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 {
|
||
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 {
|
||
.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) {
|
||
.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>
|