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.

1080 lines
27 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="home-page">
<div class="home-welcome">
<div class="home-welcome-left">
<div class="home-welcome-title text-white!">欢迎使用您的云上数字工厂</div>
<div class="home-welcome-desc text-white! opacity-80">
云上数字工厂可以整合您工厂所有设备自动化信息化的教据帮助您对工厂和生产进行建模管理统一主教报及应用集成打造您工厂的教字中心和业务中心并且与产业链相关系统协同不断提升您的智能制造能力
</div>
</div>
<div class="home-welcome-right">
<el-image
alt="banner" :src="bannerImg" fit="contain" class="home-welcome-image"
style="height: 180px;margin-right:100px" />
</div>
</div>
<el-card shadow="never" class="home-section">
<div class="section-header">
<div class="section-title">整体生产概况</div>
<div>
<el-date-picker
v-model="productionOverviewRange" type="daterange" unlink-panels value-format="YYYY-MM-DD"
start-placeholder="开始日期" end-placeholder="结束日期" size="small" />
</div>
</div>
<el-row class="production-overview-row" :gutter="0">
<el-col :span="10" class="production-overview-group production-overview-group-left">
<div v-for="item in productionOverviewLeft" :key="item.key" class="production-overview-item">
<div class="production-overview-label">{{ item.label }}</div>
<div class="production-overview-value">{{ item.value }}</div>
</div>
</el-col>
<el-col :span="10" class="production-overview-group production-overview-group-center">
<div v-for="item in productionOverviewCenter" :key="item.key" class="production-overview-item">
<div class="production-overview-label">{{ item.label }}</div>
<div class="production-overview-value production-overview-value-primary">{{ item.value }}</div>
</div>
</el-col>
<el-col :span="4" class="production-overview-group production-overview-group-right">
<div v-for="item in productionOverviewRight" :key="item.key" class="production-overview-item">
<div class="production-overview-label">{{ item.label }}</div>
<div class="production-overview-value">{{ item.value }}</div>
</div>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="home-section">
<div class="section-header">
<div class="section-title">实时生产进度</div>
</div>
<el-carousel height="190px" arrow="always" indicator-position="none" :interval="15000" class="progress-carousel">
<el-carousel-item v-for="(group, index) in productionProgressGroups" :key="index">
<el-row :gutter="16">
<el-col v-for="item in group" :key="item.id" :xl="8" :lg="8" :md="8" :sm="12" :xs="24">
<div class="progress-card">
<div class="progress-card-header">生产工单 {{ item.orderNo }}</div>
<div class="progress-card-body">
<div class="progress-col">
<div class="progress-row">
<span class="progress-label">产品名称</span>
<span class="progress-value">{{ item.productName }}</span>
</div>
<div class="progress-row">
<span class="progress-label">任务数量</span>
<span class="progress-value">-</span>
</div>
<div class="progress-row">
<span class="progress-label">客户简称</span>
<span class="progress-value">-</span>
</div>
<div class="progress-row">
<span class="progress-label">交货时间</span>
<span class="progress-value">{{ item.planEndTime }}</span>
</div>
<div class="progress-row">
<span class="progress-label">投产时间</span>
<span class="progress-value">{{ item.planStartTime }}</span>
</div>
</div>
<div class="progress-col">
<div class="progress-row">
<span class="progress-label">产品型号</span>
<span class="progress-value">-</span>
</div>
<div class="progress-row">
<span class="progress-label">完成数量</span>
<span class="progress-value">-</span>
</div>
<div class="progress-row">
<span class="progress-label">完成进度</span>
<span class="progress-value">{{ item.completeRate }}%</span>
</div>
<div class="progress-row">
<span class="progress-label">工单状态</span>
<span class="progress-value">{{ item.statusText }}</span>
</div>
<div class="progress-row">
<span class="progress-label">完工时间</span>
<span class="progress-value">-</span>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</el-carousel-item>
</el-carousel>
</el-card>
<el-row :gutter="16" class="home-section device-alarm-section">
<el-col :xl="12" :lg="12" :md="24" :sm="24" :xs="24" class="device-alarm-col">
<el-card shadow="never" class="device-alarm-card">
<div class="section-header">
<div class="section-title">设备</div>
<div class="section-actions">
<el-button type="default" size="small">查看文档</el-button>
<el-button type="primary" size="small">添加设备</el-button>
</div>
</div>
<el-row :gutter="12" class="mt-16px">
<el-col v-for="item in deviceStatusCards" :key="item.key" :span="8">
<div class="mini-card" :class="item.level">
<div class="mini-label">{{ item.label }}</div>
<div class="mini-value">{{ item.value }}</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :xl="12" :lg="12" :md="24" :sm="24" :xs="24" class="device-alarm-col mt-16px xl:mt-0 lg:mt-0">
<el-card shadow="never" class="device-alarm-card">
<div class="section-header">
<div class="section-title">告警</div>
</div>
<el-row :gutter="12" class="mt-16px">
<el-col v-for="item in alarmStatusCards" :key="item.key" :span="8">
<div class="mini-card" :class="item.level">
<div class="mini-label">{{ item.label }}</div>
<div class="mini-value">{{ item.value }}</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" class="home-section">
<div class="section-header">
<div class="section-title">设备整体情况</div>
<div>
<el-date-picker
v-model="deviceOverviewRange" type="daterange" unlink-panels value-format="YYYY-MM-DD"
start-placeholder="开始日期" end-placeholder="结束日期" size="small" />
</div>
</div>
<el-row class="device-overview-row" :gutter="0">
<el-col v-for="item in deviceOverviewTop" :key="item.key" :span="2">
<div class="device-overview-item">
<div class="production-overview-label">{{ item.label }}</div>
<div class="production-overview-value production-overview-value-primary">{{ item.value }}</div>
</div>
</el-col>
<el-col v-for="item in deviceOverviewBottom" :key="item.key" :span="2">
<div class="device-overview-item">
<div class="production-overview-label">{{ item.label }}</div>
<div class="production-overview-value production-overview-value-primary">{{ item.value }}</div>
</div>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="home-section">
<div class="section-header">
<div class="section-title">待办任务</div>
</div>
<el-carousel height="160px" arrow="always" indicator-position="none" class="todo-carousel" :interval="15000">
<el-carousel-item v-for="(group, index) in todoTaskGroups" :key="index">
<el-row :gutter="16">
<el-col v-for="item in group" :key="item.id" :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<div class="todo-card">
<div class="todo-title">{{ item.name }}</div>
<div class="todo-sub">任务编号:{{ item.id }}</div>
<div class="todo-sub">任务类型:{{ item.type }}|优先级:{{ item.priority }}</div>
<div class="todo-sub">计划时间:{{ item.planTime }}</div>
<div class="todo-sub">责任部门:{{ item.owner }}</div>
</div>
</el-col>
</el-row>
</el-carousel-item>
</el-carousel>
</el-card>
<el-row :gutter="16" class="home-section">
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-16px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>设备维修数量统计</span>
</div>
</template>
<Echart :options="deviceRepairLineOptionsData" :height="260" />
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-16px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>设备分类统计</span>
</div>
</template>
<Echart :options="deviceCategoryPieOptionsData" :height="260" />
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-16px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>设备分布统计</span>
</div>
</template>
<Echart :options="deviceDistributionBarOptionsData" :height="260" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="home-section" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-16px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>每周用户活跃量</span>
</div>
</template>
<Echart :options="barOptionsData" :height="260" />
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
<el-card shadow="never" class="mb-16px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center" @click="navigateToRoute(item.url)">
<Icon :icon="item.icon" class="mr-8px" />
<el-link type="default" :underline="false" @click="setWatermark(item.name)">
{{ item.name }}
</el-link>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<el-avatar :src="avatar" :size="35" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
</div>
<div class="mt-16px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
</div>
</div>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row>
<!-- 旧首页布局保留已通过新模块替代展示 -->
</div>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { useUserStore } from '@/store/modules/user'
import { useWatermark } from '@/hooks/web/useWatermark'
import type {
WorkplaceTotal,
Project,
Notice,
Shortcut,
ProductionOverview,
ProductionProgressItem,
DeviceStatusSummary,
AlarmStatusSummary,
DeviceOverview,
TodoTask
} from './types'
import {
pieOptions,
barOptions,
deviceRepairLineOptions,
deviceCategoryPieOptions,
deviceDistributionBarOptions,
productionOverviewMock,
productionProgressMock,
deviceStatusMock,
alarmStatusMock,
deviceOverviewMock,
todoTasksMock
} from './echarts-data'
import { HomeApi } from '@/api/home/info'
import { WeatherVO } from '@/api/home/info'
import bannerImg from '@/assets/imgs/banner.png'
defineOptions({ name: 'Home' })
const { t } = useI18n()
const userStore = useUserStore()
const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
const deviceRepairLineOptionsData = reactive<EChartsOption>(
deviceRepairLineOptions
) as EChartsOption
const deviceCategoryPieOptionsData = reactive<EChartsOption>(
deviceCategoryPieOptions
) as EChartsOption
const deviceDistributionBarOptionsData = reactive<EChartsOption>(
deviceDistributionBarOptions
) as EChartsOption
const productionOverview = ref<ProductionOverview>(productionOverviewMock)
const productionOverviewRange = ref<string[]>([])
const deviceOverview = ref<DeviceOverview>(deviceOverviewMock)
const deviceOverviewRange = ref<string[]>([])
const productionProgressTab = ref('all')
const productionProgressList = ref<ProductionProgressItem[]>(productionProgressMock)
const todoTaskList = ref<TodoTask[]>(todoTasksMock)
const deviceStatus = ref<DeviceStatusSummary>(deviceStatusMock)
const alarmStatus = ref<AlarmStatusSummary>(alarmStatusMock)
const filteredProductionProgressList = computed(() => {
return productionProgressList.value
})
const formatPercent = (value: number | undefined | null) => {
if (value === null || value === undefined) return '0%'
if (Number.isNaN(value)) return '0%'
return `${value}%`
}
const productionOverviewLeft = computed(() => [
{
key: 'orderCount',
label: '生产订单数',
value: productionOverview.value.orderCount
},
{
key: 'runningOrderCount',
label: '生产工单数',
value: productionOverview.value.runningOrderCount
},
{
key: 'startedOrderCount',
label: '开工工单数',
value: productionOverview.value.startedOrderCount
},
{
key: 'shutdownOrderCount',
label: '完工工单数',
value: productionOverview.value.shutdownOrderCount
}
])
const productionOverviewCenter = computed(() => [
{
key: 'plannedOutput',
label: '计划生产数量',
value: productionOverview.value.plannedOutput?.toLocaleString()
},
{
key: 'completedOutput',
label: '完成生产数量',
value: productionOverview.value.completedOutput?.toLocaleString()
},
{
key: 'completionRate',
label: '完工率',
value: formatPercent(productionOverview.value.qualifiedRate)
},
{
key: 'qualifiedRate',
label: '合格率',
value: formatPercent(productionOverview.value.alarmRate)
}
])
const productionOverviewRight = computed(() => [
{
key: 'attendanceCount',
label: '出勤人数',
value: productionOverview.value.onTimeRate ?? 0
},
{
key: 'attendanceRate',
label: '出勤率',
value: formatPercent(productionOverview.value.onTimeRate)
}
])
const deviceOverviewTop = computed(() => [
{
key: 'deviceTotal',
label: '总设备数',
value: deviceOverview.value.deviceTotal?.toLocaleString()
},
{
key: 'realtimeAlarm',
label: '实时告警',
value: deviceOverview.value.needRepair ?? 0
},
{
key: 'running',
label: '运行',
value: deviceOverview.value.running ?? 0
},
{
key: 'stop',
label: '停机',
value: deviceOverview.value.stop ?? 0
},
{
key: 'standby',
label: '待机',
value: 0
},
{
key: 'utilization',
label: '利用率',
value: formatPercent(deviceOverview.value.utilization)
},
{
key: 'faultRate',
label: '故障率',
value: formatPercent(deviceOverview.value.alarmRate)
}
])
const deviceOverviewBottom = computed(() => [
{
key: 'faultCount',
label: '设备故障台数',
value: deviceOverview.value.needRepair ?? 0
},
{
key: 'faultTime',
label: '设备故障时间',
value: 0
},
{
key: 'mtbf',
label: '平均故障间隔时间',
value: 0
},
{
key: 'mttr',
label: '平均故障时间',
value: 0
}
])
const deviceStatusCards = computed(() => [
{
key: 'inactive',
label: '非活动',
value: deviceStatus.value.inactive,
level: 'mini-danger'
},
{
key: 'active',
label: '活动',
value: deviceStatus.value.active,
level: 'mini-normal'
},
{
key: 'total',
label: '总数',
value: deviceStatus.value.total,
level: 'mini-total'
}
])
const alarmStatusCards = computed(() => [
{
key: 'serious',
label: '严重',
value: alarmStatus.value.serious,
level: 'mini-danger'
},
{
key: 'assignedToMe',
label: '分配给我',
value: alarmStatus.value.assignedToMe,
level: 'mini-normal'
},
{
key: 'total',
label: '总数',
value: alarmStatus.value.total,
level: 'mini-total'
}
])
const groupBySize = <T,>(list: T[], size: number) => {
const result: T[][] = []
for (let i = 0; i < list.length; i += size) {
result.push(list.slice(i, i + size))
}
return result
}
const productionProgressGroups = computed(() => groupBySize(filteredProductionProgressList.value, 3))
const todoTaskGroups = computed(() => groupBySize(todoTaskList.value, 4))
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
const weatherList = ref<WeatherVO[]>([])
const weatherEnable = ref(false)
const todayWeather = ref({} as WeatherVO)
let weatherCity = ""
/** 初始化 **/
onMounted(async () => {
// 加载
const data = await HomeApi.getWeatherInfo()
weatherEnable.value = data.isEnable
if (data.isEnable && weatherList) {
weatherList.value = data.casts
todayWeather.value = weatherList.value[0]
weatherCity = data.city
}
})
const getCount = async () => {
const data = {
project: 40,
access: 2340,
todo: 10
}
totalSate = Object.assign(totalSate, data)
}
// 获取项目数
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'akar-icons:github-fill',
message: 'https://github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date()
},
{
name: 'yudao-ui-admin-vue3',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus',
time: new Date()
},
{
name: 'yudao-ui-admin-vben',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben(antd)',
time: new Date()
},
{
name: 'yudao-cloud',
icon: 'akar-icons:github',
message: 'https://github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date()
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp',
time: new Date()
},
{
name: 'yudao-ui-admin-vue2',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
personal: 'Vue2 + element-ui',
time: new Date()
}
]
projects = Object.assign(projects, data)
}
// 获取通知公告
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '厂区进行防火演练通知',
type: '通知',
keys: ['通知', '8', '17', '21', '2', '3'],
date: new Date()
},
{
title: '职位调整公告',
type: '公告',
keys: ['公告', 'Boot', 'Cloud'],
date: new Date()
},
{
title: '端午放假通知',
type: '通知',
keys: ['通知', '无需授权'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
// 获取快捷入口
let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: '生产任务',
icon: 'akar-icons:github-fill',
url: '/mes/plan'
},
{
name: '用户管理',
icon: 'logos:vue',
url: '/system/user'
},
{
name: '采购订单',
icon: 'vscode-icons:file-type-vite',
url: '/erp/purchase/order'
},
{
name: '报表看板',
icon: 'logos:angular-icon',
url: '/erp/home'
},
{
name: '物联数据',
icon: 'logos:react',
url: '/iot/kanban'
},
{
name: '仓库管理',
icon: 'logos:webpack',
url: '/erp/stock/warehouse'
}
]
shortcut = Object.assign(shortcut, data)
}
// 跳转路由
const router = useRouter()
const navigateToRoute = (url: string) => {
router.push(url)
}
const jumpToRoute = (url: string) => {
window.open(url, '_blank'); // 在新标签页中打开
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getKanban(),
getUserAccessSource(),
getWeeklyUserActivity()
])
loading.value = false
}
// 获取快捷入口
let kanban = reactive<Shortcut[]>([])
const getKanban = async () => {
const data = [
{
name: '原料能源大屏',
icon: 'akar-icons:github-fill',
url: 'http://111.67.199.122:8100/#/de-link/RDdq31Ky'
},
{
name: '库存监控大屏',
icon: 'logos:vue',
url: 'http://111.67.199.122:8100/#/de-link/uACZrxMr'
},
{
name: '采购大屏',
icon: 'vscode-icons:file-type-vite',
url: 'http://111.67.199.122:8100/#/de-link/kFI91k27'
},
{
name: '销售大屏',
icon: 'logos:angular-icon',
url: 'http://111.67.199.122:8100/#/de-link/EPQolq4g'
},
{
name: '生产管控大屏',
icon: 'logos:react',
url: 'http://111.67.199.122:8100/#/de-link/IMZeQ9Mr'
},
{
name: '仓库管理',
icon: 'logos:webpack',
url: '/erp/stock/warehouse'
}
]
kanban = Object.assign(kanban, data)
}
getAllApi()
</script>
<style scoped>
.home-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.home-section {
margin-bottom: 16px;
}
.home-welcome {
min-height: 240px;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(90deg, #3a6bc5 0%, #2a5298 100%);
padding: 0 24px;
border-radius: 4px;
}
.home-welcome-left {
max-width: 60%;
}
.home-welcome-title {
font-size: 32px;
font-weight: 500;
margin-bottom: 8px;
}
.home-welcome-desc {
font-size: 14px;
color: #909399;
}
.home-welcome-right img {
width: 360px;
height: auto;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title {
font-size: 16px;
font-weight: 600;
}
.section-actions {
display: flex;
gap: 8px;
}
.device-alarm-section {
align-items: stretch;
}
.device-alarm-col {
display: flex;
}
.device-alarm-card {
flex: 1;
display: flex;
flex-direction: column;
}
.device-alarm-card .section-header {
min-height: 32px;
}
.home-welcome-right {
display: flex;
align-items: flex-end;
}
.home-welcome-image {
max-height: 180px;
width: auto;
}
.production-overview-row {
margin-top: 16px;
display: flex;
align-items: stretch;
gap: 48px;
}
.production-overview-group {
display: flex;
flex: 1;
justify-content: space-between;
gap: 32px;
}
.production-overview-group-center {
border-left: 1px solid var(--el-border-color-lighter);
border-right: 1px solid var(--el-border-color-lighter);
padding: 0 48px;
}
.production-overview-item {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.production-overview-label {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.production-overview-value {
margin-top: 8px;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.production-overview-value-primary {
color: #409eff;
}
.device-overview-row {
margin-top: 12px;
}
.device-overview-row-bottom {
margin-top: 4px;
}
.device-overview-item {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0 16px;
}
:deep(.device-overview-row .el-col:not(:last-child) .device-overview-item) {
border-right: 1px solid var(--el-border-color-lighter);
}
.stat-card {
padding: 12px 16px;
border-radius: 6px;
background-color: #f5f7fa;
}
.stat-label {
font-size: 13px;
color: #909399;
}
.stat-value {
margin-top: 8px;
font-size: 20px;
font-weight: 600;
}
.stat-primary {
color: #409eff;
}
.stat-danger {
color: #f56c6c;
}
.progress-carousel,
.todo-carousel {
margin-top: 16px;
}
.progress-carousel :deep(.el-carousel__arrow) {
top: 50%;
transform: translateY(-50%);
}
.progress-carousel :deep(.el-carousel__arrow--left) {
left: -8px;
}
.progress-carousel :deep(.el-carousel__arrow--right) {
right: -8px;
}
.progress-card {
padding: 12px 16px;
border-radius: 6px;
background-color: #f5f7fa;
height: 180px;
display: flex;
flex-direction: column;
}
.progress-card-header {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.progress-card-body {
display: flex;
gap: 40px;
}
.progress-col {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-row {
display: flex;
font-size: 12px;
}
.progress-label {
width: 80px;
flex-shrink: 0;
color: #909399;
}
.progress-value {
flex: 1;
color: #303133;
word-break: break-word;
}
.mini-card {
padding: 12px 16px;
border-radius: 6px;
background-color: #f5f7fa;
}
.mini-label {
font-size: 13px;
color: #909399;
}
.mini-value {
margin-top: 8px;
font-size: 18px;
font-weight: 600;
}
.mini-danger {
background-color: #fde2e2;
}
.mini-normal {
background-color: #f5f7fa;
}
.mini-total {
background-color: #e9f5ff;
}
.todo-card {
padding: 12px 16px;
border-radius: 6px;
background-color: #f5f7fa;
height: 150px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.todo-title {
font-size: 14px;
font-weight: 600;
}
.todo-sub {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
@media (max-width: 768px) {
.home-welcome {
flex-direction: column;
align-items: flex-start;
}
.home-welcome-left {
max-width: 100%;
margin-bottom: 12px;
}
.home-welcome-right img {
width: 100%;
}
}
</style>