|
|
@@ -1,54 +1,123 @@
|
|
|
<template>
|
|
|
- <div class="dashboard-container">
|
|
|
- <!-- 顶部统计卡片 -->
|
|
|
- <a-row :gutter="20">
|
|
|
- <a-col :xs="24" :sm="12" :md="6" v-for="(item, index) in statCards" :key="index">
|
|
|
- <a-card hoverable class="stat-card-item">
|
|
|
- <div class="stat-card-content">
|
|
|
- <div class="stat-icon-wrapper" :style="{ background: item.color }">
|
|
|
- <component :is="item.icon" :style="{ fontSize: '24px', color: '#fff' }" />
|
|
|
+ <div class="workbench-container">
|
|
|
+ <!-- 顶部问候区域 -->
|
|
|
+ <a-card class="greeting-card">
|
|
|
+ <a-row align="middle" justify="space-between">
|
|
|
+ <a-col>
|
|
|
+ <a-typography-title :level="3" style="margin: 0">
|
|
|
+ {{ greetingText }},{{ userName }}
|
|
|
+ </a-typography-title>
|
|
|
+ <a-typography-text type="secondary">
|
|
|
+ {{ currentDate }} {{ currentWeek }}
|
|
|
+ </a-typography-text>
|
|
|
+ </a-col>
|
|
|
+ <a-col>
|
|
|
+ <a-statistic :value="currentTime" :prefix="h(ClockCircleOutlined)" />
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- 今日概览卡片 -->
|
|
|
+ <a-row :gutter="[16, 16]" style="margin-top: 16px">
|
|
|
+ <a-col :xs="24" :sm="12" :md="6" v-for="(item, index) in todayStats" :key="index">
|
|
|
+ <a-card hoverable @click="item.onClick">
|
|
|
+ <a-statistic
|
|
|
+ :title="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ :prefix="h(item.icon)"
|
|
|
+ :value-style="{ color: item.color }"
|
|
|
+ />
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+
|
|
|
+ <!-- 主要内容区域 -->
|
|
|
+ <a-row :gutter="20" class="main-content">
|
|
|
+ <!-- 左侧:今日待办 + 最近消息 -->
|
|
|
+ <a-col :xs="24" :lg="12">
|
|
|
+ <!-- 今日待办 -->
|
|
|
+ <a-card hoverable class="todo-card">
|
|
|
+ <template #title>
|
|
|
+ <div class="card-header">
|
|
|
+ <span><CheckSquareOutlined /> 今日待办</span>
|
|
|
+ <a-button type="link" size="small" @click="goRoute('/system/todo')">查看全部</a-button>
|
|
|
</div>
|
|
|
- <div class="stat-info">
|
|
|
- <div class="stat-label">{{ item.label }}</div>
|
|
|
- <div class="stat-value">
|
|
|
- {{ item.value }}
|
|
|
+ </template>
|
|
|
+ <div class="todo-list">
|
|
|
+ <a-empty v-if="todoList.length === 0" description="暂无待办事项" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
|
|
+ <div v-else v-for="item in todoList" :key="item.id" class="todo-item" @click="goRoute('/system/todo')">
|
|
|
+ <div class="todo-checkbox">
|
|
|
+ <CheckCircleOutlined v-if="item.status === '1'" class="checked" />
|
|
|
+ <ClockCircleOutlined v-else class="pending" />
|
|
|
</div>
|
|
|
+ <div class="todo-content">
|
|
|
+ <div class="todo-title">{{ item.title }}</div>
|
|
|
+ <div class="todo-time">
|
|
|
+ <CalendarOutlined />
|
|
|
+ <span>{{ item.dueDate }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <a-tag :color="getPriorityColor(item.priority)" size="small">
|
|
|
+ {{ getPriorityText(item.priority) }}
|
|
|
+ </a-tag>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="stat-card-footer">
|
|
|
- <span>较昨日</span>
|
|
|
- <span :class="item.trend >= 0 ? 'trend-up' : 'trend-down'">
|
|
|
- {{ Math.abs(item.trend) }}%
|
|
|
- <component :is="item.trend >= 0 ? CaretUpOutlined : CaretDownOutlined" />
|
|
|
- </span>
|
|
|
- </div>
|
|
|
</a-card>
|
|
|
- </a-col>
|
|
|
- </a-row>
|
|
|
|
|
|
- <!-- 中部图表和快捷入口 -->
|
|
|
- <a-row :gutter="20" class="mt-20">
|
|
|
- <a-col :xs="24" :lg="16">
|
|
|
- <a-card hoverable class="chart-card">
|
|
|
+ <!-- 最近消息 -->
|
|
|
+ <a-card hoverable class="message-card mt-20">
|
|
|
<template #title>
|
|
|
<div class="card-header">
|
|
|
- <span>访问趋势</span>
|
|
|
- <a-radio-group v-model:value="chartPeriod" size="small">
|
|
|
- <a-radio-button value="week">本周</a-radio-button>
|
|
|
- <a-radio-button value="month">本月</a-radio-button>
|
|
|
- </a-radio-group>
|
|
|
+ <span><MessageOutlined /> 最近消息</span>
|
|
|
+ <a-button type="link" size="small" @click="goRoute('/message')">查看全部</a-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <div class="chart-wrapper">
|
|
|
- <line-chart :chart-data="lineChartData" />
|
|
|
+ <div class="message-list">
|
|
|
+ <a-empty v-if="recentMessages.length === 0" description="暂无消息" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
|
|
+ <div v-else v-for="item in recentMessages" :key="item.id" class="message-item" @click="goRoute('/message')">
|
|
|
+ <a-avatar :src="item.avatar" :size="40">{{ item.sender?.charAt(0) }}</a-avatar>
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-header">
|
|
|
+ <span class="message-sender">{{ item.sender }}</span>
|
|
|
+ <span class="message-time">{{ item.time }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="message-text">{{ item.content }}</div>
|
|
|
+ </div>
|
|
|
+ <a-badge v-if="!item.isRead" dot />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</a-card>
|
|
|
</a-col>
|
|
|
- <a-col :xs="24" :lg="8">
|
|
|
- <a-card hoverable class="quick-entry-card">
|
|
|
+
|
|
|
+ <!-- 右侧:日程安排 + 快捷入口 -->
|
|
|
+ <a-col :xs="24" :lg="12">
|
|
|
+ <!-- 本周日程 -->
|
|
|
+ <a-card hoverable class="schedule-card">
|
|
|
<template #title>
|
|
|
<div class="card-header">
|
|
|
- <span>快捷导航</span>
|
|
|
+ <span><CalendarOutlined /> 本周日程</span>
|
|
|
+ <span class="week-range">{{ weekRange }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <a-timeline class="schedule-timeline">
|
|
|
+ <a-timeline-item v-for="item in scheduleList" :key="item.id" :color="item.color">
|
|
|
+ <div class="schedule-item">
|
|
|
+ <div class="schedule-time">
|
|
|
+ <ClockCircleOutlined />
|
|
|
+ <span>{{ item.time }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="schedule-title">{{ item.title }}</div>
|
|
|
+ <div class="schedule-desc">{{ item.description }}</div>
|
|
|
+ </div>
|
|
|
+ </a-timeline-item>
|
|
|
+ </a-timeline>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- 快捷入口 -->
|
|
|
+ <a-card hoverable class="quick-entry-card mt-20">
|
|
|
+ <template #title>
|
|
|
+ <div class="card-header">
|
|
|
+ <span><AppstoreOutlined /> 快捷入口</span>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div class="quick-nav-grid">
|
|
|
@@ -59,177 +128,288 @@
|
|
|
@click="goRoute(item.path)"
|
|
|
>
|
|
|
<div class="nav-icon" :style="{ background: item.bg }">
|
|
|
- <component :is="item.icon" :style="{ fontSize: '20px', color: item.color }" />
|
|
|
+ <component :is="item.icon" :style="{ fontSize: '24px', color: item.color }" />
|
|
|
</div>
|
|
|
<span class="nav-label">{{ item.name }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</a-card>
|
|
|
-
|
|
|
- <a-card hoverable class="mt-20 system-info-card">
|
|
|
- <template #title>
|
|
|
- <div class="card-header">
|
|
|
- <span>系统概览</span>
|
|
|
- <a-tag size="small">v3.9.0</a-tag>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div class="info-list">
|
|
|
- <div class="info-item">
|
|
|
- <span class="label">Vue版本</span>
|
|
|
- <span class="value">3.x</span>
|
|
|
- </div>
|
|
|
- <div class="info-item">
|
|
|
- <span class="label">Ant Design Vue</span>
|
|
|
- <span class="value">4.x</span>
|
|
|
- </div>
|
|
|
- <div class="info-item">
|
|
|
- <span class="label">Vite</span>
|
|
|
- <span class="value">5.x</span>
|
|
|
- </div>
|
|
|
- <div class="info-item">
|
|
|
- <span class="label">当前环境</span>
|
|
|
- <span class="value">{{ env }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </a-card>
|
|
|
- </a-col>
|
|
|
- </a-row>
|
|
|
-
|
|
|
- <!-- 底部项目介绍 -->
|
|
|
- <a-row :gutter="20" class="mt-20">
|
|
|
- <a-col :span="24">
|
|
|
- <a-card hoverable class="project-card">
|
|
|
- <div class="project-intro">
|
|
|
- <div class="project-logo">
|
|
|
- <img src="@/assets/logo/logo.png" alt="logo" />
|
|
|
- </div>
|
|
|
- <div class="project-desc">
|
|
|
- <h3>予书后台管理系统</h3>
|
|
|
- <p>一款基于 Vue3 + Vite + Ant Design Vue 的现代化后台管理系统。提供丰富的组件和功能,开箱即用,助力开发者快速构建高质量的 Web 应用。</p>
|
|
|
- <div class="project-actions">
|
|
|
- <a-button type="primary" @click="goTarget('http://yushu.vip')">
|
|
|
- <template #icon><ShareAltOutlined /></template>
|
|
|
- 访问官网
|
|
|
- </a-button>
|
|
|
- <a-button @click="goTarget('https://gitee.com/y_project/yushu-Vue')">
|
|
|
- <template #icon><StarOutlined /></template>
|
|
|
- Gitee Star
|
|
|
- </a-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </a-card>
|
|
|
</a-col>
|
|
|
</a-row>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup name="Index">
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
+<script setup name="Workbench">
|
|
|
+import { ref, onMounted, onUnmounted, computed } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
|
+import { Empty } from 'ant-design-vue'
|
|
|
import {
|
|
|
- UserOutlined, MessageOutlined, FolderOutlined, MonitorOutlined, SettingOutlined,
|
|
|
- FileTextOutlined, BellOutlined, CommentOutlined, CaretUpOutlined, CaretDownOutlined,
|
|
|
- ShareAltOutlined, StarOutlined, LineChartOutlined, ShoppingOutlined, ReadOutlined
|
|
|
+ ClockCircleOutlined, CalendarOutlined, CheckSquareOutlined, MessageOutlined,
|
|
|
+ CheckCircleOutlined, BellOutlined, FileTextOutlined, AppstoreOutlined,
|
|
|
+ UserOutlined, SettingOutlined, FolderOutlined, TeamOutlined
|
|
|
} from '@ant-design/icons-vue'
|
|
|
-import LineChart from './dashboard/LineChart.vue'
|
|
|
-import { getDashboardStats, getVisitsTrend } from '@/api/system/statistics'
|
|
|
+import { getMyTodo } from '@/api/system/todo'
|
|
|
+import { getConversationList } from '@/api/system/message'
|
|
|
+import { getRouters } from '@/api/menu'
|
|
|
+import { formatTimeAgo } from '@/utils/formatTime'
|
|
|
+import useUserStore from '@/store/modules/user'
|
|
|
|
|
|
const router = useRouter()
|
|
|
-const env = import.meta.env.MODE
|
|
|
-const loading = ref(false)
|
|
|
-
|
|
|
-// 统计数据
|
|
|
-const statCards = ref([
|
|
|
- { label: '用户总数', value: 0, icon: UserOutlined, color: '#409eff', trend: 0 },
|
|
|
- { label: '消息数量', value: 0, icon: MessageOutlined, color: '#67c23a', trend: 0 },
|
|
|
- { label: '文件数量', value: 0, icon: FolderOutlined, color: '#e6a23c', trend: 0 },
|
|
|
- { label: '今日访问', value: 0, icon: MonitorOutlined, color: '#f56c6c', trend: 0 }
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+// 当前时间
|
|
|
+const currentTime = ref('')
|
|
|
+const currentDate = ref('')
|
|
|
+const currentWeek = ref('')
|
|
|
+const greetingText = ref('')
|
|
|
+const userName = computed(() => userStore.nickName || userStore.name || '用户')
|
|
|
+
|
|
|
+// 今日统计
|
|
|
+const todayStats = ref([
|
|
|
+ {
|
|
|
+ label: '今日待办',
|
|
|
+ value: 0,
|
|
|
+ icon: CheckSquareOutlined,
|
|
|
+ color: '#409eff',
|
|
|
+ onClick: () => router.push('/system/todo')
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '未读消息',
|
|
|
+ value: 0,
|
|
|
+ icon: MessageOutlined,
|
|
|
+ color: '#67c23a',
|
|
|
+ onClick: () => router.push('/message')
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '系统通知',
|
|
|
+ value: 0,
|
|
|
+ icon: BellOutlined,
|
|
|
+ color: '#e6a23c',
|
|
|
+ onClick: () => router.push('/system/notification')
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '待审批',
|
|
|
+ value: 0,
|
|
|
+ icon: FileTextOutlined,
|
|
|
+ color: '#f56c6c',
|
|
|
+ onClick: () => {}
|
|
|
+ }
|
|
|
])
|
|
|
|
|
|
-// 图表数据
|
|
|
-const chartPeriod = ref('week')
|
|
|
-const lineChartData = ref({
|
|
|
- expectedData: [0, 0, 0, 0, 0, 0, 0],
|
|
|
- actualData: [0, 0, 0, 0, 0, 0, 0]
|
|
|
-})
|
|
|
+// 待办列表
|
|
|
+const todoList = ref([])
|
|
|
|
|
|
-// 快捷入口
|
|
|
-const quickLinks = ref([
|
|
|
- { name: '用户管理', path: '/system/user', icon: UserOutlined, color: '#409eff', bg: '#ecf5ff' },
|
|
|
- { name: '角色管理', path: '/system/role', icon: UserOutlined, color: '#67c23a', bg: '#f0f9eb' },
|
|
|
- { name: '菜单管理', path: '/system/menu', icon: SettingOutlined, color: '#e6a23c', bg: '#fdf6ec' },
|
|
|
- { name: '文件管理', path: '/file', icon: FolderOutlined, color: '#f56c6c', bg: '#fef0f0' },
|
|
|
- { name: '消息中心', path: '/message', icon: MessageOutlined, color: '#409eff', bg: '#ecf5ff' },
|
|
|
- { name: '通知公告', path: '/system/notice', icon: BellOutlined, color: '#e6a23c', bg: '#fdf6ec' },
|
|
|
- { name: '操作日志', path: '/system/log/operlog', icon: FileTextOutlined, color: '#909399', bg: '#f4f4f5' },
|
|
|
- { name: 'AI 对话', path: '/ai/chat', icon: CommentOutlined, color: '#764ba2', bg: '#f8f5fa' }
|
|
|
+// 最近消息
|
|
|
+const recentMessages = ref([])
|
|
|
+
|
|
|
+// 本周日程
|
|
|
+const weekRange = ref('')
|
|
|
+const scheduleList = ref([
|
|
|
+ { id: 1, time: '周一 09:00', title: '团队周会', description: '讨论本周工作计划', color: 'blue' },
|
|
|
+ { id: 2, time: '周二 14:00', title: '项目评审', description: '新功能需求评审', color: 'green' },
|
|
|
+ { id: 3, time: '周三 10:30', title: '技术分享', description: 'Vue3 最佳实践', color: 'orange' },
|
|
|
+ { id: 4, time: '周四 15:00', title: '客户沟通', description: '产品演示和反馈收集', color: 'purple' },
|
|
|
+ { id: 5, time: '周五 16:00', title: '周总结', description: '本周工作总结和复盘', color: 'red' }
|
|
|
])
|
|
|
|
|
|
-/** 加载统计数据 */
|
|
|
-async function loadDashboardStats() {
|
|
|
- loading.value = true
|
|
|
+// 快捷入口
|
|
|
+const quickLinks = ref([])
|
|
|
+
|
|
|
+// 图标映射
|
|
|
+const iconMap = {
|
|
|
+ 'user': UserOutlined,
|
|
|
+ 'peoples': TeamOutlined,
|
|
|
+ 'tree-table': SettingOutlined,
|
|
|
+ 'folder': FolderOutlined,
|
|
|
+ 'message': MessageOutlined,
|
|
|
+ 'bell': BellOutlined,
|
|
|
+ 'log': FileTextOutlined,
|
|
|
+ 'setting': SettingOutlined
|
|
|
+}
|
|
|
+
|
|
|
+// 颜色映射
|
|
|
+const colorMap = [
|
|
|
+ { color: '#409eff', bg: '#ecf5ff' },
|
|
|
+ { color: '#67c23a', bg: '#f0f9eb' },
|
|
|
+ { color: '#e6a23c', bg: '#fdf6ec' },
|
|
|
+ { color: '#f56c6c', bg: '#fef0f0' },
|
|
|
+ { color: '#409eff', bg: '#ecf5ff' },
|
|
|
+ { color: '#e6a23c', bg: '#fdf6ec' },
|
|
|
+ { color: '#909399', bg: '#f4f4f5' },
|
|
|
+ { color: '#909399', bg: '#f4f4f5' }
|
|
|
+]
|
|
|
+
|
|
|
+// 更新时间
|
|
|
+function updateTime() {
|
|
|
+ const now = new Date()
|
|
|
+ const hours = now.getHours()
|
|
|
+ const minutes = now.getMinutes().toString().padStart(2, '0')
|
|
|
+ const seconds = now.getSeconds().toString().padStart(2, '0')
|
|
|
+
|
|
|
+ currentTime.value = `${hours}:${minutes}:${seconds}`
|
|
|
+
|
|
|
+ const year = now.getFullYear()
|
|
|
+ const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ const day = now.getDate().toString().padStart(2, '0')
|
|
|
+ currentDate.value = `${year}年${month}月${day}日`
|
|
|
+
|
|
|
+ const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
|
|
+ currentWeek.value = weekDays[now.getDay()]
|
|
|
+
|
|
|
+ // 问候语
|
|
|
+ if (hours < 6) {
|
|
|
+ greetingText.value = '凌晨好'
|
|
|
+ } else if (hours < 9) {
|
|
|
+ greetingText.value = '早上好'
|
|
|
+ } else if (hours < 12) {
|
|
|
+ greetingText.value = '上午好'
|
|
|
+ } else if (hours < 14) {
|
|
|
+ greetingText.value = '中午好'
|
|
|
+ } else if (hours < 18) {
|
|
|
+ greetingText.value = '下午好'
|
|
|
+ } else if (hours < 22) {
|
|
|
+ greetingText.value = '晚上好'
|
|
|
+ } else {
|
|
|
+ greetingText.value = '夜深了'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取本周日期范围
|
|
|
+function getWeekRange() {
|
|
|
+ const now = new Date()
|
|
|
+ const day = now.getDay()
|
|
|
+ const diff = now.getDate() - day + (day === 0 ? -6 : 1)
|
|
|
+ const monday = new Date(now.setDate(diff))
|
|
|
+ const sunday = new Date(monday)
|
|
|
+ sunday.setDate(monday.getDate() + 6)
|
|
|
+
|
|
|
+ const format = (date) => {
|
|
|
+ const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ const d = date.getDate().toString().padStart(2, '0')
|
|
|
+ return `${m}.${d}`
|
|
|
+ }
|
|
|
+
|
|
|
+ weekRange.value = `${format(monday)} - ${format(sunday)}`
|
|
|
+}
|
|
|
+
|
|
|
+// 加载快捷入口(从菜单获取)
|
|
|
+async function loadQuickLinks() {
|
|
|
try {
|
|
|
- const res = await getDashboardStats()
|
|
|
+ const res = await getRouters()
|
|
|
if (res.code === 200 && res.data) {
|
|
|
- const data = res.data
|
|
|
- const trends = data.trends || {}
|
|
|
-
|
|
|
- statCards.value[0].value = data.userCount || 0
|
|
|
- statCards.value[0].trend = trends.userTrend || 0
|
|
|
-
|
|
|
- statCards.value[1].value = data.messageCount || 0
|
|
|
- statCards.value[1].trend = trends.messageTrend || 0
|
|
|
-
|
|
|
- statCards.value[2].value = data.fileCount || 0
|
|
|
- statCards.value[2].trend = trends.fileTrend || 0
|
|
|
+ const menus = []
|
|
|
+ // 递归提取所有菜单
|
|
|
+ const extractMenus = (list) => {
|
|
|
+ list.forEach(item => {
|
|
|
+ if (item.path && item.meta && item.meta.title) {
|
|
|
+ menus.push({
|
|
|
+ name: item.meta.title,
|
|
|
+ path: item.path,
|
|
|
+ icon: iconMap[item.meta.icon] || SettingOutlined
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (item.children && item.children.length > 0) {
|
|
|
+ extractMenus(item.children)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ extractMenus(res.data)
|
|
|
|
|
|
- statCards.value[3].value = data.todayVisits || 0
|
|
|
- statCards.value[3].trend = trends.visitTrend || 0
|
|
|
+ // 取前8个菜单作为快捷入口
|
|
|
+ quickLinks.value = menus.slice(0, 8).map((item, index) => ({
|
|
|
+ ...item,
|
|
|
+ color: colorMap[index].color,
|
|
|
+ bg: colorMap[index].bg
|
|
|
+ }))
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('加载统计数据失败:', error)
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
+ console.error('加载快捷入口失败:', error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** 加载访问趋势 */
|
|
|
-async function loadVisitsTrend() {
|
|
|
+// 加载待办事项
|
|
|
+async function loadTodoList() {
|
|
|
try {
|
|
|
- const res = await getVisitsTrend()
|
|
|
- if (res.code === 200 && res.data && res.data.data) {
|
|
|
- const trendData = res.data.data
|
|
|
- const actualData = new Array(7).fill(0)
|
|
|
- trendData.forEach((item, index) => {
|
|
|
- if (index < 7) {
|
|
|
- actualData[index] = item.count || 0
|
|
|
- }
|
|
|
- })
|
|
|
- lineChartData.value.actualData = actualData
|
|
|
- lineChartData.value.expectedData = actualData.map(v => Math.round(v * 1.1))
|
|
|
- }
|
|
|
+ const res = await getMyTodo()
|
|
|
+ const list = res.rows || res.data || []
|
|
|
+ todoList.value = list
|
|
|
+ .filter(item => item.status === '0')
|
|
|
+ .slice(0, 5)
|
|
|
+ .map(item => ({
|
|
|
+ id: item.todoId,
|
|
|
+ title: item.title,
|
|
|
+ dueDate: item.dueDate ? item.dueDate.substring(0, 10) : '无截止日期',
|
|
|
+ priority: item.priority,
|
|
|
+ status: item.status
|
|
|
+ }))
|
|
|
+
|
|
|
+ todayStats.value[0].value = list.filter(item => item.status === '0').length
|
|
|
} catch (error) {
|
|
|
- console.error('加载访问趋势失败:', error)
|
|
|
+ console.error('加载待办失败:', error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function goTarget(url) {
|
|
|
- window.open(url, '_blank')
|
|
|
+// 加载最近消息
|
|
|
+async function loadRecentMessages() {
|
|
|
+ try {
|
|
|
+ const res = await getConversationList()
|
|
|
+ const list = res.data || res.rows || []
|
|
|
+ recentMessages.value = list
|
|
|
+ .filter(item => item.conversation_id !== 'sys_notification')
|
|
|
+ .slice(0, 5)
|
|
|
+ .map(item => ({
|
|
|
+ id: item.conversation_id,
|
|
|
+ sender: item.other_members || '未知用户',
|
|
|
+ avatar: item.avatar || '',
|
|
|
+ content: item.last_message || '',
|
|
|
+ time: formatTimeAgo(item.last_message_time),
|
|
|
+ isRead: (item.unread_count || 0) === 0
|
|
|
+ }))
|
|
|
+
|
|
|
+ const unreadCount = list.filter(item => (item.unread_count || 0) > 0).length
|
|
|
+ todayStats.value[1].value = unreadCount
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载消息失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取优先级颜色
|
|
|
+function getPriorityColor(priority) {
|
|
|
+ const colors = { '0': 'default', '1': 'blue', '2': 'orange', '3': 'red' }
|
|
|
+ return colors[priority] || 'default'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取优先级文本
|
|
|
+function getPriorityText(priority) {
|
|
|
+ const texts = { '0': '低', '1': '普通', '2': '重要', '3': '紧急' }
|
|
|
+ return texts[priority] || '普通'
|
|
|
}
|
|
|
|
|
|
function goRoute(path) {
|
|
|
router.push(path)
|
|
|
}
|
|
|
|
|
|
+let timer = null
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
- loadDashboardStats()
|
|
|
- loadVisitsTrend()
|
|
|
+ updateTime()
|
|
|
+ getWeekRange()
|
|
|
+ loadQuickLinks()
|
|
|
+ loadTodoList()
|
|
|
+ loadRecentMessages()
|
|
|
+
|
|
|
+ timer = setInterval(updateTime, 1000)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (timer) {
|
|
|
+ clearInterval(timer)
|
|
|
+ }
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.dashboard-container {
|
|
|
+.workbench-container {
|
|
|
padding: 24px;
|
|
|
background-color: #f0f2f5;
|
|
|
min-height: 100vh;
|
|
|
@@ -239,188 +419,339 @@ onMounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.stat-card-item {
|
|
|
+// 问候区域
|
|
|
+.greeting-section {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 32px;
|
|
|
margin-bottom: 20px;
|
|
|
- border: none;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
- :deep(.ant-card-body) {
|
|
|
- padding: 20px;
|
|
|
- }
|
|
|
-
|
|
|
- .stat-card-content {
|
|
|
+ .greeting-content {
|
|
|
display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
|
- .stat-icon-wrapper {
|
|
|
- width: 48px;
|
|
|
- height: 48px;
|
|
|
- border-radius: 12px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-right: 16px;
|
|
|
- box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
|
|
- }
|
|
|
-
|
|
|
- .stat-info {
|
|
|
- .stat-label {
|
|
|
+ .greeting-text {
|
|
|
+ h1 {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0 0 8px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .greeting-subtitle {
|
|
|
font-size: 14px;
|
|
|
color: #909399;
|
|
|
- margin-bottom: 4px;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
- .stat-value {
|
|
|
- font-size: 24px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .greeting-time {
|
|
|
+ .time-display {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ background: #409eff;
|
|
|
+ padding: 12px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+
|
|
|
+ .time-icon {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .current-time {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #fff;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+// 概览卡片
|
|
|
+.overview-cards {
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
|
- .stat-card-footer {
|
|
|
- border-top: 1px solid #ebeef5;
|
|
|
- padding-top: 12px;
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
+ .stat-card-item {
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+ transition: all 0.3s;
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
- .trend-up {
|
|
|
- color: #67c23a;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-card-body) {
|
|
|
+ padding: 20px;
|
|
|
}
|
|
|
|
|
|
- .trend-down {
|
|
|
- color: #f56c6c;
|
|
|
+ .stat-card-content {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ .stat-icon-wrapper {
|
|
|
+ width: 52px;
|
|
|
+ height: 52px;
|
|
|
+ border-radius: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-info {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 26px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.chart-card {
|
|
|
+// 主要内容区域
|
|
|
+.main-content {
|
|
|
.card-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- }
|
|
|
- .chart-wrapper {
|
|
|
- height: 350px;
|
|
|
+ font-weight: 500;
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.quick-entry-card {
|
|
|
- .quick-nav-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(4, 1fr);
|
|
|
- gap: 16px;
|
|
|
-
|
|
|
- .quick-nav-item {
|
|
|
+// 待办卡片
|
|
|
+.todo-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+
|
|
|
+ .todo-list {
|
|
|
+ .todo-item {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 14px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ background: #f5f7fa;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
&:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
+ background: #ecf5ff;
|
|
|
}
|
|
|
|
|
|
- .nav-icon {
|
|
|
- width: 40px;
|
|
|
- height: 40px;
|
|
|
- border-radius: 10px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-bottom: 8px;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
- .nav-label {
|
|
|
- font-size: 12px;
|
|
|
- color: #606266;
|
|
|
+ .todo-checkbox {
|
|
|
+ font-size: 18px;
|
|
|
+
|
|
|
+ .checked {
|
|
|
+ color: #67c23a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pending {
|
|
|
+ color: #e6a23c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .todo-content {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .todo-title {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .todo-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.system-info-card {
|
|
|
- .card-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- }
|
|
|
+// 消息卡片
|
|
|
+.message-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
- .info-list {
|
|
|
- .info-item {
|
|
|
+ .message-list {
|
|
|
+ .message-item {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 12px 0;
|
|
|
- border-bottom: 1px solid #f0f2f5;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 14px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
|
- &:last-child {
|
|
|
- border-bottom: none;
|
|
|
+ &:hover {
|
|
|
+ background: #ecf5ff;
|
|
|
}
|
|
|
|
|
|
- .label {
|
|
|
- color: #909399;
|
|
|
- font-size: 14px;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
- .value {
|
|
|
- color: #303133;
|
|
|
- font-weight: 500;
|
|
|
- font-size: 14px;
|
|
|
+ .message-content {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+
|
|
|
+ .message-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 4px;
|
|
|
+
|
|
|
+ .message-sender {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.project-card {
|
|
|
- .project-intro {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 32px;
|
|
|
- padding: 10px;
|
|
|
-
|
|
|
- .project-logo {
|
|
|
- img {
|
|
|
- width: 80px;
|
|
|
- height: 80px;
|
|
|
- }
|
|
|
+// 日程卡片
|
|
|
+.schedule-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ .week-range {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ font-weight: normal;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ .schedule-timeline {
|
|
|
+ margin-top: 16px;
|
|
|
|
|
|
- .project-desc {
|
|
|
- h3 {
|
|
|
- margin: 0 0 12px 0;
|
|
|
- font-size: 20px;
|
|
|
+ .schedule-item {
|
|
|
+ .schedule-time {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .schedule-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
}
|
|
|
|
|
|
- p {
|
|
|
- margin: 0 0 20px 0;
|
|
|
- color: #606266;
|
|
|
- line-height: 1.6;
|
|
|
+ .schedule-desc {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 快捷入口
|
|
|
+.quick-entry-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+
|
|
|
+ .quick-nav-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 20px;
|
|
|
+ padding: 16px;
|
|
|
+
|
|
|
+ .quick-nav-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 20px 10px;
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: all 0.3s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
- .project-actions {
|
|
|
+ .nav-icon {
|
|
|
+ width: 56px;
|
|
|
+ height: 56px;
|
|
|
+ border-radius: 8px;
|
|
|
display: flex;
|
|
|
- gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-bottom:10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .nav-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
- .project-intro {
|
|
|
+ .greeting-content {
|
|
|
flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
text-align: center;
|
|
|
-
|
|
|
- .project-actions {
|
|
|
- justify-content: center;
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ .quick-nav-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr) !important;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
+
|