feat:添加第二种样式的大屏页面

main
黄伟杰 1 week ago
parent 0ec8df976c
commit 8fd545671c

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

@ -611,6 +611,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
}
},
{
path: '/iot/report/dashboardPage/Dashboard1',
component: () => import('@/views/report/dashboardPage/dashboard1/Dashboard8.vue'),
name: 'IotReportDashboard1',
meta: {
title: '智能制造产线任务总览',
hidden: true,
noTagsView: true,
canTo: true
}
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/Error/404.vue'),

@ -0,0 +1,197 @@
<template>
<div class="dashboard-container">
<div class="bg-grid"></div>
<div class="bg-scan"><div class="scan-line"></div></div>
<DashboardHeader />
<main>
<div class="layout">
<el-row :gutter="10" class="main-row">
<el-col :span="5" class="col">
<div class="col-item col-item-overview">
<!-- 设备概况 -->
<DeviceOverview />
</div>
<div class="col-item col-item-payment">
<!-- Payment method -->
<PaymentMethod />
</div>
<div class="col-item col-item-extra">
<!-- 产量趋势 -->
<ProductionTrend />
</div>
</el-col>
<el-col :span="14" class="col">
<div class="center-shell">
<img class="dashboard-center-image" src="@/assets/imgs/dashboard_img.png" alt="dashboard" />
</div>
</el-col>
<el-col :span="5" class="col">
<div class="col-item col-item-event">
<!-- 事件提醒 -->
<EventReminder />
</div>
<div class="col-item col-item-task">
<!-- 任务列表 -->
<TaskList />
</div>
<div class="col-item col-item-energy">
<!-- 能耗监测 -->
<EnergyMonitor />
</div>
</el-col>
</el-row>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import DashboardHeader from './components/DashboardHeader.vue'
import DeviceOverview from './components/DeviceOverview.vue'
import PaymentMethod from './components/PaymentMethod.vue'
import EventReminder from './components/EventReminder.vue'
import TaskList from './components/TaskList.vue'
import EnergyMonitor from './components/EnergyMonitor.vue'
import ProductionTrend from './components/ProductionTrend.vue'
</script>
<style scoped>
/* Define CSS Variables locally for this dashboard */
.dashboard-container {
--bg: #050816;
--bg-deep: #020617;
--card-bg: rgba(15, 23, 42, 0.86);
--border: rgba(56, 189, 248, 0.35);
--text: #e5f0ff;
--muted: #94a3b8;
--primary: #38bdf8;
--accent: #22d3ee;
--blue: #1e90ff;
--green: #22c55e;
--purple: #8b5cf6;
--warn: #f59e0b;
--danger: #ef4444;
--gap: 10px;
--header-h: 86px;
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft Yahei", Arial, sans-serif;
color: var(--text);
background-color: var(--bg-deep);
background-image:
radial-gradient(circle at 20% 0, rgba(56, 189, 248, 0.26) 0, transparent 48%),
radial-gradient(circle at 80% 110%, rgba(129, 140, 248, 0.22) 0, transparent 52%),
linear-gradient(135deg, #020617 0%, #020617 45%, #020617 100%);
}
.bg-grid {
position: absolute;
inset: 0;
pointer-events: none;
background-image: linear-gradient(rgba(15,23,42,0.8) 1px, transparent 1px),
linear-gradient(90deg, rgba(15,23,42,0.8) 1px, transparent 1px);
background-size: 70px 70px;
opacity: 0.55;
z-index: 0;
}
.bg-scan {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.scan-line {
position: absolute;
top: -40%;
left: 0;
width: 100%;
height: 40%;
background: radial-gradient(circle at 50% 0, rgba(56, 189, 248, 0.38), transparent 70%);
opacity: 0.5;
filter: blur(32px);
animation: scanDown 16s linear infinite;
}
@keyframes scanDown {
0% { transform: translateY(-100%); }
100% { transform: translateY(260%); }
}
main {
height: calc(100vh - var(--header-h));
padding: var(--gap);
box-sizing: border-box;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: var(--gap);
min-height: 0;
}
.layout {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
gap: var(--gap);
width: 100%;
min-height: 0;
}
.main-row {
flex: 1;
height: 100%;
min-height: 0;
}
.col {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
gap: var(--gap);
}
.col-item {
flex: 1;
min-height: 0;
display: flex;
}
.col-item :deep(> *) {
width: 100%;
height: 100%;
}
.center-shell {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.55);
background: rgba(2,6,23,0.18);
padding: 10px;
}
.dashboard-center-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>

@ -0,0 +1,269 @@
<template>
<header>
<div class="header-inner">
<div class="header-left">
<div class="time">{{ timeStr }}</div>
<div class="date">{{ dateStr }}</div>
</div>
<div class="header-center">
<div class="title-wrap">
<svg class="title-frame" viewBox="0 0 1200 120" preserveAspectRatio="none" aria-hidden="true">
<defs>
<linearGradient id="frameStroke" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="rgba(34,211,238,0.0)" />
<stop offset="0.18" stop-color="rgba(34,211,238,0.85)" />
<stop offset="0.5" stop-color="rgba(96,165,250,0.95)" />
<stop offset="0.82" stop-color="rgba(34,211,238,0.85)" />
<stop offset="1" stop-color="rgba(34,211,238,0.0)" />
</linearGradient>
<linearGradient id="frameFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="rgba(30,144,255,0.20)" />
<stop offset="1" stop-color="rgba(2,6,23,0.0)" />
</linearGradient>
<filter id="frameGlow" x="-30%" y="-60%" width="160%" height="220%">
<feGaussianBlur stdDeviation="3.2" result="blur" />
<feColorMatrix
in="blur"
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
result="colored"
/>
<feMerge>
<feMergeNode in="colored" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<path d="M10 5 L150 115 L1050 115 L1190 5 Z" fill="url(#frameFill)" />
<path
d="M10 5 L150 115 L1050 115 L1190 5"
stroke="url(#frameStroke)"
stroke-width="4"
fill="none"
filter="url(#frameGlow)"
/>
</svg>
<div class="title">产线运行看板</div>
</div>
</div>
<div class="header-right">
<div class="back-btn" @click="goBack">
<Icon icon="ep:back" />
<span>返回</span>
</div>
<div class="weather">
<Icon icon="fa-solid:cloud-sun" class="weather-icon" />
<div class="weather-meta">
<div class="temp">{{ weather.temp }}</div>
<div class="desc">{{ weather.desc }}</div>
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const timeStr = ref('')
const dateStr = ref('')
const weather = ref({ temp: '27°C', desc: 'Cloudy to clear' })
let timer: number | undefined
const goBack = () => {
router.back()
}
const updateTime = () => {
const d = new Date()
const h = String(d.getHours()).padStart(2, '0')
const mi = String(d.getMinutes()).padStart(2, '0')
const s = String(d.getSeconds()).padStart(2, '0')
timeStr.value = `${h}:${mi}:${s}`
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const weekMap = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
dateStr.value = `${weekMap[d.getDay()]}, ${y}-${m}-${day}`
}
onMounted(() => {
updateTime()
timer = window.setInterval(updateTime, 1000)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style scoped>
header {
height: var(--header-h);
position: relative;
z-index: 2;
display: flex;
align-items: center;
padding: 0 18px;
background:
linear-gradient(to bottom, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.85)),
radial-gradient(circle at 50% 0, rgba(56, 189, 248, 0.22), transparent 60%);
border-bottom: 1px solid rgba(148, 163, 184, 0.35);
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.9);
}
.header-inner {
width: 100%;
display: grid;
grid-template-columns: 320px 1fr 320px;
align-items: center;
gap: 12px;
}
.header-left {
justify-self: start;
display: flex;
flex-direction: column;
gap: 2px;
}
.time {
font-size: 22px;
font-weight: 800;
color: #e0f2fe;
letter-spacing: 1px;
}
.date {
font-size: 12px;
color: var(--muted);
}
.header-center {
justify-self: center;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.title-wrap {
position: relative;
width: min(980px, 100%);
height: 64px;
display: flex;
align-items: center;
justify-content: center;
}
.title-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.95;
}
.title {
position: relative;
z-index: 1;
font-size: 28px;
font-weight: 900;
letter-spacing: 3px;
background: linear-gradient(to bottom, #e0f2fe, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(56, 189, 248, 0.65);
}
.header-right {
justify-self: end;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 20px;
}
.back-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
border-radius: 4px;
background: rgba(30, 64, 175, 0.3);
border: 1px solid rgba(56, 189, 248, 0.3);
color: #e0f2fe;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.back-btn:hover {
background: rgba(30, 64, 175, 0.6);
border-color: rgba(56, 189, 248, 0.8);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.4);
}
.weather {
display: flex;
align-items: center;
gap: 12px;
color: var(--muted);
}
.weather-icon {
font-size: 28px;
color: var(--accent);
}
.weather-meta {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.temp {
font-size: 16px;
font-weight: 700;
color: #e0f2fe;
}
.desc {
font-size: 12px;
color: var(--muted);
}
@media (max-width: 1600px) {
.title {
font-size: 24px;
}
.title-wrap {
height: 58px;
}
.header-inner {
grid-template-columns: 280px 1fr 280px;
}
}
@media (max-width: 1366px) {
.title {
font-size: 20px;
letter-spacing: 3px;
}
.title-wrap {
height: 54px;
}
.time {
font-size: 18px;
}
}
</style>

@ -0,0 +1,157 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>设备概况</span>
</div>
<div class="panel-body overview-body">
<div v-for="item in overviewItems" :key="item.key" class="gauge-item">
<div class="gauge" :style="getGaugeStyle(item.percent, item.color)">
<div class="gauge-inner">
<div class="gauge-value">{{ item.value }}</div>
<div class="gauge-label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { colors } from '../utils'
const overviewItems = [
{ key: 'device', label: '设备数量', value: '987,965', percent: 78, color: colors.cyan },
{ key: 'running', label: '运行数量', value: '30', percent: 66, color: colors.blue },
{ key: 'idle', label: '待机数量', value: '2', percent: 42, color: colors.warn },
{ key: 'alarm', label: '报警数量', value: '10', percent: 58, color: colors.danger }
]
const getGaugeStyle = (percent: number, color: string) => {
const p = Math.max(0, Math.min(100, percent))
return {
background: `conic-gradient(${color} ${p * 3.6}deg, rgba(148,163,184,0.18) 0deg)`
}
}
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
box-shadow:
0 18px 45px rgba(15,23,42,0.95),
0 0 0 1px rgba(15,23,42,1),
inset 0 0 0 1px rgba(56,189,248,0.05);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
.card::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(56,189,248,0.75);
opacity: 0.6;
pointer-events: none;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-left: none;
border-top: none;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.overview-body {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.gauge-item {
display: flex;
align-items: center;
justify-content: center;
}
.gauge {
width: 110px;
height: 110px;
border-radius: 50%;
padding: 8px;
box-sizing: border-box;
}
.gauge-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(2,6,23,0.92);
border: 1px solid rgba(148,163,184,0.25);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
text-align: center;
}
.gauge-value {
font-size: 18px;
font-weight: 900;
color: #e5f0ff;
}
.gauge-label {
font-size: 11px;
color: rgba(148,163,184,0.95);
}
@media (max-width: 1366px) {
.gauge {
width: 96px;
height: 96px;
}
}
</style>

@ -0,0 +1,187 @@
<template>
<div class="card">
<div class="panel-title panel-title-between">
<div class="panel-title-left">
<span class="title-dot"></span>
<span>能耗监测</span>
</div>
<div class="tabs">
<span class="tab" :class="{ active: energyTab === 'first' }" @click="energyTab = 'first'">First aid</span>
<span class="tab" :class="{ active: energyTab === 'after' }" @click="energyTab = 'after'">Aftermarket</span>
</div>
</div>
<div class="panel-body">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
const energyTab = ref<'first' | 'after'>('first')
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const render = () => {
if (!chart) return
const x = ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
const y =
energyTab.value === 'first'
? [820, 1650, 980, 1240, 1560, 1320, 1680]
: [680, 1420, 880, 1100, 1320, 1180, 1480]
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { top: '18%', left: '6%', right: '4%', bottom: '12%', containLabel: true },
xAxis: { type: 'category', data: x, axisLine: style.axisLine, axisLabel: { ...style.axisLabel, fontSize: 10 } },
yAxis: { type: 'value', axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: style.splitLine },
series: [
{
type: 'bar',
barWidth: 12,
itemStyle: {
borderRadius: [6, 6, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colors.blue },
{ offset: 1, color: 'rgba(30,144,255,0.10)' }
])
},
data: y
}
]
})
}
const resize = () => {
chart?.resize()
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
})
watch(energyTab, () => {
render()
})
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
box-shadow:
0 18px 45px rgba(15,23,42,0.95),
0 0 0 1px rgba(15,23,42,1),
inset 0 0 0 1px rgba(56,189,248,0.05);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
.card::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(56,189,248,0.75);
opacity: 0.6;
pointer-events: none;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-left: none;
border-top: none;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
}
.panel-title-between {
justify-content: space-between;
}
.panel-title-left {
display: inline-flex;
align-items: center;
gap: 10px;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
}
.tabs {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 11px;
font-weight: 600;
color: rgba(148,163,184,0.95);
}
.tab {
cursor: pointer;
user-select: none;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.4);
background: rgba(2,6,23,0.2);
}
.tab.active {
border-color: rgba(56,189,248,0.85);
color: #e0f2fe;
box-shadow: 0 0 14px rgba(56,189,248,0.35);
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.chart {
width: 100%;
height: 100%;
min-height: 160px;
}
</style>

@ -0,0 +1,239 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>事件提醒</span>
</div>
<div class="panel-body body">
<div class="event-list">
<div v-for="item in eventItems" :key="item.key" class="event-row">
<div class="event-name">
<span class="event-bullet" :style="{ borderColor: item.color }"></span>
<span>{{ item.name }}</span>
</div>
<div class="event-count" :style="{ color: item.color }">{{ item.count }}</div>
</div>
</div>
<div class="event-chart">
<div class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as echarts from 'echarts'
import { colors } from '../utils'
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const eventItems = [
{ key: 'check', name: '点检', count: 1, percent: 30, color: colors.cyan },
{ key: 'maintain', name: '保养', count: 1, percent: 42, color: colors.warn },
{ key: 'repair', name: '维修', count: 1, percent: 20, color: colors.danger }
]
const render = () => {
if (!chart) return
const percentMap = Object.fromEntries(eventItems.map((i) => [i.name, i.percent])) as Record<string, number>
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item' },
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
itemGap: 14,
selectedMode: false,
textStyle: {
rich: {
percent: { fontSize: 18, fontWeight: 900, color: '#e5f0ff', lineHeight: 20 },
name: { fontSize: 12, fontWeight: 700, color: 'rgba(148,163,184,0.95)', lineHeight: 16 }
}
},
formatter: (name: string) => `{percent|${percentMap[name] ?? 0}%}\n{name|${name}}`
},
series: [
{
type: 'pie',
radius: ['48%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
label: { show: false },
labelLine: { show: false },
padAngle: 2,
itemStyle: { borderRadius: 8, borderWidth: 6, borderColor: 'rgba(2,6,23,0.9)' },
emphasis: { scale: false },
data: [
{ value: 30, name: '点检', itemStyle: { color: colors.cyan } },
{ value: 42, name: '保养', itemStyle: { color: colors.warn } },
{ value: 20, name: '维修', itemStyle: { color: colors.danger } }
]
}
]
})
}
const resize = () => {
chart?.resize()
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
})
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
box-shadow:
0 18px 45px rgba(15,23,42,0.95),
0 0 0 1px rgba(15,23,42,1),
inset 0 0 0 1px rgba(56,189,248,0.05);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
.card::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(56,189,248,0.75);
opacity: 0.6;
pointer-events: none;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-left: none;
border-top: none;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.body {
display: grid;
grid-template-columns: 0.7fr 1.3fr;
gap: 10px;
align-items: center;
}
.event-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.event-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 6px;
border: 1px solid rgba(30,64,175,0.7);
background: rgba(15,23,42,0.7);
}
.event-name {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #e5f0ff;
}
.event-bullet {
width: 12px;
height: 12px;
border-radius: 50%;
border: 3px solid;
box-sizing: border-box;
}
.event-count {
font-size: 14px;
font-weight: 900;
}
.event-chart {
height: 100%;
min-height: 170px;
display: flex;
min-height: 0;
}
.chart-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.chart {
width: 100%;
height: 100%;
}
@media (max-width: 1366px) {
.body {
grid-template-columns: 0.8fr 1.2fr;
}
}
</style>

@ -0,0 +1,170 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>Payment method</span>
<div class="date-filter">
<el-date-picker
v-model="pickedDate"
type="date"
format="YYYY MM/DD"
value-format="YYYY-MM-DD"
:clearable="false"
size="small"
/>
</div>
</div>
<div class="panel-body">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const pickedDate = ref('2023-08-31')
const render = () => {
if (!chart) return
const x = ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
const y = [60, 120, 165, 140, 185, 150, 190]
chart.setOption({
backgroundColor: 'transparent',
grid: { top: '14%', left: '6%', right: '4%', bottom: '12%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: x, axisLine: style.axisLine, axisLabel: { ...style.axisLabel, fontSize: 10 } },
yAxis: { type: 'value', axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: style.splitLine },
series: [
{
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 6,
lineStyle: { width: 2, color: colors.cyan },
itemStyle: { color: colors.cyan },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(34,211,238,0.38)' },
{ offset: 1, color: 'rgba(34,211,238,0.06)' }
])
},
data: y
}
]
})
}
const resize = () => {
chart?.resize()
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
})
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
box-shadow:
0 18px 45px rgba(15,23,42,0.95),
0 0 0 1px rgba(15,23,42,1),
inset 0 0 0 1px rgba(56,189,248,0.05);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
.card::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(56,189,248,0.75);
opacity: 0.6;
pointer-events: none;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-left: none;
border-top: none;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
}
.date-filter {
margin-left: auto;
display: inline-flex;
align-items: center;
}
.date-filter :deep(.el-input__wrapper) {
background: rgba(2,6,23,0.35);
box-shadow: none;
border: 1px solid rgba(148,163,184,0.35);
}
.date-filter :deep(.el-input__inner) {
color: rgba(224,242,254,0.95);
font-size: 12px;
font-weight: 700;
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.chart {
width: 100%;
height: 100%;
min-height: 160px;
}
</style>

@ -0,0 +1,165 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>产量趋势</span>
<span class="tag">今日</span>
</div>
<div class="panel-body">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const render = () => {
if (!chart) return
const x = ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
const output = [320, 460, 520, 610, 720, 690, 780]
const passRate = [98.5, 99.2, 98.1, 98.9, 99.0, 98.6, 99.3]
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { top: '18%', left: '6%', right: '6%', bottom: '12%', containLabel: true },
xAxis: { type: 'category', data: x, axisLine: style.axisLine, axisLabel: { ...style.axisLabel, fontSize: 10 } },
yAxis: [
{ type: 'value', axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: style.splitLine },
{ type: 'value', min: 96, max: 100, axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: { show: false } }
],
series: [
{
name: '产量',
type: 'bar',
barWidth: 12,
itemStyle: {
borderRadius: [6, 6, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colors.purple },
{ offset: 1, color: 'rgba(139,92,246,0.10)' }
])
},
data: output
},
{
name: '良率',
type: 'line',
yAxisIndex: 1,
smooth: true,
showSymbol: false,
lineStyle: { width: 2, color: colors.cyan },
data: passRate
}
]
})
}
const resize = () => {
chart?.resize()
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
})
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
box-shadow:
0 18px 45px rgba(15,23,42,0.95),
0 0 0 1px rgba(15,23,42,1),
inset 0 0 0 1px rgba(56,189,248,0.05);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
.card::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(56,189,248,0.75);
opacity: 0.6;
pointer-events: none;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-left: none;
border-top: none;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
}
.tag {
margin-left: auto;
border-radius: 999px;
padding: 2px 8px;
font-size: 11px;
font-weight: 700;
border: 1px solid rgba(148,163,184,0.4);
color: rgba(148,163,184,0.95);
background: rgba(2,6,23,0.2);
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.chart {
width: 100%;
height: 100%;
min-height: 160px;
}
</style>

@ -0,0 +1,152 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>任务列表</span>
</div>
<div class="panel-body table-body">
<table class="task-table">
<thead>
<tr>
<th>时间</th>
<th>类型</th>
<th>设备</th>
<th>责任人</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="row in taskRows" :key="row.id" :class="row.status">
<td>{{ row.time }}</td>
<td>{{ row.type }}</td>
<td>{{ row.device }}</td>
<td>{{ row.owner }}</td>
<td class="status-cell" :style="{ color: row.color }">{{ row.statusLabel }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { colors } from '../utils'
const taskRows = [
{ id: 1, time: '10:20', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan },
{ id: 2, time: '10:25', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan },
{ id: 3, time: '10:30', type: '维修', device: '1号装配机', owner: 'XXX', status: 'danger', statusLabel: '维修', color: colors.danger },
{ id: 4, time: '10:35', type: '保养', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '保养', color: colors.warn },
{ id: 5, time: '10:40', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan },
{ id: 6, time: '10:45', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan }
]
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
box-shadow:
0 18px 45px rgba(15,23,42,0.95),
0 0 0 1px rgba(15,23,42,1),
inset 0 0 0 1px rgba(56,189,248,0.05);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
.card::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid rgba(56,189,248,0.75);
opacity: 0.6;
pointer-events: none;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-left: none;
border-top: none;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.table-body {
padding: 0;
}
.task-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 12px;
}
.task-table thead {
background: radial-gradient(circle at 0 0, rgba(56,189,248,0.18), transparent 70%);
}
.task-table th {
padding: 10px 8px;
color: var(--accent);
font-weight: 700;
text-align: left;
border-bottom: 1px solid rgba(51,65,85,0.9);
}
.task-table td {
padding: 10px 8px;
border-bottom: 1px solid rgba(30,64,175,0.3);
color: #e5f0ff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-table tbody tr:hover td {
background: rgba(56,189,248,0.08);
}
.status-cell {
font-weight: 800;
}
</style>

@ -0,0 +1,62 @@
import { onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
export const colors = {
blue: '#1e90ff',
cyan: '#22d3ee',
green: '#22c55e',
purple: '#8b5cf6',
warn: '#f59e0b',
danger: '#ef4444'
}
export const style = {
axisLine: { lineStyle: { color: 'rgba(148,163,184,0.45)' } },
axisLabel: { color: '#a8b7d8', fontSize: 12 },
splitLine: { lineStyle: { color: 'rgba(30,64,175,0.55)', type: 'dashed' } },
legendText: { color: '#e5f0ff', fontSize: 12 }
}
export function animateCount(el: HTMLElement | null, target: number, suffix: string, duration: number) {
if (!el) return
const start = 0
const t0 = performance.now()
const step = (t: number) => {
const p = Math.min(1, (t - t0) / duration)
const v = start + (target - start) * p
const out = Number.isInteger(target) ? Math.round(v) : Math.round(v * 10) / 10
el.textContent = suffix ? `${out}${suffix}` : `${out}`
if (p < 1) requestAnimationFrame(step)
}
requestAnimationFrame(step)
}
export function useChart(domId: string) {
let chart: echarts.ECharts | null = null
const init = () => {
const el = document.getElementById(domId)
if (!el) return null
chart = echarts.init(el, 'dark', { renderer: 'canvas' })
return chart
}
const resize = () => {
chart?.resize()
}
onMounted(() => {
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
})
return {
init,
resize,
instance: () => chart
}
}
Loading…
Cancel
Save