Commit 62a1c35f authored by xinzhedeai's avatar xinzhedeai

add:canvas charts

parent c9fc65ef
......@@ -123,10 +123,6 @@ onMounted(() => {
// background-size: cover;
// margin-left: -0.1rem;
}
.card-footer {
height: 0.1rem;
background-image: url();
}
.status-grid {
display: grid;
......
<template>
<n-card :bordered="false" class="ai-warning-card">
<div class="card-header">
<h2 class="card-title">当月司机AI预警分析</h2>
</div>
<div class="chart-container" ref="chartRef"></div>
<div class="legend">
<div class="item"><span class="dot blue"></span> 疲劳闭眼: 10</div>
<div class="item"><span class="dot cyan"></span> 疲劳打哈欠: 5</div>
<div class="item"><span class="dot green"></span> 违规打电话: 15</div>
<div class="item"><span class="dot yellow"></span> 违规抽烟: 0</div>
<div class="item"><span class="dot orange"></span> 左顾右盼: 2</div>
<div class="item"><span class="dot red"></span> 人脸丢失: 0</div>
</div>
</n-card>
<!-- 图表容器 Canvas -->
<div class="chart-container wrapper">
<h2 class="card-title">设备状态总览</h2>
<canvas
ref="chartCanvasRef"
width="430"
height="290"
class="chart-canvas"
></canvas>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
chartInstance.setOption({
series: [{
name: '预警类型', type: 'pie', radius: ['30%', '70%'],
itemStyle: { borderRadius: 4, borderColor: 'var(--n-bg-color-secondary)', borderWidth: 2 },
label: { show: false },
data: [
{ value: 10, name: '疲劳闭眼', itemStyle: { color: '#36a2eb' } },
{ value: 5, name: '疲劳打哈欠', itemStyle: { color: '#4bc0c0' } },
{ value: 15, name: '违规打电话', itemStyle: { color: '#6dd230' } },
{ value: 0, name: '违规抽烟', itemStyle: { color: '#facc15' } },
{ value: 2, name: '左顾右盼', itemStyle: { color: '#ff9f40' } },
{ value: 0, name: '人脸丢失', itemStyle: { color: '#ff6384' } }
]
}]
});
};
import { ref, onMounted, watch } from 'vue';
// 1. 定义 TS 类型接口
interface ChartDataItem {
name: string;
value: number;
color: string;
}
interface RingLayer {
outer: number;
inner: number;
}
// 2. 核心配置与数据(可通过 props 透传扩展)
const chartData: ChartDataItem[] = [
{ name: '疲劳驾驶', value: 10, color: '#409EFF' },
{ name: '违规打电话', value: 15, color: '#52C41A' },
{ name: '违规抽烟', value: 0, color: '#FAAD14' },
{ name: '左顾右盼', value: 2, color: '#FA8C16' },
{ name: '人脸丢失', value: 0, color: '#F5222D' },
];
// 调整环形图层尺寸以适应新的 canvas 尺寸
const ringLayers: RingLayer[] = [
{ outer: 110, inner: 100 }, // 第1层:疲劳闭眼
{ outer: 90, inner: 80 }, // 第3层:违规打电话
{ outer: 80, inner: 70 }, // 第4层:违规抽烟
{ outer: 70, inner: 60 }, // 第5层:左顾右盼
{ outer: 60, inner: 50 }, // 第6层:人脸丢失
];
// 3. 获取 Canvas 元素 Ref
const chartCanvasRef = ref<HTMLCanvasElement | null>(null);
// 4. 封装绘制核心函数
const drawChart = () => {
const canvas = chartCanvasRef.value;
if (!canvas) {
console.error('Canvas element not found');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Failed to get 2D context');
return;
}
// 清空画布(防止重复绘制)
ctx.clearRect(0, 0, canvas.width, canvas.height);
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const bgColor = '#E5E6EB'; // 每层独立背景色
const startAngle = -Math.PI / 2; // 12点钟方向起始
// 步骤1:绘制每层独立的深灰色背景
const drawBackground = () => {
ringLayers.forEach((layer) => {
ctx.beginPath();
ctx.arc(centerX, centerY, layer.outer, 0, Math.PI * 2);
ctx.arc(centerX, centerY, layer.inner, Math.PI * 2, 0, true);
ctx.closePath();
ctx.fillStyle = bgColor;
ctx.fill();
// 层间白色描边区分
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.stroke();
});
};
// 步骤2:按比例绘制数值扇区
const drawSectors = () => {
const validData = chartData.filter((item) => item.value > 0);
const total = validData.reduce((sum, item) => sum + item.value, 0);
if (total === 0) {
console.warn('No valid data to draw');
// 即使没有有效数据也绘制背景和图例
drawBackground();
drawLegend();
return;
}
// 先绘制背景
drawBackground();
chartData.forEach((item, index) => {
if (item.value <= 0) return;
const layer = ringLayers[index];
if (!layer) return;
const resizeChart = () => chartInstance?.resize();
// 计算扇区角度比例
const angleRatio = item.value / total;
const endAngle = startAngle + Math.PI * 2 * angleRatio;
// 绘制扇区
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, layer.outer, startAngle, endAngle);
ctx.lineTo(
centerX + layer.inner * Math.cos(endAngle),
centerY + layer.inner * Math.sin(endAngle)
);
ctx.arc(centerX, centerY, layer.inner, endAngle, startAngle, true);
ctx.closePath();
ctx.fillStyle = item.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.stroke();
});
};
// 步骤3:绘制右侧图例
const drawLegend = () => {
const legendX = centerX + 120; // 调整图例位置以适应新尺寸
const legendYStart = 60;
const itemGap = 25;
chartData.forEach((item, index) => {
const legendY = legendYStart + index * itemGap;
// 绘制颜色方块
ctx.fillStyle = item.color;
ctx.fillRect(legendX, legendY, 15, 15);
ctx.strokeStyle = '#e0e0e0';
ctx.strokeRect(legendX, legendY, 15, 15);
// 绘制类别名称
ctx.fillStyle = '#333';
ctx.font = '12px Microsoft YaHei';
ctx.fillText(item.name, legendX + 25, legendY + 12);
// 绘制数值
ctx.fillStyle = '#666';
ctx.fillText(item.value.toString(), legendX + 100, legendY + 12);
});
};
// 执行绘制流程
drawSectors();
drawLegend();
};
// 5. 挂载后初始化图表(DOM 渲染完成后执行)
onMounted(() => {
nextTick(initChart);
window.addEventListener('resize', resizeChart);
console.log('Component mounted, attempting to draw chart');
// 使用 nextTick 确保 DOM 已完全渲染
setTimeout(() => {
drawChart();
}, 0);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeChart);
chartInstance?.dispose();
// 可选:监听数据变化重新绘制(如需动态更新数据可启用)
watch([() => chartData], () => {
drawChart();
}, { deep: true });
// 导出绘制函数以便外部调用
defineExpose({
drawChart
});
</script>
<style scoped lang="scss">
.ai-warning-card {
padding: 0.15rem;
background: var(--n-bg-color-secondary);
.card-header {
margin-bottom: 0.15rem;
.card-title {
font-size: 0.18rem;
font-weight: 600;
color: var(--n-text-color-primary);
}
}
.chart-container {
width: 100%;
overflow: auto;
background-image: url("@/assets/jinrun/module-bg.png");
background-repeat: no-repeat;
}
.chart-container {
width: 100%;
height: 1.2rem;
}
.chart-canvas {
// background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
// margin: 20px 0;
display: block;
}
.legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.05rem;
margin-top: 0.1rem;
font-size: 0.12rem;
color: var(--n-text-color-secondary);
.item { display: flex; align-items: center; gap: 0.05rem; }
.dot {
width: 0.1rem; height: 0.1rem; border-radius: 50%; display: inline-block;
&.blue { background: #36a2eb; }
&.cyan { background: #4bc0c0; }
&.green { background: #6dd230; }
&.yellow { background: #facc15; }
&.orange { background: #ff9f40; }
&.red { background: #ff6384; }
}
.wrapper{
overflow: visible;
width: 4.6rem;
height: 3rem;
position: relative;
padding: 0.15rem;
padding-top: 0.45rem;
.card-title {
position: absolute;
left: 0.25rem;
top: -0.15rem;
font-weight: 500;
font-size: 0.2rem;
color: #ffffff;
text-shadow: 0rem 0rem 0rem rgba(5, 38, 68, 0.5);
}
}
</style>
\ No newline at end of file
......@@ -237,7 +237,8 @@ const navTo = () => {
<!-- 右侧模块容器 -->
<transition name="slide-right">
<div class="right-modules" v-show="isRightModulesVisible">
<!-- <RightAiWarning />
<RightAiWarning />
<!--
<RightEnvMonitor />
<RightTodayAlarm /> -->
</div>
......@@ -478,14 +479,23 @@ const navTo = () => {
.right-modules:not(.v-enter-active):not(.v-leave-active) ~ .right-toggle:not(.collapsed) {
right: 0;
border-radius: 10px 0 0 10px;
background: pink;
}
.left-modules, .right-modules {
width: 4.6rem;
// background: pink;
// margin-top: .6rem;
padding-top: 0.6rem;
padding-top: 1rem;
margin-left: 0.4rem;
position: absolute;
}
.left-modules{
left: 0;
}
.right-modules{
right: 0;
}
.arrow-font{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment