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

982 lines
24 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="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>