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.

1075 lines
28 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.code }}
<el-tag :type="getPlanStatusTagType(item.status)" effect="light">
{{ getPlanStatusLabel(item.status) }}
</el-tag>
</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">{{ item.planNumber }}</span>
</div>
<div class="progress-row">
<span class="progress-label">生产线</span>
<span class="progress-value">{{ item.feedingPipelineName }}</span>
</div>
<div class="progress-row">
<span class="progress-label">计划开始时间</span>
<span class="progress-value">{{ formatDate(item.planStartTime) }}</span>
</div>
<div class="progress-row">
<span class="progress-label">计划结束时间</span>
<span class="progress-value">{{ formatDate(item.planEndTime) }}</span>
</div>
</div>
<div class="progress-col">
<div class="progress-row">
<span class="progress-label">完工数量</span>
<span class="progress-value">{{ item.wangongNumber }}</span>
</div>
<div class="progress-row">
<span class="progress-label">合格数量</span>
<span class="progress-value">{{ item.passNumber }}</span>
</div>
<div class="progress-row">
<span class="progress-label">不合格数量</span>
<span class="progress-value">{{ item.noPassNumber }}%</span>
</div>
<div class="progress-row">
<span class="progress-label">合格率</span>
<span class="progress-value">{{ item.passRate }}</span>
</div>
<div class="progress-row">
<span class="progress-label">实际结束时间</span>
<span class="progress-value">{{ formatDate(item.endTime) }}</span>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</el-carousel-item>
</el-carousel>
</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.code }}</div>
<div class="todo-sub">任务类型:{{ item.type }}</div>
<div class="todo-sub">目标:{{ item.deviceName }}</div>
<div class="todo-sub">创建时间:{{ formatDate(item.createTime) }}</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 moldStatusCards" :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" v-model = "formData">
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">总设备数</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.totalDevices }}</div>
</div>
</el-col>
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">运行</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.runningCount }}</div>
</div>
</el-col>
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">待机中</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.standbyCount }}</div>
</div>
</el-col>
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">故障中</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.faultCount }}</div>
</div>
</el-col>
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">报警中</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.warningCount }}</div>
</div>
</el-col>
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">利用率</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.utilizationRate }}</div>
</div>
</el-col>
<el-col :span="2">
<div class="device-overview-item">
<div class="production-overview-label">故障率</div>
<div class="production-overview-value production-overview-value-primary">{{ formData?.faultRate }}</div>
</div>
</el-col>
</el-row>
</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="deviceRepairLineOptions" :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="moldTypeBarOptionsData" :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 { useUserStore } from '@/store/modules/user'
import { useWatermark } from '@/hooks/web/useWatermark'
import type {
WorkplaceTotal,
Project,
Notice,
Shortcut,
ProductionProgressItem,
TodoTask,
DeviceOperationStatus
} from './types'
import {
pieOptions,
barOptions,
} from './echarts-data'
import { WeatherVO } from '@/api/home/info'
import bannerImg from '@/assets/imgs/banner.png'
import { DashboardApi,DashboardProductVO,DeviceStatusVO } from '@/api/dashboard'
import { formatDate } from '@/utils/formatTime'
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 productionOverviewRange = ref<string[]>([])
const formatPercent = (value: number | undefined | null) => {
if (value === null || value === undefined) return '0%'
if (Number.isNaN(value)) return '0%'
return `${value}%`
}
const productionOverviewLeft = ref<{ key: string; label: string; value: number; }[]>([])
const productionOverviewCenter = ref<{ key: string; label: string; value: number; }[]>([])
const plan = ref<ProductionProgressItem[]>([])
const todo = ref<TodoTask[]>([])
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
}
// 获取统计数
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 = ""
const production = ref({} as DashboardProductVO)
const productionProgressGroups = computed(() => groupBySize(plan.value, 3))
const todoTaskGroups = computed(() => groupBySize(todo.value, 4))
const deviceStatusCards = ref<DeviceStatusVO[]>([])
const moldStatusCards = ref<DeviceStatusVO[]>([])
const formData = ref<DeviceOperationStatus>()
const deviceRepairLineOptions = ref<EChartsOption>({
title: {
text: '设备维修数量统计',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
grid: {
left: 40,
right: 30,
bottom: 50,
top: 60,
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value'
},
series: [
{
name: '维修数量',
type: 'line',
smooth: true,
// data: [3, 5, 4, 6, 8, 7]
}
]
})
const deviceCategoryPieOptionsData = ref<EChartsOption>(
{title: {
text: '设备分类统计',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '设备类别',
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
}
]
}
)
const moldTypeBarOptionsData = ref<EChartsOption>({
title: {
text: '模具分类统计',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 40,
right: 20,
bottom: 30,
top: 60,
containLabel: true
},
xAxis: {
type: 'category',
// data: ['一车间', '二车间', '三车间', '四车间', '五车间'],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: '模具数量',
type: 'bar',
// data: [10, 8, 6, 5, 4]
}
]
}
)
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
/** 初始化 **/
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
// }
production.value = await DashboardApi.getProduction(productionOverviewRange.value)
plan.value = await DashboardApi.getPlan()
deviceStatusCards.value = await DashboardApi.getDevice()
moldStatusCards.value = await DashboardApi.getMold()
todo.value = await DashboardApi.getTodoList()
formData.value = await DashboardApi.getDeviceOperationalStatus()
const Options = await DashboardApi.getDeviceRepairLineOptions()
deviceRepairLineOptions.value.xAxis!.data = Options.xaxis
deviceRepairLineOptions.value.series![0].data = Options.series
const deviceTypeOptions = await DashboardApi.getDeviceTypePieOptions()
deviceCategoryPieOptionsData.value.series![0].data = deviceTypeOptions
const moldTypeOptions = await DashboardApi.getMoldTypeBarOptions()
console.log(moldTypeOptions)
moldTypeBarOptionsData.value.xAxis!.data = moldTypeOptions.xaxis
moldTypeBarOptionsData.value.series![0].data = moldTypeOptions.series
productionOverviewLeft.value = production.value.taskItems
productionOverviewCenter.value = production.value.planItems
})
const getPlanStatusLabel = (value: any) => {
const v = value === '' || value === null || value === undefined ? undefined : String(value)
if (v == '1') return '已排产'
if (v == '6') return '试产'
if (v == '2') return '量产'
if (v == '3') return '暂停'
if (v == '4') return '待入库'
return '-'
}
const getPlanStatusTagType = (value: any) => {
const v = value === '' || value === null || value === undefined ? undefined : String(value)
if (v === '1') return 'success'
if (v === '0') return 'warning'
return 'info'
}
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 {
display: flex;
min-height: 240px;
padding: 0 24px;
background: linear-gradient(90deg, #3a6bc5 0%, #2a5298 100%);
border-radius: 4px;
justify-content: space-between;
align-items: center;
}
.home-welcome-left {
max-width: 60%;
}
.home-welcome-title {
margin-bottom: 8px;
font-size: 32px;
font-weight: 500;
}
.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 {
width: auto;
max-height: 180px;
}
.production-overview-row {
display: flex;
margin-top: 16px;
align-items: stretch;
gap: 48px;
}
.production-overview-group {
display: flex;
flex: 1;
justify-content: space-between;
gap: 32px;
}
.production-overview-group-center {
padding: 0 48px;
border-right: 1px solid var(--el-border-color-lighter);
border-left: 1px solid var(--el-border-color-lighter);
}
.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;
background-color: #f5f7fa;
border-radius: 6px;
}
.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) {
transform: translate(30px, -50%); /* X轴移动30pxY轴居中 */
}
/* 右箭头往左移动 */
.progress-carousel :deep(.el-carousel__arrow--right) {
transform: translate(-30px, -50%); /* X轴移动-30pxY轴居中 */
}
.progress-carousel :deep(.el-carousel__arrow--left) {
left: -8px;
}
.progress-carousel :deep(.el-carousel__arrow--right) {
right: -8px;
}
.progress-card {
display: flex;
height: 180px;
padding: 12px 16px;
background-color: #f5f7fa;
border-radius: 6px;
flex-direction: column;
}
.progress-card-header {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.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;
background-color: #f5f7fa;
border-radius: 6px;
}
.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 {
display: flex;
height: 150px;
padding: 12px 16px;
background-color: #f5f7fa;
border-radius: 6px;
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 (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>