You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

487 lines
16 KiB
Vue

<template>
<div class="app-container home">
<!-- 欢迎栏 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="24">
<el-card shadow="never">
<div class="welcome-header">
<div>
<h2 class="welcome-title">{{ $t('dashboard.welcome') }}</h2>
<p class="welcome-desc">{{ $t('dashboard.description') }}</p>
</div>
<div class="welcome-meta">
<el-tag type="primary">{{ currentDate }}</el-tag>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 核心指标 -->
<el-row :gutter="20" class="mb-4">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" v-for="item in statCards" :key="item.key">
<el-card shadow="hover" class="stat-card" @click="goTarget(item.path)">
<div class="stat-icon" :style="{ background: item.color }">
<svg-icon :icon-class="item.icon" />
</div>
<div class="stat-info">
<div class="stat-value">{{ item.value }}</div>
<div class="stat-label">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 状态分布图表 -->
<el-row :gutter="20" class="mb-4">
<el-col :xs="24" :md="12" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.taskStatus') }}</span>
</template>
<div ref="taskStatusChartRef" class="chart-box"></div>
</el-card>
</el-col>
<el-col :xs="24" :md="12" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.deviceStatus') }}</span>
</template>
<div ref="deviceStatusChartRef" class="chart-box"></div>
</el-card>
</el-col>
<el-col :xs="24" :md="12" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.locationStatus') }}</span>
</template>
<div ref="locationStatusChartRef" class="chart-box"></div>
</el-card>
</el-col>
</el-row>
<!-- 最近任务与快捷入口 -->
<el-row :gutter="20">
<el-col :xs="24" :lg="16" class="mb-4">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>{{ $t('dashboard.recentTasks') }}</span>
<el-button link type="primary" @click="goTarget('/base/taskQueue')">{{ $t('dashboard.more') }}</el-button>
</div>
</template>
<el-table v-loading="loading" :data="recentTasks" stripe>
<el-table-column prop="taskCode" :label="$t('dashboard.taskCode')" min-width="120" />
<el-table-column prop="materialName" :label="$t('dashboard.materialName')" min-width="120" />
<el-table-column prop="startPoint" :label="$t('dashboard.startPoint')" min-width="100" />
<el-table-column prop="endPoint" :label="$t('dashboard.endPoint')" min-width="100" />
<el-table-column prop="taskStatus" :label="$t('dashboard.taskStatus')" width="100">
<template #default="scope">
<dict-tag :options="wcs_task_status" :value="scope.row.taskStatus" />
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('dashboard.createTime')" width="170">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, CREATED_TIME_DISPLAY_FORMAT) }}</span>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.quickLinks') }}</span>
</template>
<div class="quick-link-list">
<div v-for="link in quickLinks" :key="link.path" class="quick-link" @click="goTarget(link.path)">
<svg-icon :icon-class="link.icon" class="quick-link-icon" />
<div class="quick-link-info">
<div class="quick-link-title">{{ link.title }}</div>
<div class="quick-link-desc">{{ link.desc }}</div>
</div>
<svg-icon icon-class="caret-forward" class="quick-link-arrow" />
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup name="Index" lang="ts">
import * as echarts from 'echarts';
import { parseTime } from '@/utils/ruoyi';
import { countTaskQueue, listTaskQueue } from '@/api/wcs/taskQueue';
import { countTaskDetail } from '@/api/wcs/taskDetail';
import { countDeviceInfo } from '@/api/wcs/deviceInfo';
import { countDeviceParam } from '@/api/wcs/deviceParam';
import { countLocationInfo } from '@/api/wcs/locationInfo';
import { countMaterialInfo } from '@/api/wcs/materialInfo';
import { countStoreInfo } from '@/api/wcs/storeInfo';
import { countPathInfo } from '@/api/wcs/pathInfo';
import { countDeviceHost } from '@/api/wcs/deviceHost';
import { TaskQueueVO } from '@/api/wcs/taskQueue/types';
import { CREATED_TIME_DISPLAY_FORMAT } from './wcs/utils/createTimeRange';
import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { t } = useI18n();
const router = useRouter();
const { wcs_task_status } = toRefs<any>(proxy?.useDict('wcs_task_status'));
const loading = ref(false);
const currentDate = ref('');
const statCards = ref([
{ key: 'taskQueue', label: t('dashboard.taskQueue'), value: 0, icon: 'list', color: '#3b82f6', path: '/base/taskQueue' },
{ key: 'taskDetail', label: t('dashboard.taskDetail'), value: 0, icon: 'component', color: '#6366f1', path: '/base/taskDetail' },
{ key: 'deviceHost', label: t('dashboard.deviceHost'), value: 0, icon: 'server', color: '#64748b', path: '/base/deviceHost' },
{ key: 'deviceInfo', label: t('dashboard.deviceInfo'), value: 0, icon: 'chart', color: '#10b981', path: '/base/deviceInfo' },
{ key: 'deviceParam', label: t('dashboard.deviceParam'), value: 0, icon: 'edit', color: '#0ea5e9', path: '/base/deviceParam' },
{ key: 'locationInfo', label: t('dashboard.locationInfo'), value: 0, icon: 'dict', color: '#f59e0b', path: '/base/locationInfo' },
{ key: 'materialInfo', label: t('dashboard.materialInfo'), value: 0, icon: 'excel', color: '#8b5cf6', path: '/base/materialInfo' },
{ key: 'storeInfo', label: t('dashboard.storeInfo'), value: 0, icon: 'tree', color: '#ec4899', path: '/base/storeInfo' },
{ key: 'pathInfo', label: t('dashboard.pathInfo'), value: 0, icon: 'build', color: '#14b8a6', path: '/base/pathInfo' }
]);
const quickLinks = ref([
{ title: t('dashboard.taskQueue'), desc: t('dashboard.taskQueueDesc'), icon: 'list', path: '/base/taskQueue' },
{ title: t('dashboard.taskDetail'), desc: t('dashboard.taskDetailDesc'), icon: 'component', path: '/base/taskDetail' },
{ title: t('dashboard.deviceHost'), desc: t('dashboard.deviceHostDesc'), icon: 'server', path: '/base/deviceHost' },
{ title: t('dashboard.deviceInfo'), desc: t('dashboard.deviceInfoDesc'), icon: 'chart', path: '/base/deviceInfo' },
{ title: t('dashboard.deviceParam'), desc: t('dashboard.deviceParamDesc'), icon: 'edit', path: '/base/deviceParam' },
{ title: t('dashboard.locationInfo'), desc: t('dashboard.locationInfoDesc'), icon: 'dict', path: '/base/locationInfo' },
{ title: t('dashboard.materialInfo'), desc: t('dashboard.materialInfoDesc'), icon: 'excel', path: '/base/materialInfo' },
{ title: t('dashboard.storeInfo'), desc: t('dashboard.storeInfoDesc'), icon: 'tree', path: '/base/storeInfo' },
{ title: t('dashboard.pathInfo'), desc: t('dashboard.pathInfoDesc'), icon: 'build', path: '/base/pathInfo' }
]);
const recentTasks = ref<TaskQueueVO[]>([]);
const taskStatusChartRef = ref<HTMLDivElement>();
const deviceStatusChartRef = ref<HTMLDivElement>();
const locationStatusChartRef = ref<HTMLDivElement>();
let taskStatusChart: echarts.ECharts | null = null;
let deviceStatusChart: echarts.ECharts | null = null;
let locationStatusChart: echarts.ECharts | null = null;
const goTarget = (path: string) => {
if (path.startsWith('http')) {
window.open(path, '__blank');
} else {
router.push(path);
}
};
const formatDate = (date: Date) => {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${weekdays[date.getDay()]}`;
};
const getCount = (res: any) => (res && typeof res.data === 'number' ? res.data : 0);
const loadCounts = async () => {
const [taskQueueRes, taskDetailRes, hostRes, deviceRes, deviceParamRes, locationRes, materialRes, storeRes, pathRes] = await Promise.all([
countTaskQueue(),
countTaskDetail(),
countDeviceHost(),
countDeviceInfo(),
countDeviceParam(),
countLocationInfo(),
countMaterialInfo(),
countStoreInfo(),
countPathInfo()
]);
statCards.value[0].value = getCount(taskQueueRes);
statCards.value[1].value = getCount(taskDetailRes);
statCards.value[2].value = getCount(hostRes);
statCards.value[3].value = getCount(deviceRes);
statCards.value[4].value = getCount(deviceParamRes);
statCards.value[5].value = getCount(locationRes);
statCards.value[6].value = getCount(materialRes);
statCards.value[7].value = getCount(storeRes);
statCards.value[8].value = getCount(pathRes);
};
const loadTaskStatusChart = async () => {
const statusOptions = wcs_task_status.value || [];
if (!statusOptions.length) {
taskStatusChart?.setOption({
title: { text: t('dashboard.noData'), left: 'center', top: 'center', textStyle: { fontSize: 14, color: '#999' } },
series: []
});
return;
}
const data = await Promise.all(
statusOptions.map(async (dict: any) => {
const res = await countTaskQueue({ taskStatus: parseInt(dict.value) });
return { name: dict.label, value: getCount(res) };
})
);
taskStatusChart?.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
type: 'pie',
radius: '65%',
data,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}
]
});
};
const loadDeviceStatusChart = async () => {
const statuses = [
{ label: t('dashboard.deviceNormal'), value: 0 },
{ label: t('dashboard.deviceBusy'), value: 1 },
{ label: t('dashboard.deviceAbnormal'), value: 2 }
];
const data = await Promise.all(
statuses.map(async (status) => {
const res = await countDeviceInfo({ deviceStatus: status.value });
return { name: status.label, value: getCount(res) };
})
);
deviceStatusChart?.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
color: ['#10b981', '#f59e0b', '#ef4444'],
series: [
{
type: 'pie',
radius: ['40%', '65%'],
data,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}
]
});
};
const loadLocationStatusChart = async () => {
const statuses = [
{ label: t('dashboard.locationUnused'), value: 0 },
{ label: t('dashboard.locationUsed'), value: 1 },
{ label: t('dashboard.locationLocked'), value: 2 },
{ label: t('dashboard.locationAbnormal'), value: 3 }
];
const data = await Promise.all(
statuses.map(async (status) => {
const res = await countLocationInfo({ locationStatus: status.value });
return { name: status.label, value: getCount(res) };
})
);
locationStatusChart?.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
color: ['#94a3b8', '#3b82f6', '#f59e0b', '#ef4444'],
series: [
{
type: 'pie',
radius: ['40%', '65%'],
data,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}
]
});
};
const loadRecentTasks = async () => {
loading.value = true;
try {
const res = await listTaskQueue({ pageNum: 1, pageSize: 5, params: {} });
recentTasks.value = res.rows || [];
} finally {
loading.value = false;
}
};
const initCharts = () => {
if (taskStatusChartRef.value) taskStatusChart = echarts.init(taskStatusChartRef.value);
if (deviceStatusChartRef.value) deviceStatusChart = echarts.init(deviceStatusChartRef.value);
if (locationStatusChartRef.value) locationStatusChart = echarts.init(locationStatusChartRef.value);
};
const loadCharts = async () => {
await Promise.all([loadTaskStatusChart(), loadDeviceStatusChart(), loadLocationStatusChart()]);
};
const handleResize = () => {
taskStatusChart?.resize();
deviceStatusChart?.resize();
locationStatusChart?.resize();
};
onMounted(() => {
currentDate.value = formatDate(new Date());
initCharts();
Promise.all([loadCounts(), loadRecentTasks(), loadCharts()]);
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
taskStatusChart?.dispose();
deviceStatusChart?.dispose();
locationStatusChart?.dispose();
});
</script>
<style lang="scss" scoped>
.home {
font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 13px;
color: var(--el-text-color-regular);
overflow-x: hidden;
.mb-4 {
margin-bottom: 16px;
}
.welcome-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
.welcome-title {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.welcome-desc {
margin: 0;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.welcome-meta {
font-size: 14px;
}
}
.stat-card {
cursor: pointer;
transition:
transform 0.2s,
box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: var(--app-shadow-md);
}
:deep(.el-card__body) {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
flex-shrink: 0;
.svg-icon {
width: 26px;
height: 26px;
}
}
.stat-info {
flex: 1;
min-width: 0;
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1.2;
}
.stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
}
}
.chart-box {
width: 100%;
height: 280px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.quick-link-list {
display: flex;
flex-direction: column;
gap: 8px;
.quick-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: var(--app-radius-md);
cursor: pointer;
transition: background 0.2s;
&:hover {
background: var(--el-fill-color-light);
}
.quick-link-icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.quick-link-info {
flex: 1;
min-width: 0;
.quick-link-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.quick-link-desc {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 2px;
}
}
.quick-link-arrow {
color: var(--el-text-color-secondary);
}
}
}
}
</style>