@@ -1,266 +1,326 @@
< script lang = "ts" setup >
import type {
WorkbenchProjectItem ,
WorkbenchQuickNavItem ,
WorkbenchTodoItem ,
WorkbenchTrendItem ,
} from '@easyflow/common-ui' ;
import type { EchartsUIType } from '@easyflow/plugins/echarts' ;
import { ref } from 'vue' ;
import { useRouter } from 'vue-rout er' ;
import type {
DashboardOverviewQu ery ,
DashboardOverviewResponse ,
DashboardRange ,
DashboardSummary ,
DashboardTrendItem ,
} from '#/api/dashboard' ;
import {
AnalysisChartCar d,
WorkbenchHeader ,
WorkbenchProjec t,
WorkbenchQuickNav ,
WorkbenchTodo ,
WorkbenchTrends ,
} from '@easyflow/common-ui ' ;
import { preferences } from '@easyflow/preferences' ;
compute d,
nextTick ,
onBeforeUnmoun t,
onMounted ,
reactive ,
ref ,
} from 'vue ' ;
import { AnalysisChartCard } from '@easyflow/common-ui' ;
import { EchartsUI , useEcharts } from '@easyflow/plugins/echarts' ;
import { useUserStore } from '@easyflow/stores' ;
import { openWindow } from '@easyflow/utils' ;
import AnalyticsVisitsSource from '../analytics/analytics-visits-source. vue';
import { RefreshRight } from '@element-plus/icons- vue';
import { ElButton , ElEmpty , ElRadioButton , ElRadioGroup } from 'element-plus' ;
import { getDashboardOverview } from '#/api/dashboard' ;
interface SummaryCardItem {
label : string ;
value : string ;
}
let greetingTimer : null | ReturnType < typeof setInterval > = null ;
const userStore = useUserStore ( ) ;
const now = ref ( new Date ( ) ) ;
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
// 例如: url: /dashboard/workspace
const projectItems : WorkbenchProjectItem [ ] = [
{
color : '' ,
content : '不要等待机会,而要创造机会。' ,
date : '2021-04-01' ,
group : '开源组' ,
icon : 'carbon:logo-github' ,
title : 'Github' ,
url : 'https://github.com' ,
} ,
{
color : '#3fb27f' ,
content : '现在的你决定将来的你。' ,
date : '2021-04-01' ,
group : '算法组' ,
icon : 'ion:logo-vue' ,
title : 'Vue' ,
url : 'https://vuejs.org' ,
} ,
{
color : '#e18525' ,
content : '没有什么才能比努力更重要。' ,
date : '2021-04-01' ,
group : '上班摸鱼' ,
icon : 'ion:logo-html5' ,
title : 'Html5' ,
url : 'https://developer.mozilla.org/zh-CN/docs/Web/HTML' ,
} ,
{
color : '#bf0c2c' ,
content : '热情和欲望可以突破一切难关。' ,
date : '2021-04-01' ,
group : 'UI' ,
icon : 'ion:logo-angular' ,
title : 'Angular' ,
url : 'https://angular.io' ,
} ,
{
color : '#00d8ff' ,
content : '健康的身体是实现目标的基石。' ,
date : '2021-04-01' ,
group : '技术牛' ,
icon : 'bx:bxl-react' ,
title : 'React' ,
url : 'https://reactjs.org' ,
} ,
{
color : '#EBD94E' ,
content : '路是走出来的,而不是空想出来的。' ,
date : '2021-04-01' ,
group : '架构组' ,
icon : 'ion:logo-javascript' ,
title : 'Js' ,
url : 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript' ,
} ,
const filters = reactive < Required < DashboardOverviewQuery > > ( {
range : '7d' ,
} ) ;
const overview = ref < DashboardOverviewResponse | null > ( null ) ;
const isLoading = ref ( false ) ;
const errorMessage = ref ( '' ) ;
const trendChartRef = ref < EchartsUIType > ( ) ;
const { renderEcharts : renderTrendEcharts } = useEcharts ( trendChartRef ) ;
const rangeOptions : Array < { label : string ; value : DashboardRange } > = [
{ label : '今日' , value : 'today' },
{ label : '近 7 天' , value : '7d' } ,
{ label : '近 30 天' , value : '30d' } ,
] ;
// 同样,这里的 url 也可以使用以 http 开头的外部链接
const quickNavItems : WorkbenchQuickNavItem [ ] = [
{
color : '#1fdaca' ,
icon : 'ion:home-outline' ,
title : '首页' ,
url : '/' ,
} ,
{
color : '#bf0c2c' ,
icon : 'ion:grid-outline' ,
title : '仪表盘' ,
url : '/dashboard' ,
} ,
{
color : '#e18525' ,
icon : 'ion:layers-outline' ,
title : '组件' ,
url : '/demos/features/icons' ,
} ,
{
color : '#3fb27f' ,
icon : 'ion:settings-outline' ,
title : '系统管理' ,
url : '/demos/features/login-expired' , // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
} ,
{
color : '#4daf1bc9' ,
icon : 'ion:key-outline' ,
title : '权限管理' ,
url : '/demos/access/page-control' ,
} ,
{
color : '#00d8ff' ,
icon : 'ion:bar-chart-outline' ,
title : '图表' ,
url : '/analytics' ,
} ,
] ;
const emptySummary : DashboardSummary = {
activeUserTotal : 0 ,
botTotal : 0 ,
knowledgeBaseTotal : 0 ,
userTotal : 0 ,
workflowTotal : 0 ,
} ;
const todoItems = ref < WorkbenchTodoItem [ ] > ( [
const summary = computed ( ( ) => overview . value ? . summary ? ? emptySummary ) ;
const trends = computed < DashboardTrendItem [ ] > (
( ) => overview . value ? . trends ? ? [ ] ,
) ;
const summaryCards = computed < SummaryCardItem [ ] > ( ( ) => [
{ label : '用户总量' , value : formatCount ( summary . value . userTotal ) } ,
{ label : '活跃用户' , value : formatCount ( summary . value . activeUserTotal ) } ,
{ label : '助手数量' , value : formatCount ( summary . value . botTotal ) } ,
{ label : '工作流数量' , value : formatCount ( summary . value . workflowTotal ) } ,
{
completed : false ,
content : ` 审查最近提交到Git仓库的前端代码, 确保代码质量和规范。 ` ,
date : '2024-07-30 11:00:00' ,
title : '审查前端代码提交' ,
} ,
{
completed : true ,
content : ` 检查并优化系统性能, 降低CPU使用率。 ` ,
date : '2024-07-30 11:00:00' ,
title : '系统性能优化' ,
} ,
{
completed : false ,
content : ` 进行系统安全检查,确保没有安全漏洞或未授权的访问。 ` ,
date : '2024-07-30 11:00:00' ,
title : '安全检查' ,
} ,
{
completed : false ,
content : ` 更新项目中的所有npm依赖包, 确保使用最新版本。 ` ,
date : '2024-07-30 11:00:00' ,
title : '更新项目依赖' ,
} ,
{
completed : false ,
content : ` 修复用户报告的页面UI显示问题, 确保在不同浏览器中显示一致。 ` ,
date : '2024-07-30 11:00:00' ,
title : '修复UI显示问题' ,
label : '知识库数量' ,
value : formatCount ( summary . value . knowledgeBaseTotal ) ,
} ,
] ) ;
const trendItems : WorkbenchTrendItem [ ] = [
{
avatar : 'svg:avatar-1' ,
content : ` 在 <a>开源组</a> 创建了项目 <a>Vue</a> ` ,
date : '刚刚' ,
title : '威廉' ,
} ,
{
avatar : 'svg:avatar-2' ,
content : ` 关注了 <a>威廉</a> ` ,
date : '1个小时前' ,
title : '艾文' ,
} ,
{
avatar : 'svg:avatar-3' ,
content : ` 发布了 <a>个人动态</a> ` ,
date : '1天前' ,
title : '克里斯' ,
} ,
{
avatar : 'svg:avatar-4' ,
content : ` 发表文章 <a>如何编写一个Vite插件</a> ` ,
date : '2天前' ,
title : 'EasyFlow' ,
} ,
{
avatar : 'svg:avatar-1' ,
content : ` 回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a> ` ,
date : '3天前' ,
title : '皮特' ,
} ,
{
avatar : 'svg:avatar-2' ,
content : ` 关闭了问题 <a>如何运行项目</a> ` ,
date : '1周前' ,
title : '杰克' ,
} ,
{
avatar : 'svg:avatar-3' ,
content : ` 发布了 <a>个人动态</a> ` ,
date : '1周前' ,
title : '威廉' ,
} ,
{
avatar : 'svg:avatar-4' ,
content : ` 推送了代码到 <a>Github</a> ` ,
date : '2021-04-01 20:00' ,
title : '威廉' ,
} ,
{
avatar : 'svg:avatar-4' ,
content : ` 发表文章 <a>如何编写使用 Admin EasyFlow</a> ` ,
date : '2021-03-01 20:00' ,
title : 'EasyFlow' ,
} ,
] ;
const router = useRouter ( ) ;
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
// This is a sample method, adjust according to the actual project requirements
function navTo ( nav : WorkbenchProjectItem | WorkbenchQuickNavItem ) {
if ( nav . url ? . startsWith ( 'http' ) ) {
openWindow ( nav . url ) ;
return ;
const updatedAtText = computed ( ( ) => {
if ( ! overview . value ? . updatedAt ) {
return '尚未获取' ;
}
if ( nav . url ? . startsWith ( '/' ) ) {
router . push ( nav . url ) . catch ( ( error ) => {
console . error ( 'Navigation failed:' , error ) ;
return formatDateTime ( overview . value . updatedAt ) ;
} ) ;
const displayName = computed ( ( ) => {
return (
userStore . userInfo ? . nickname ? . trim ( ) ||
userStore . userInfo ? . loginName ? . trim ( ) ||
'同学'
) ;
} ) ;
const greetingText = computed ( ( ) => {
const hour = now . value . getHours ( ) ;
if ( hour < 11 ) {
return '上午好' ;
}
if ( hour < 14 ) {
return '中午好' ;
}
if ( hour < 18 ) {
return '下午好' ;
}
return '晚上好' ;
} ) ;
const greetingTitle = computed (
( ) => ` ${ greetingText . value } , ${ displayName . value } ` ,
) ;
async function loadOverview ( ) {
isLoading . value = true ;
errorMessage . value = '' ;
try {
const data = await getDashboardOverview ( {
range : filters . range ,
} ) ;
} else {
console . warn ( ` Unknown URL for navigation item: ${ nav . title } -> ${ nav . url } ` ) ;
overview . value = data ;
await renderCharts ( ) ;
} catch ( error ) {
overview . value = null ;
errorMessage . value =
( error as Error ) ? . message || '工作台数据加载失败,请稍后重试。' ;
} finally {
isLoading . value = false ;
}
}
async function renderCharts ( ) {
await nextTick ( ) ;
renderTrendChart ( ) ;
}
function renderTrendChart ( ) {
const xAxisData = trends . value . map ( ( item ) => item . label ) ;
const activeUserData = trends . value . map ( ( item ) => item . activeUserTotal ) ;
renderTrendEcharts ( {
color : [ 'hsl(var(--primary))' ] ,
grid : {
bottom : 18 ,
containLabel : true ,
left : 12 ,
right : 12 ,
top : 24 ,
} ,
legend : {
itemGap : 18 ,
top : 0 ,
} ,
tooltip : {
trigger : 'axis' ,
} ,
xAxis : {
axisLine : {
lineStyle : {
color : 'hsl(var(--border))' ,
} ,
} ,
axisTick : {
show : false ,
} ,
data : xAxisData ,
type : 'category' ,
} ,
yAxis : {
axisLine : {
show : false ,
} ,
axisTick : {
show : false ,
} ,
splitLine : {
lineStyle : {
color : 'hsl(var(--border))' ,
type : 'dashed' ,
} ,
} ,
type : 'value' ,
} ,
series : [
{
data : activeUserData ,
name : '活跃用户' ,
smooth : true ,
symbolSize : 8 ,
type : 'line' ,
} ,
] ,
} ) ;
}
function handleRangeChange ( value : boolean | number | string | undefined ) {
if ( value !== 'today' && value !== '7d' && value !== '30d' ) {
return ;
}
filters . range = value ;
void loadOverview ( ) ;
}
function formatCount ( value ? : number ) {
return Number ( value || 0 ) . toLocaleString ( 'zh-CN' ) ;
}
function formatDateTime ( value : string ) {
const date = new Date ( value ) ;
if ( Number . isNaN ( date . getTime ( ) ) ) {
return value ;
}
return date . toLocaleString ( 'zh-CN' , {
hour12 : false ,
} ) ;
}
onMounted ( async ( ) => {
greetingTimer = setInterval ( ( ) => {
now . value = new Date ( ) ;
} , 60 * 1000 ) ;
await loadOverview ( ) ;
} ) ;
onBeforeUnmount ( ( ) => {
if ( greetingTimer ) {
clearInterval ( greetingTimer ) ;
greetingTimer = null ;
}
} ) ;
< / script >
< template >
< div class = "p-5 " >
< WorkbenchHeader
: avatar = "userStore.userInfo?.avatar || preferences.app.defaultAvatar"
>
< template # title >
早安 , { { userStore . userInfo ? . realName } } , 开始您一天的工作吧 !
< / template >
< template # description > 今日晴 , 20 ℃ - 32 ℃ ! < / template >
< / WorkbenchHeader >
< div class = "space-y-6 px-5 pb-5 pt-1 " >
< section >
< div
class = "flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"
>
< div >
< div class = "text-3xl font-semibold tracking-tight lg:text-4xl" >
{ { greetingTitle } }
< / div >
< / div >
< div
class = "flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-end"
>
< ElRadioGroup
:model-value = "filters.range"
size = "default"
@ update :model-value = "handleRangeChange"
>
< ElRadioButton
v-for = "item in rangeOptions"
:key = "item.value"
:value = "item.value"
:label = "item.label"
>
{ { item . label } }
< / ElRadioButton >
< / ElRadioGroup >
< div class = "mt-5 flex flex-col lg:flex-row" >
< div class = "mr-4 w-full lg:w-3/5" >
< WorkbenchProject :items = "projectItems" title = "项目" @click ="navTo" / >
< WorkbenchTrends :items = "trendItems" class = "mt-5" title = "最新动态" / >
< ElButton :icon = "RefreshRight" @click ="loadOverview" > 刷新 < / ElButton >
< / div >
< / div >
< div class = "w-full lg:w-2/5" >
< WorkbenchQuickNav
:items = "quickNavItems"
class = "mt-5 lg:mt-0"
title = "快捷导航"
@click ="navTo"
/ >
< WorkbenchTodo :items = "todoItems" class = "mt-5" title = "待办事项" / >
< AnalysisChartCard class = "mt-5" title = "访问来源" >
< AnalyticsVisitsSource / >
< div class = "text-muted-foreground mt-4 text-sm" >
最新更新时间 : { { updatedAtText } }
< / div >
< / section >
< section
v-if = "isLoading && !overview"
class = "grid gap-4 md:grid-cols-2 xl:grid-cols-4"
>
< div
v-for = "item in 8"
:key = "item"
class = "border-border bg-muted/50 h-28 animate-pulse rounded-3xl border"
> < / div >
< / section >
< section
v-else-if = "errorMessage"
class = "border-border bg-card rounded-3xl border border-dashed p-10"
>
< ElEmpty description = "工作台加载失败" >
< template # default >
< div class = "space-y-3 text-center" >
< p class = "text-muted-foreground text-sm" > { { errorMessage } } < / p >
< ElButton type = "primary" @click ="loadOverview" > 重新加载 < / ElButton >
< / div >
< / template >
< / ElEmpty >
< / section >
< template v-else >
< section class = "grid gap-4 sm:grid-cols-2 xl:grid-cols-5" >
< div
v-for = "item in summaryCards"
:key = "item.label"
class = "border-border/70 bg-card rounded-3xl border px-5 py-5 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm"
>
< div class = "text-muted-foreground text-xs font-medium" >
{ { item . label } }
< / div >
< div
class = "text-foreground mt-3 text-3xl font-semibold tracking-tight"
>
{ { item . value } }
< / div >
< / div >
< / section >
< section >
< AnalysisChartCard title = "趋势变化" >
< div class = "space-y-2" >
< p class = "text-muted-foreground text-sm" >
观察活跃用户在所选时间范围内的变化趋势 。
< / p >
< EchartsUI ref = "trendChartRef" height = "360px" / >
< / div >
< / AnalysisChartCard >
< / div >
< / div >
< / section >
< / template >
< / div >
< / template >