This commit is contained in:
tcy
2025-09-22 15:42:15 +08:00
34 changed files with 3226 additions and 333 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

BIN
src/assets/demo/chi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src/assets/demo/nowifi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
src/assets/demo/people.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

BIN
src/assets/demo/qin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

BIN
src/assets/demo/que.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

BIN
src/assets/demo/rebot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

BIN
src/assets/demo/time.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

BIN
src/assets/demo/tui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

BIN
src/assets/demo/wifi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

View File

@ -5,11 +5,26 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
// 定义props类型
interface TrendSeriesItem {
name: string;
data: number[];
color: string;
}
interface TrendData {
dates: string[];
series: TrendSeriesItem[];
}
// 定义props
const props = defineProps<{
trendData: TrendData;
}>();
// 图表DOM引用 // 图表DOM引用
const chartRef = ref(null); const chartRef = ref(null);
@ -24,7 +39,7 @@ const initChart = () => {
const option = { const option = {
xAxis: { xAxis: {
type: "category", type: "category",
data: ["09-04", "09-05", "09-06", "09-07", "09-08", "09-09", "09-10"], data: props.trendData.dates,
axisTick: { axisTick: {
show: false // 去除刻度线 show: false // 去除刻度线
} }
@ -52,48 +67,15 @@ const initChart = () => {
bottom: '3%', bottom: '3%',
containLabel: true containLabel: true
}, },
series: [ series: props.trendData.series.map((item, index) => ({
{ name: item.name,
name: "维护提醒", data: item.data,
data: [120, 200, 150, 80, 70, 110, 130], type: "bar",
type: "bar", barWidth: '10%' ,
itemStyle: { itemStyle: {
color: "rgb(0, 179, 255)", color: item.color,
},
}, },
{ })),
name: "数据异常",
data: [80, 170, 100, 50, 90, 140, 170],
type: "bar",
itemStyle: {
color: "rgb(22, 93, 255)",
},
},
{
name: "信号减弱",
data: [60, 140, 100, 120, 110, 100, 130],
type: "bar",
itemStyle: {
color: "rgb(255, 153, 0)",
},
},
{
name: "温度过高",
data: [60, 140, 100, 120, 110, 100, 130],
type: "bar",
itemStyle: {
color: "rgb(250, 220, 25)",
},
},
{
name: "通讯中断",
data: [60, 140, 100, 120, 110, 100, 130],
type: "bar",
itemStyle: {
color: "rgb(251, 62, 122)",
}
}
],
}; };
chartInstance.setOption(option); chartInstance.setOption(option);
@ -106,6 +88,13 @@ const handleResize = () => {
} }
}; };
// 监听props变化
watch(() => props.trendData, () => {
if (chartInstance) {
initChart();
}
}, { deep: true });
// 生命周期钩子 // 生命周期钩子
onMounted(() => { onMounted(() => {
initChart(); initChart();
@ -120,8 +109,6 @@ onMounted(() => {
} }
}; };
}); });
</script> </script>
<style scoped> <style scoped>

View File

@ -5,10 +5,29 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
// 定义props类型
interface PieItem {
value: number;
name: string;
displayName: string;
color: string;
}
interface PieData {
normal: PieItem;
interrupt: PieItem;
abnormal: PieItem;
serious: PieItem;
}
// 定义props
const props = defineProps<{
pieData: PieData;
}>();
// 图表DOM引用 // 图表DOM引用
const chartRef = ref(null); const chartRef = ref(null);
@ -22,7 +41,10 @@ const initChart = () => {
} }
const option = { const option = {
tooltip: { tooltip: {
trigger: 'item' trigger: 'item',
formatter: (params: any) => {
return `${params.data.displayName}: ${params.value}`;
}
}, },
grid: { grid: {
left: '0%', left: '0%',
@ -37,6 +59,10 @@ const initChart = () => {
right: '5%', // 调整图例位置,使其更靠近左侧 right: '5%', // 调整图例位置,使其更靠近左侧
itemWidth: 15, itemWidth: 15,
itemHeight: 15, itemHeight: 15,
formatter: (name: string) => {
const item = Object.values(props.pieData).find(item => item.name === name);
return item?.displayName || name;
}
}, },
series: [ series: [
{ {
@ -46,16 +72,32 @@ const initChart = () => {
show: false show: false
}, },
color: [ color: [
'rgb(0, 179, 255)', // 提示信息 props.pieData.normal.color,
'rgb(45, 214, 131)', // 一般告警 props.pieData.interrupt.color,
'rgb(255, 208, 35)', // 重要告警 props.pieData.abnormal.color,
'rgb(227, 39, 39)' // 严重告警 props.pieData.serious.color
], ],
data: [ data: [
{ value: 1048, name: '提示信息' }, {
{ value: 735, name: '一般告警' }, value: props.pieData.normal.value,
{ value: 580, name: '重要告警' }, name: props.pieData.normal.name,
{ value: 484, name: '严重告警' }, displayName: props.pieData.normal.displayName
},
{
value: props.pieData.interrupt.value,
name: props.pieData.interrupt.name,
displayName: props.pieData.interrupt.displayName
},
{
value: props.pieData.abnormal.value,
name: props.pieData.abnormal.name,
displayName: props.pieData.abnormal.displayName
},
{
value: props.pieData.serious.value,
name: props.pieData.serious.name,
displayName: props.pieData.serious.displayName
}
], ],
emphasis: { emphasis: {
itemStyle: { itemStyle: {
@ -78,6 +120,13 @@ const handleResize = () => {
} }
}; };
// 监听props变化
watch(() => props.pieData, () => {
if (chartInstance) {
initChart();
}
}, { deep: true });
// 生命周期钩子 // 生命周期钩子
onMounted(() => { onMounted(() => {
initChart(); initChart();
@ -92,8 +141,6 @@ onMounted(() => {
} }
}; };
}); });
</script> </script>
<style scoped> <style scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-table :data="alarmLevels" :border="false" style="width: 100%"> <el-table :data="localAlarmLevels" :border="false" style="width: 100%">
<el-table-column prop="levelName" label="级别名称" align="center"> <el-table-column prop="levelName" label="级别名称" align="center">
<template #default="scope"> <template #default="scope">
<span :class="['level-name', `level-${scope.row.level}`]">{{ scope.row.levelName }}</span> <span :class="['level-name', `level-${scope.row.level}`]">{{ scope.row.levelName }}</span>
@ -78,7 +78,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue'; import { ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
// 定义告警等级类型 // 定义告警等级类型
@ -102,49 +102,19 @@ interface ConfigData extends AlarmLevel {
processDescription: string; processDescription: string;
} }
// 模拟数据 // 定义props
const alarmLevels = ref<AlarmLevel[]>([ const props = defineProps<{
{ alarmLevels: AlarmLevel[];
id: 1, }>();
levelName: '严重告警',
description: '系统或应用出现严重故障', // 本地数据副本
priority: '一级', const localAlarmLevels = ref<AlarmLevel[]>([]);
responseTime: '15分钟以内',
processingMethod: ['系统锁定', '声光报警', '短信通知'], // 初始化本地数据
enabled: true, watch(() => props.alarmLevels, (newVal) => {
level: 1 // 深拷贝以避免直接修改props
}, localAlarmLevels.value = JSON.parse(JSON.stringify(newVal));
{ }, { immediate: true, deep: true });
id: 2,
levelName: '重要告警',
description: '系统或应用出现严重故障',
priority: '二级',
responseTime: '30分钟以内',
processingMethod: ['声光报警', '短信通知', '系统记录'],
enabled: true,
level: 2
},
{
id: 3,
levelName: '一般告警',
description: '非关键性故障或潜在风险',
priority: '三级',
responseTime: '120分钟以内',
processingMethod: ['短信通知', '系统记录'],
enabled: true,
level: 3
},
{
id: 4,
levelName: '提示信息',
description: '系统或应用非关键性变化或即将达到阈值的状态',
priority: '四级',
responseTime: '24小时以内',
processingMethod: ['短信通知'],
enabled: false,
level: 4
}
]);
// 对话框相关状态 // 对话框相关状态
const configDialogVisible = ref(false); const configDialogVisible = ref(false);
@ -198,10 +168,10 @@ const handleConfig = (row: AlarmLevel) => {
const handleConfigSave = () => { const handleConfigSave = () => {
if (currentConfigData.value) { if (currentConfigData.value) {
// 找到对应的告警等级并更新 // 找到对应的告警等级并更新
const index = alarmLevels.value.findIndex(item => item.id === currentConfigData.value!.id); const index = localAlarmLevels.value.findIndex(item => item.id === currentConfigData.value!.id);
if (index !== -1) { if (index !== -1) {
alarmLevels.value[index] = { localAlarmLevels.value[index] = {
...alarmLevels.value[index], ...localAlarmLevels.value[index],
enabled: currentConfigData.value!.enabled enabled: currentConfigData.value!.enabled
}; };
} }
@ -217,9 +187,9 @@ const handleDelete = (id: number) => {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const index = alarmLevels.value.findIndex(item => item.id === id); const index = localAlarmLevels.value.findIndex(item => item.id === id);
if (index !== -1) { if (index !== -1) {
alarmLevels.value.splice(index, 1); localAlarmLevels.value.splice(index, 1);
ElMessage.success('删除成功'); ElMessage.success('删除成功');
} }
}).catch(() => { }).catch(() => {

View File

@ -8,7 +8,7 @@
<div class="total-header"> <div class="total-header">
<span class="total-title">今日报警总数</span> <span class="total-title">今日报警总数</span>
</div> </div>
<div class="total-number">28</div> <div class="total-number">{{ totalData.totalAlarm }}</div>
</div> </div>
<div class="icon-section"> <div class="icon-section">
<el-icon class="total-icon blue"> <el-icon class="total-icon blue">
@ -20,7 +20,7 @@
<el-icon class="trend-icon green"> <el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升"> <img src="/src/assets/demo/up.png" alt="上升">
</el-icon> </el-icon>
<span class="comparison-text green">8</span> <span class="comparison-text green">+{{ totalData.totalIncrease }}</span>
<span class="period-text">较上月同期</span> <span class="period-text">较上月同期</span>
</div> </div>
</div> </div>
@ -34,7 +34,7 @@
<div class="total-header"> <div class="total-header">
<span class="total-title">未处理报警</span> <span class="total-title">未处理报警</span>
</div> </div>
<div class="total-number">8</div> <div class="total-number">{{ totalData.unprocessedAlarm }}</div>
</div> </div>
<div class="icon-section"> <div class="icon-section">
<el-icon class="total-icon purple"> <el-icon class="total-icon purple">
@ -46,7 +46,7 @@
<el-icon class="trend-icon green"> <el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升"> <img src="/src/assets/demo/up.png" alt="上升">
</el-icon> </el-icon>
<span class="comparison-text green">8</span> <span class="comparison-text green">+{{ totalData.unprocessedIncrease }}</span>
<span class="period-text">较上月同期</span> <span class="period-text">较上月同期</span>
</div> </div>
</div> </div>
@ -60,7 +60,7 @@
<div class="total-header"> <div class="total-header">
<span class="total-title">已处理报警</span> <span class="total-title">已处理报警</span>
</div> </div>
<div class="total-number">20</div> <div class="total-number">{{ totalData.processedAlarm }}</div>
</div> </div>
<div class="icon-section"> <div class="icon-section">
<el-icon class="total-icon green"> <el-icon class="total-icon green">
@ -72,7 +72,7 @@
<el-icon class="trend-icon green"> <el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升"> <img src="/src/assets/demo/up.png" alt="上升">
</el-icon> </el-icon>
<span class="comparison-text green">8</span> <span class="comparison-text green">+{{ totalData.processedIncrease }}</span>
<span class="period-text">较上月同期</span> <span class="period-text">较上月同期</span>
</div> </div>
</div> </div>
@ -86,7 +86,7 @@
<div class="total-header"> <div class="total-header">
<span class="total-title">严重报警</span> <span class="total-title">严重报警</span>
</div> </div>
<div class="total-number">3</div> <div class="total-number">{{ totalData.seriousAlarm }}</div>
</div> </div>
<div class="icon-section"> <div class="icon-section">
<el-icon class="total-icon orange"> <el-icon class="total-icon orange">
@ -98,7 +98,7 @@
<el-icon class="trend-icon green"> <el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升"> <img src="/src/assets/demo/up.png" alt="上升">
</el-icon> </el-icon>
<span class="comparison-text green">8</span> <span class="comparison-text green">+{{ totalData.seriousIncrease }}</span>
<span class="period-text">较上月同期</span> <span class="period-text">较上月同期</span>
</div> </div>
</div> </div>
@ -107,6 +107,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from 'vue';
// 定义props类型
interface TotalData {
totalAlarm: number;
unprocessedAlarm: number;
processedAlarm: number;
seriousAlarm: number;
totalIncrease: number;
unprocessedIncrease: number;
processedIncrease: number;
seriousIncrease: number;
}
// 定义props
const props = defineProps<{
totalData: TotalData;
}>();
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -12,14 +12,14 @@
<el-col :span="16"> <el-col :span="16">
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警管理" :font-level="2" /> <TitleComponent title="报警管理" :font-level="2" />
<totalView /> <totalView :totalData="totalData" />
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<!-- 报警级别分布 --> <!-- 报警级别分布 -->
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警级别分布" :font-level="2" /> <TitleComponent title="报警级别分布" :font-level="2" />
<levelPie /> <levelPie :pieData="pieData" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -29,7 +29,7 @@
<el-col :span="24"> <el-col :span="24">
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警趋势分析" :font-level="2" /> <TitleComponent title="报警趋势分析" :font-level="2" />
<fenxiBar /> <fenxiBar :trendData="trendData" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -39,7 +39,7 @@
<el-col :span="24"> <el-col :span="24">
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警级别设置" :font-level="2" /> <TitleComponent title="报警级别设置" :font-level="2" />
<levelSet /> <levelSet :alarmLevels="alarmLevelsData" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -47,11 +47,128 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue';
import TitleComponent from '@/components/TitleComponent/index.vue'; import TitleComponent from '@/components/TitleComponent/index.vue';
import levelPie from '@/views/integratedManage/alarmManage/components/levelPie.vue' import levelPie from '@/views/integratedManage/alarmManage/components/levelPie.vue'
import fenxiBar from '@/views/integratedManage/alarmManage/components/fenxiBar.vue' import fenxiBar from '@/views/integratedManage/alarmManage/components/fenxiBar.vue'
import totalView from '@/views/integratedManage/alarmManage/components/totalView.vue'; import totalView from '@/views/integratedManage/alarmManage/components/totalView.vue';
import levelSet from '@/views/integratedManage/alarmManage/components/levelSet.vue'; import levelSet from '@/views/integratedManage/alarmManage/components/levelSet.vue';
// 模拟报警总数数据
const totalData = ref({
totalAlarm: 28,
unprocessedAlarm: 8,
processedAlarm: 20,
seriousAlarm: 3,
totalIncrease: 8,
unprocessedIncrease: 3,
processedIncrease: 5,
seriousIncrease: 1
});
// 模拟报警级别分布数据
const pieData = ref({
normal: {
value: 1048,
name: '提示信息',
displayName: '提示信息',
color: 'rgb(0, 179, 255)'
},
interrupt: {
value: 735,
name: '一般告警',
displayName: '一般告警',
color: 'rgb(45, 214, 131)'
},
abnormal: {
value: 580,
name: '重要告警',
displayName: '重要告警',
color: 'rgb(255, 208, 35)'
},
serious: {
value: 484,
name: '严重告警',
displayName: '严重告警',
color: 'rgb(227, 39, 39)'
}
});
// 模拟报警趋势数据
const trendData = ref({
dates: ['09-04', '09-05', '09-06', '09-07', '09-08', '09-09', '09-10'],
series: [
{
name: '维护提醒',
data: [120, 200, 150, 80, 70, 110, 130],
color: 'rgb(0, 179, 255)'
},
{
name: '数据异常',
data: [80, 170, 100, 50, 90, 140, 170],
color: 'rgb(22, 93, 255)'
},
{
name: '信号减弱',
data: [60, 140, 100, 120, 110, 100, 130],
color: 'rgb(255, 153, 0)'
},
{
name: '温度过高',
data: [60, 140, 100, 120, 110, 100, 130],
color: 'rgb(250, 220, 25)'
},
{
name: '通讯中断',
data: [60, 140, 100, 120, 110, 100, 130],
color: 'rgb(251, 62, 122)'
}
]
});
// 模拟告警级别设置数据
const alarmLevelsData = ref([
{
id: 1,
levelName: '严重告警',
description: '系统或应用出现严重故障',
priority: '一级',
responseTime: '15分钟以内',
processingMethod: ['系统锁定', '声光报警', '短信通知'],
enabled: true,
level: 1
},
{
id: 2,
levelName: '重要告警',
description: '系统或应用出现严重故障',
priority: '二级',
responseTime: '30分钟以内',
processingMethod: ['声光报警', '短信通知', '系统记录'],
enabled: true,
level: 2
},
{
id: 3,
levelName: '一般告警',
description: '非关键性故障或潜在风险',
priority: '三级',
responseTime: '120分钟以内',
processingMethod: ['短信通知', '系统记录'],
enabled: true,
level: 3
},
{
id: 4,
levelName: '提示信息',
description: '系统或应用非关键性变化或即将达到阈值的状态',
priority: '四级',
responseTime: '24小时以内',
processingMethod: ['短信通知'],
enabled: false,
level: 4
}
]);
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,327 @@
<template>
<div class="chart-container">
<!-- 图表标题和时间范围选择器 -->
<div class="chart-header">
<h2>出勤趋势分析</h2>
<div class="chart-actions">
<button @click="timeRange = 'week'" :class="{ active: timeRange === 'week' }">每周</button>
<button @click="timeRange = 'month'" :class="{ active: timeRange === 'month' }">每月</button>
</div>
</div>
<!-- 图表内容区域 -->
<div ref="chartRef" class="chart-content"></div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import * as echarts from 'echarts';
// 接收从父组件传入的数据
const props = defineProps({
attendData: {
type: Object,
default: () => ({
week: {
xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
actualCount: [40, 20, 30, 15, 22, 63, 58],
expectedCount: [100, 556, 413, 115, 510, 115, 317]
},
month: {
xAxis: ['第1周', '第2周', '第3周', '第4周'],
actualData: [280, 360, 320, 400],
theoreticalData: [300, 400, 350, 450]
}
})
}
});
// 图表DOM引用
const chartRef = ref(null);
// 图表实例
let chartInstance = null;
// 时间范围状态
const timeRange = ref('week');
// 根据时间范围计算当前显示的数据
const chartData = computed(() => {
const dataForRange = props.attendData[timeRange.value] || props.attendData.week;
// 处理字段名称差异
if (timeRange.value === 'week') {
return {
xAxis: dataForRange.xAxis || [],
actualCount: dataForRange.actualCount || [],
expectedCount: dataForRange.expectedCount || []
};
} else {
return {
xAxis: dataForRange.xAxis || [],
actualCount: dataForRange.actualData || [],
expectedCount: dataForRange.theoreticalData || []
};
}
});
// 定义颜色常量
const ACTUAL_COUNT_COLOR = '#029CD4'; // 蓝色 - 实际人数
const EXPECTED_COUNT_COLOR = '#0052D9'; // 蓝色 - 应出勤人数
// 初始化图表
const initChart = () => {
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
// 使用计算后的数据
const { xAxis, actualCount, expectedCount } = chartData.value;
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,1)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 14
},
formatter: function(params) {
const actualCount = params[0].value;
const expectedCount = params[1].value;
return `
<div style="padding: 5px;">
<div style="color: ${params[0].color};">实际人数: ${actualCount}</div>
<div style="color: ${params[1].color};">应出勤人数: ${expectedCount}</div>
</div>
`;
}
},
legend: {
top: 30,
left: 'center',
itemWidth: 10,
itemHeight: 10,
itemGap: 25,
data: ['实际人数', '应出勤人数'],
textStyle: {
color: '#666',
fontSize: 12
}
},
grid: {
top: '30%',
right: '10%',
bottom: '10%',
left: '6%',
containLabel: true
},
xAxis: {
data: xAxis,
type: 'category',
boundaryGap: true,
axisLabel: {
textStyle: {
color: '#666',
fontSize: 12
}
},
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#ddd'
}
}
},
yAxis: [
{
type: 'value',
name: '人数',
nameTextStyle: {
color: '#666',
fontSize: 12
},
interval: 100,
axisLabel: {
textStyle: {
color: '#666',
fontSize: 12
}
},
axisTick: {
show: false
},
axisLine: {
show: false
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
}
],
series: [
{
name: '实际人数',
type: 'bar',
barWidth: '40%',
itemStyle: {
color: ACTUAL_COUNT_COLOR
},
data: actualCount
},
{
name: '应出勤人数',
type: 'line',
showSymbol: false,
symbol: 'circle',
symbolSize: 6,
emphasis: {
showSymbol: true,
symbolSize: 10
},
lineStyle: {
width: 2,
color: EXPECTED_COUNT_COLOR
},
itemStyle: {
color: EXPECTED_COUNT_COLOR,
borderColor: '#fff',
borderWidth: 2
},
data: expectedCount
}
]
};
chartInstance.setOption(option);
};
// 响应窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 监听时间范围变化,更新图表
watch(timeRange, () => {
initChart();
});
// 监听数据变化,更新图表
watch(() => props.attendData, () => {
initChart();
}, { deep: true });
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
});
</script>
<style scoped>
.chart-container {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 500px;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h2 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-actions button {
background: none;
border: 1px solid #e0e0e0;
padding: 5px 12px;
border-radius: 4px;
margin-left: 8px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.chart-actions button.active {
background-color: #1890ff;
color: white;
border-color: #1890ff;
}
.chart-content {
width: 100%;
height: calc(100% - 54px);
padding: 10px;
}
@media (max-width: 768px) {
.chart-container {
height: 450px;
}
}
@media (max-width: 480px) {
.chart-container {
height: 400px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.chart-actions {
width: 100%;
display: flex;
justify-content: space-between;
}
.chart-actions button {
margin: 0;
flex: 1;
margin-right: 5px;
}
.chart-actions button:last-child {
margin-right: 0;
}
}
.model {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<div class="box">
<div class="total">
<div class="infoBox">
<div class="date text-color">2025-08-26</div>
<div class="temperature text-color">28</div>
<div class="role text-color">中午好管理员</div>
<div class="cycle text-color">加入项目已经89天</div>
</div>
<img src="@/assets/demo/icTicket.png" alt="" class="imgbox">
</div>
</div>
</template>
<style scoped lang="scss">
.total {
width: 100%;
position: relative;
overflow: hidden;
.imgbox {
position: absolute;
top: 60px;
left: 210px;
}
.infoBox {
height: 217px;
border-radius: 12px;
padding: 30px;
background: rgba(24, 109, 245, 1);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
.text-color {
color: rgba(255, 255, 255, 1);
}
.date {
font-size: 16px;
}
.temperature {
font-weight: 600;
font-size: 28px;
}
.role {
font-size: 24px;
}
.cycle {
font-size: 16px;
}
}
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<div class="box">
<div class="chart-header">
<TitleComponent title="审批" :font-level="2" />
<span>更多</span>
</div>
<div class="approval-content">
<div
v-for="(item, index) in approvalData"
:key="index"
class="approval-item"
>
<div class="approval-left">
<div class="approval-icon">
<img :src="item.iconPath" :alt="item.type">
</div>
<div class="approval-info">
<div class="info">
<div class="type">{{ item.type }}</div>
<div class="day">{{ item.days }}</div>
</div>
<div class="info1">
<div class="time">
<img src="@/assets/demo/time.png" alt="时间">
<span>{{ item.timeRange }}</span>
</div>
<div class="people">
<img src="@/assets/demo/people.png" alt="人员">
<span>{{ item.people }}</span>
</div>
</div>
</div>
</div>
<div class="approval-tag">
<el-tag :type="item.statusType">{{ item.status }}</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
// 接收从父组件传入的数据
const props = defineProps({
approvalData: {
type: Array,
default: () => []
}
});
</script>
<style scoped lang="scss">
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
.chart-header span {
color: #186DF5;
font-size: 14px;
cursor: pointer;
}
.approval-content {
background-color: white;
.approval-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #F2F8FC;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #F2F3F5;
transition: all 0.3s ease;
}
.approval-item:hover {
border-color: #E4E6EB;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.approval-left {
display: flex;
align-items: center;
flex: 1;
}
.approval-icon {
width: 48px;
height: 48px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
position: relative;
overflow: hidden;
background: #186DF5;
}
.approval-icon img {
width: 26px;
height: 26px;
}
.approval-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.info {
display: flex;
justify-content: space-between;
align-items: center;
width: 90px;
}
.info .type {
font-size: 14px;
font-weight: 500;
color: #333;
}
.info .day {
font-size: 14px;
color: #666;
}
.info1 {
display: flex;
align-items: center;
gap: 16px;
}
.info1 .time,
.info1 .people {
display: flex;
align-items: center;
font-size: 12px;
color: rgba(113, 128, 150, 1);
}
.info1 img {
width: 12px;
height: 12px;
margin-right: 4px;
}
.approval-tag {
margin-left: 16px;
}
.approval-tag .el-tag {
padding: 4px 12px;
font-size: 12px;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<div class="box">
<div class="chart-header">
<TitleComponent title="日历" :font-level="2" />
</div>
<div class="calendar-container">
<div class="calendar-header">
<el-button size="small" type="text" @click="prevMonth">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span class="current-month">{{ currentYear }} {{ currentMonthName }}</span>
<el-button size="small" type="text" @click="nextMonth">
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="calendar-weekdays">
<span v-for="day in weekdays" :key="day" class="weekday">{{ day }}</span>
</div>
<div class="calendar-days">
<!-- 上月剩余天数 -->
<div v-for="(day, index) in prevMonthDays" :key="'prev-' + index" class="day prev-month-day">
{{ day }}
</div>
<!-- 当月天数 -->
<div
v-for="day in currentMonthDays"
:key="day"
class="day current-month-day"
:class="{
'current-day': isCurrentDay(day),
'selected-day': isSelectedDay(day)
}"
@click="selectDay(day)"
>
{{ day }}
<!-- 今天有红点标记 -->
<span v-if="isToday(day)" class="today-marker"></span>
<!-- 考勤状态标记 -->
<span v-if="getAttendanceStatus(day)" class="attendance-marker" :class="getAttendanceStatus(day)"></span>
</div>
<!-- 下月开始天数 -->
<div v-for="(day, index) in nextMonthDays" :key="'next-' + index" class="day next-month-day">
{{ day }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
// 接收从父组件传入的数据
const props = defineProps({
calendarData: {
type: Object,
default: () => ({
// 初始化当前日期
today: new Date(),
currentDate: new Date(2025, 8, 27), // 2025年9月27日截图中显示的日期
selectedDate: new Date(2025, 8, 27),
// 模拟考勤数据
attendanceData: {
2025: {
9: {
1: 'normal',
4: 'late',
8: 'absent',
10: 'leave',
15: 'normal',
20: 'normal',
25: 'late',
27: 'normal'
}
}
}
})
}
});
// 初始化当前日期
const today = ref(props.calendarData.today);
const currentDate = ref(props.calendarData.currentDate);
const selectedDate = ref(props.calendarData.selectedDate);
// 模拟考勤数据
const attendanceData = ref(props.calendarData.attendanceData);
// 星期几的显示
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// 计算属性
const currentYear = computed(() => currentDate.value.getFullYear());
const currentMonth = computed(() => currentDate.value.getMonth());
const currentMonthName = computed(() => monthNames[currentMonth.value]);
// 获取当月的天数
const currentMonthDays = computed(() => {
return new Date(currentYear.value, currentMonth.value + 1, 0).getDate();
});
// 获取当月第一天是星期几0-60是星期日
const firstDayOfMonth = computed(() => {
return new Date(currentYear.value, currentMonth.value, 1).getDay();
});
// 获取上月剩余天数
const prevMonthDays = computed(() => {
const days = [];
const prevMonth = new Date(currentYear.value, currentMonth.value, 0).getDate(); // 上月最后一天
for (let i = firstDayOfMonth.value - 1; i >= 0; i--) {
days.push(prevMonth - i);
}
return days;
});
// 获取下月开始天数
const nextMonthDays = computed(() => {
const days = [];
const totalDays = prevMonthDays.value.length + currentMonthDays.value;
const nextDays = 35 - totalDays; // 显示5周共35天
for (let i = 1; i <= nextDays; i++) {
days.push(i);
}
return days;
});
// 方法
const prevMonth = () => {
currentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1);
};
const nextMonth = () => {
currentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1);
};
const selectDay = (day) => {
selectedDate.value = new Date(currentYear.value, currentMonth.value, day);
// 显示选择的日期和考勤状态
let message = `Selected: ${currentMonthName.value} ${day}, ${currentYear.value}`;
const status = getAttendanceStatus(day);
if (status) {
const statusMap = {
normal: '正常',
late: '迟到',
absent: '缺勤',
leave: '请假'
};
message += ` - 考勤状态: ${statusMap[status] || '未知'}`;
}
ElMessage.success(message);
};
// 获取考勤状态
const getAttendanceStatus = (day) => {
if (attendanceData.value[currentYear.value] &&
attendanceData.value[currentYear.value][currentMonth.value + 1] &&
attendanceData.value[currentYear.value][currentMonth.value + 1][day]) {
return attendanceData.value[currentYear.value][currentMonth.value + 1][day];
}
return null;
};
const isToday = (day) => {
return day === today.value.getDate() &&
currentMonth.value === today.value.getMonth() &&
currentYear.value === today.value.getFullYear();
};
const isCurrentDay = (day) => {
return day === today.value.getDate() &&
currentMonth.value === today.value.getMonth() &&
currentYear.value === today.value.getFullYear();
};
const isSelectedDay = (day) => {
return day === selectedDate.value.getDate() &&
currentMonth.value === selectedDate.value.getMonth() &&
currentYear.value === selectedDate.value.getFullYear();
};
</script>
<style scoped lang="scss">
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
.chart-header span {
color: #186DF5;
font-size: 14px;
cursor: pointer;
}
.calendar-container {
background: white;
border-radius: 8px;
padding: 16px;
// border: 1px solid #F2F3F5;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 8px 0;
}
.current-month {
font-size: 16px;
font-weight: 500;
color: #333;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-bottom: 8px;
}
.weekday {
text-align: center;
font-size: 14px;
color: #666;
font-weight: 500;
padding: 8px 0;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
height: 350px;
}
.day {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 40px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.prev-month-day,
.next-month-day {
color: #C0C4CC;
}
.current-month-day {
color: #333;
}
.current-month-day:hover {
background-color: #ECF5FF;
color: #186DF5;
}
.current-day {
position: relative;
}
.today-marker {
position: absolute;
bottom: 4px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #FF6B3B;
}
.selected-day {
background-color: #186DF5;
color: white !important;
font-weight: 500;
}
.selected-day:hover {
background-color: #4096ff;
color: white !important;
}
// 考勤状态标记样式
.attendance-marker {
position: absolute;
bottom: 2px;
width: 6px;
height: 6px;
border-radius: 50%;
}
.attendance-marker.normal {
background-color: #52c41a;
}
.attendance-marker.late {
background-color: #faad14;
}
.attendance-marker.absent {
background-color: #f5222d;
}
.attendance-marker.leave {
background-color: #1890ff;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="box">
<TitleComponent title="今日出勤" :font-level="2" />
<div class="todayAttend">
<div class="todayAttendItem">
<img :src="props.todayAttendData.attendance.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">出勤</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.attendance.count }}</span>
</div>
</div>
<div class="todayAttendItem">
<img :src="props.todayAttendData.late.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">迟到</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.late.count }}</span>
</div>
</div>
<div class="todayAttendItem">
<img :src="props.todayAttendData.earlyLeave.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">早退</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.earlyLeave.count }}</span>
</div>
</div>
<div class="todayAttendItem">
<img :src="props.todayAttendData.absent.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">缺勤</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.absent.count }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
// 接收从父组件传入的数据
const props = defineProps({
todayAttendData: {
type: Object,
default: () => ({})
}
});
</script>
<style scoped lang="scss">
.todayAttend {
display: flex;
justify-content: space-between;
align-items: center;
}
.todayAttendItem {
width: 110px;
height: 100px;
background: #E5F0FF;
padding: 5px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
.todayAttendItemInfo{
display: flex;
justify-content: space-around;
align-items: center;
.todayAttendItemTitle {
color: rgba(113, 128, 150, 1);
font-size: 12px;
}
.todayAttendItemNum {
font-size: 16px;
color: rgba(0, 30, 59, 1);
}
}
}
</style>

View File

@ -0,0 +1,179 @@
<template>
<div class="schedule-table-container">
<el-table
:data="scheduleData"
style="width: 100%"
max-height="600"
stripe
border
>
<!-- 固定列 -->
<el-table-column fixed prop="name" label="姓名" width="120" align="center" />
<el-table-column fixed="left" prop="position" label="岗位" width="120" align="center" />
<el-table-column fixed="left" prop="weeklyHours" label="周总计/小时" width="120" align="center" />
<!-- 日期列 - 纵向显示号数和星期几 -->
<el-table-column
v-for="(dateInfo, index) in currentMonthDates"
:key="index"
:prop="`day${index + 1}`"
width="80"
align="center"
>
<template #header>
<div class="vertical-header">
<div class="date-number">{{ dateInfo.date }}</div>
<div class="week-day">{{ dateInfo.weekDay }}</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
// 员工列表
const employees = [
{ name: '张三', position: '水泥工', weeklyHours: 142 },
{ name: '李四', position: '电工', weeklyHours: 138 },
{ name: '王五', position: '木工', weeklyHours: 145 },
{ name: '赵六', position: '钢筋工', weeklyHours: 140 },
{ name: '钱七', position: '油漆工', weeklyHours: 135 },
{ name: '孙八', position: '瓦工', weeklyHours: 143 },
{ name: '周九', position: '钳工', weeklyHours: 137 },
{ name: '吴十', position: '管道工', weeklyHours: 139 },
{ name: '郑十一', position: '焊工', weeklyHours: 141 },
{ name: '王十二', position: '起重工', weeklyHours: 136 }
];
// 排班类型
const shifts = ['早班', '中班', '晚班', '休息'];
// 获取当前月的日期信息
const currentMonthDates = ref<any[]>([]);
// 计算当前月份并生成日期信息
const getCurrentMonthDates = () => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth(); // 0-11
// 获取当月第一天
const firstDay = new Date(year, month, 1);
// 获取当月最后一天
const lastDay = new Date(year, month + 1, 0);
// 当月总天数
const daysInMonth = lastDay.getDate();
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const dates = [];
// 生成当月所有日期信息
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const weekDayIndex = date.getDay(); // 0-60表示星期日
dates.push({
date: i,
weekDay: weekdays[weekDayIndex],
fullDate: `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
});
}
return dates;
};
// 生成排班数据
const scheduleData = computed(() => {
return Array.from({ length: 20 }, (_, index) => {
// 循环使用员工数据
const employee = employees[index % employees.length];
// 为每行生成不同的排班组合
const rowData = {
name: employee.name,
position: employee.position,
weeklyHours: employee.weeklyHours
};
// 为当月每一天生成排班数据
currentMonthDates.value.forEach((_, dayIndex) => {
// 使用不同的种子生成略有变化的排班模式
const seed = (index * 3 + dayIndex + 1) % shifts.length;
rowData[`day${dayIndex + 1}`] = shifts[seed];
});
return rowData;
});
});
// 组件挂载时获取当前月数据
onMounted(() => {
currentMonthDates.value = getCurrentMonthDates();
});
</script>
<style scoped>
.schedule-table-container {
overflow-x: auto;
}
/* 优化滚动条样式 */
.schedule-table-container::-webkit-scrollbar {
height: 8px;
}
.schedule-table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.schedule-table-container::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 4px;
}
.schedule-table-container::-webkit-scrollbar-thumb:hover {
background: #909399;
}
/* 优化表格样式 */
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-table__header-wrapper th) {
background-color: #fafafa;
font-weight: 500;
padding: 0 !important;
height: auto !important;
min-height: 60px;
}
:deep(.el-table__body-wrapper) {
overflow-x: visible;
}
/* 纵向表头样式 */
.vertical-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 8px 0;
}
.date-number {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.week-day {
font-size: 12px;
color: #666;
}
</style>

View File

@ -0,0 +1,290 @@
<template>
<div class="total-view-container">
<div class="total-view-content">
<!-- 使用循环生成统计卡片 -->
<div v-for="(item, index) in statsItems" :key="index" class="stats-card">
<div class="stats-card-header">
<span class="stats-title">{{ item.title }}</span>
<span class="stats-change" :class="{ positive: item.data.isPositive, negative: !item.data.isPositive }">
{{ item.data.isPositive ? '↑' : '↓' }} {{ item.data.change }} {{ item.compareText }}
</span>
</div>
<div class="stats-card-body">
<div class="stats-value">{{ item.data.value }}</div>
<div class="stats-chart">
<div :ref="el => chartRefs[index] = el" class="chart-container"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
// 接收从父组件传入的数据
const props = defineProps({
totalData: {
type: Object,
default: () => ({
attendance: {
value: 248,
change: '+8.2%',
isPositive: true,
chartData: [150, 230, 224, 218, 135, 300, 220],
color: '#FF7D00',
title: '总出勤人数',
compareText: '较昨日同期',
chartType: 'bar'
},
rest: {
value: 8,
change: '+8.2%',
isPositive: true,
chartData: [10, 12, 15, 8, 7, 9, 10],
color: '#00C48C',
title: '调休',
compareText: '较上月同期',
chartType: 'line'
},
leave: {
value: 24,
change: '-10%',
isPositive: false,
chartData: [30, 25, 28, 22, 20, 26, 24],
color: '#FF5252',
title: '本月请假',
compareText: '较昨日同期',
chartType: 'line'
},
rate: {
value: '96.8%',
change: '+10%',
isPositive: true,
chartData: [90, 92, 94, 95, 97, 98, 96.8],
color: '#029CD4',
title: '平均出勤率',
compareText: '较昨日同期',
chartType: 'line'
}
})
}
});
// 图表引用数组
const chartRefs = ref([]);
// 转换totalData为数组格式方便循环渲染
const statsItems = computed(() => {
return Object.keys(props.totalData).map(key => ({
title: props.totalData[key].title,
data: {
value: props.totalData[key].value,
change: props.totalData[key].change,
isPositive: props.totalData[key].isPositive,
chartData: props.totalData[key].chartData,
color: props.totalData[key].color
},
compareText: props.totalData[key].compareText,
chartType: props.totalData[key].chartType
}));
});
// 初始化图表
const initCharts = () => {
const chartInstances = [];
// 循环初始化所有图表
statsItems.value.forEach((item, index) => {
if (!chartRefs.value[index]) return;
const chartInstance = echarts.init(chartRefs.value[index]);
// 根据图表类型设置不同的配置
if (item.chartType === 'bar') {
// 柱状图配置
chartInstance.setOption({
tooltip: { show: false },
grid: { top: 0, bottom: 0, left: -70, right: 0, containLabel: true },
xAxis: {
type: 'category',
data: Array(item.data.chartData.length).fill(''),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false }
},
yAxis: {
type: 'value',
show: false
},
series: [{
data: item.data.chartData,
type: 'bar',
barWidth: 10,
itemStyle: {
color: item.data.color,
borderRadius: [10, 10, 0, 0] // 柱状图圆角
},
emphasis: {
focus: 'series'
}
}]
});
} else if (item.chartType === 'line') {
// 折线图配置
chartInstance.setOption({
tooltip: { show: false },
grid: { top: 10, bottom: 0, left: -30, right: 0, containLabel: true },
xAxis: {
type: 'category',
data: Array(item.data.chartData.length).fill(''),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false }
},
yAxis: {
type: 'value',
show: false
},
series: [{
data: item.data.chartData,
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
width: 4, // 折线图线条加粗
color: item.data.color
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: `${item.data.color}33`
}, {
offset: 1,
color: `${item.data.color}02`
}])
}
}]
});
}
chartInstances.push(chartInstance);
});
// 响应窗口大小变化
window.addEventListener('resize', () => {
chartInstances.forEach(instance => {
instance.resize();
});
});
};
// 监听props变化重新初始化图表
watch(() => props.totalData, () => {
// 清空之前的图表引用
chartRefs.value = [];
// 等待DOM更新后重新初始化图表
nextTick(() => {
initCharts();
});
}, { deep: true });
// 组件挂载后初始化图表
onMounted(() => {
initCharts();
});
</script>
<style scoped lang="scss">
.total-view-container {
background-color: #fff;
border-radius: 8px;
padding: 20px;
height: 217px;
}
.total-view-content {
display: flex;
justify-content: space-between;
gap: 0;
}
.stats-card {
flex: 1;
padding: 16px;
background-color: #fff;
border-radius: 0;
border: none;
border-right: 1px dashed #E4E7ED;
}
.stats-card:last-child {
border-right: none;
}
.stats-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.stats-title {
font-size: 14px;
color: #606266;
}
.stats-change {
font-size: 12px;
}
.stats-change.positive {
color: #52C41A;
}
.stats-change.negative {
color: #F5222D;
}
.stats-card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.stats-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
.stats-chart {
width: 100%;
height: 60px;
}
.chart-container {
width: 100%;
height: 100%;
}
// 响应式布局
@media screen and (max-width: 1200px) {
.total-view-content {
flex-wrap: wrap;
}
.stats-card {
width: calc(50% - 10px);
}
}
@media screen and (max-width: 768px) {
.stats-card {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,325 @@
<template>
<div class="model">
<!-- 标题栏 -->
<el-row :gutter="24">
<el-col :span="12">
<TitleComponent title="考勤管理" subtitle="项目出勤情况、人员排班及请假调休管理" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<!-- 子col1下拉 -->
<el-col :span="4">
<el-select placeholder="选择电站">
<el-option label="所有电站" value="all"></el-option>
</el-select>
</el-col>
<!-- 子col2下拉框容器 -->
<el-col :span="4">
<el-select placeholder="日期范围">
<el-option label="所有月份" value="all"></el-option>
</el-select>
</el-col>
<el-col :span="4">
<el-button type="primary">
导出数据
<el-icon class="el-icon--right">
<UploadFilled />
</el-icon>
</el-button>
</el-col>
</el-col>
</el-row>
<!-- 第一行totalView infoBox -->
<el-row :gutter="20">
<el-col :span="17">
<totalView :totalData="totalData"></totalView>
</el-col>
<el-col :span="7">
<infoBox></infoBox>
</el-col>
</el-row>
<!-- 第二行人员排班和出勤趋势分析 -->
<el-row :gutter="20">
<el-col :span="17">
<div class="analysis-content">
<attendTrend :attendData="attendData"></attendTrend>
<el-card>
<TitleComponent title="人员排班" :fontLevel="2" />
<renyuanpaiban></renyuanpaiban>
</el-card>
</div>
</el-col>
<!-- 右侧日历卡片 -->
<el-col :span="7">
<div class="calendar-content">
<el-card>
<calendar :calendarData="calendarData"></calendar>
<todayAttend :todayAttendData="todayAttendData"></todayAttend>
<approval :approvalData="approvalData"></approval>
</el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import infoBox from '@/views/integratedManage/attendManage/components/infoBox.vue'
import attendTrend from '@/views/integratedManage/attendManage/components/attendTrend.vue'
import todayAttend from '@/views/integratedManage/attendManage/components/leftBox/todayAttend.vue'
import approval from '@/views/integratedManage/attendManage/components/leftBox/approval.vue'
import calendar from '@/views/integratedManage/attendManage/components/leftBox/calendar.vue'
import totalView from '@/views/integratedManage/attendManage/components/totalView.vue'
import renyuanpaiban from '@/views/integratedManage/attendManage/components/renyuanpaiban.vue'
import { ref } from 'vue';
// 出勤数据 - 用于attendTrend组件
const attendData = ref(
{
week: {
xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
actualCount: [40, 20, 30, 15, 22, 63, 58, 43, 39, 36],
expectedCount: [100, 556, 413, 115, 510, 115, 317, 118, 14, 7]
},
month: {
xAxis: ['第1周', '第2周', '第3周', '第4周'],
actualData: [280, 360, 320, 400],
theoreticalData: [300, 400, 350, 450]
},
}
)
// Mock数据 - 更新为循环生成所需的数据结构
const totalData = ref({
attendance: {
value: 248,
change: '+8.2%',
isPositive: true,
chartData: [150, 230, 224, 218, 135, 300, 220],
color: '#FF7D00',
title: '总出勤人数',
compareText: '较昨日同期',
chartType: 'bar'
},
rest: {
value: 8,
change: '+8.2%',
isPositive: true,
chartData: [10, 12, 15, 8, 7, 9, 10],
color: '#00C48C',
title: '调休',
compareText: '较上月同期',
chartType: 'line'
},
leave: {
value: 24,
change: '-10%',
isPositive: false,
chartData: [30, 25, 28, 22, 20, 26, 24],
color: '#FF5252',
title: '本月请假',
compareText: '较昨日同期',
chartType: 'line'
},
rate: {
value: '96.8%',
change: '+10%',
isPositive: true,
chartData: [90, 92, 94, 95, 97, 98, 96.8],
color: '#029CD4',
title: '平均出勤率',
compareText: '较昨日同期',
chartType: 'line'
}
});
// 审批数据 - 用于approval组件
const approvalData = ref([
{
type: '事假',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '病假',
days: 2,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '调休',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '事假',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '事假',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '已通过',
statusType: 'success',
iconPath: '/src/assets/demo/approval.png'
}
]);
// 今日出勤数据 - 用于todayAttend组件
const todayAttendData = ref({
attendance: {
count: 150,
icon: '/src/assets/demo/qin.png'
},
late: {
count: 5,
icon: '/src/assets/demo/chi.png'
},
earlyLeave: {
count: 2,
icon: '/src/assets/demo/tui.png'
},
absent: {
count: 8,
icon: '/src/assets/demo/que.png'
}
});
// 日历数据 - 用于calendar组件
const calendarData = ref({
// 初始化当前日期
today: new Date(),
currentDate: new Date(2025, 8, 27), // 2025年9月27日截图中显示的日期
selectedDate: new Date(2025, 8, 27),
// 模拟考勤数据
attendanceData: {
2025: {
9: {
1: 'normal',
4: 'late',
8: 'absent',
10: 'leave',
15: 'normal',
20: 'normal',
25: 'late',
27: 'normal'
}
}
}
});
</script>
<style scoped lang="scss">
.model {
padding: 24px 20px;
background-color: rgba(242, 248, 252, 1);
}
/* 标题栏与内容区域间距 */
.el-row+.el-row {
margin-top: 24px;
}
/* 分析内容区域 */
.analysis-content {
display: flex;
flex-direction: column;
gap: 45px;
// border: 1px solid red;
}
/* 日历内容区域 */
.calendar-content {
display: flex;
flex-direction: column;
}
/* 右侧日历卡片内组件间距 */
.calendar-content .el-card {
display: flex;
flex-direction: column;
height: 100%;
}
.calendar-content .el-card > * {
margin-bottom: 16px;
}
.calendar-content .el-card > *:last-child {
margin-bottom: 0;
flex: 1;
}
/* 卡片样式统一 */
.el-card {
border-radius: 8px !important;
border: none !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
/* 下拉选择器和按钮样式调整 */
.el-select {
width: 100%;
margin-right: 12px;
}
.el-button {
margin-left: 8px;
}
/* 响应式布局优化 */
@media screen and (max-width: 1200px) {
.model {
padding: 16px;
}
.el-row+.el-row {
margin-top: 16px;
}
.analysis-content {
gap: 16px;
}
/* 日历卡片内组件间距 */
.calendar-content .el-card > * {
margin-bottom: 12px;
}
}
/* 更细粒度的响应式调整 */
@media screen and (max-width: 768px) {
.model {
padding: 12px;
}
.el-select {
margin-right: 8px;
}
.el-button {
margin-left: 4px;
padding: 8px 12px;
}
}
</style>

View File

@ -1,9 +1,65 @@
<template> <template>
<div class="manage-form-container"> <div class="manage-form-container">
<!-- 搜索和筛选区域 --> <!-- 搜索和筛选区域 -->
<div class="search-filter-section">
<el-row gutter="12" align="middle">
<el-col :span="2">
<el-select v-model="searchForm.deviceType" placeholder="设备类型" clearable>
<el-option label="全部类型" value="" />
<el-option label="逆变器" value="逆变器" />
<el-option label="传感器" value="传感器" />
<el-option label="电表" value="电表" />
<el-option label="摄像头" value="摄像头" />
<el-option label="控制器" value="控制器" />
</el-select>
</el-col>
<el-col :span="2">
<el-select v-model="searchForm.status" placeholder="设备状态" clearable>
<el-option label="全部状态" value="" />
<el-option label="正常" value="normal" />
<el-option label="异常" value="abnormal" />
<el-option label="中断" value="interrupt" />
</el-select>
</el-col>
<el-col :span="2">
<el-select v-model="searchForm.protocol" placeholder="通讯状态" clearable>
<el-option label="全部状态" value="" />
<el-option label="Modbus TCP" value="Modbus TCP" />
<el-option label="其他协议" value="其他" />
</el-select>
</el-col>
<el-col :span="2">
<el-select v-model="searchForm.station" placeholder="所属电站" clearable>
<el-option label="全部电站" value="" />
<el-option label="兴电基站1" value="兴电基站1" />
<el-option label="兴电基站2" value="兴电基站2" />
<el-option label="兴电基站3" value="兴电基站3" />
<el-option label="兴电基站4" value="兴电基站4" />
<el-option label="兴电基站5" value="兴电基站5" />
</el-select>
</el-col>
<el-col :span="2">
<el-button type="primary" @click="handleSearch" style="width: 100%">
<el-icon><Search /></el-icon>
搜索
</el-button>
</el-col>
<el-col :span="3">
<el-button type="primary" @click="handleAddDevice" style="width: 100%">
<el-icon><CirclePlus /></el-icon>
添加设备
</el-button>
</el-col>
<el-col :span="3">
<el-button type="primary" @click="handleBatchConfig" style="width: 100%">
<el-icon><Setting /></el-icon>
批量配置
</el-button>
</el-col>
</el-row>
</div>
<!-- 设备信息表格 --> <!-- 设备信息表格 -->
<el-table v-loading="loading" :data="deviceList" style="width: 100%" height="calc(100vh - 300px)"> <el-table v-loading="loading" :data="deviceList" style="width: 100%">
<el-table-column prop="deviceId" label="设备ID" min-width="120" align="center" /> <el-table-column prop="deviceId" label="设备ID" min-width="120" align="center" />
<el-table-column prop="deviceName" label="设备名称" min-width="120" align="center" /> <el-table-column prop="deviceName" label="设备名称" min-width="120" align="center" />
<el-table-column prop="deviceType" label="类型" min-width="100" align="center"> <el-table-column prop="deviceType" label="类型" min-width="100" align="center">
@ -19,22 +75,22 @@
<el-table-column prop="lastOnlineTime" label="最后在线时间" min-width="150" align="center" /> <el-table-column prop="lastOnlineTime" label="最后在线时间" min-width="150" align="center" />
<el-table-column prop="status" label="状态" min-width="80"> <el-table-column prop="status" label="状态" min-width="80">
<template #default="scope"> <template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)" :effect="getStatusTagEffect(scope.row.status)"> <el-tag :type="getStatusTagType(scope.row.status)" :effect="light">
{{ getStatusText(scope.row.status) }} {{ getStatusText(scope.row.status) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-width="150" fixed="right"> <el-table-column label="操作" min-width="150" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button type="primary" link @click="handleDetails(scope.row)" size="small"> <span style="color: #1890ff; cursor: pointer; margin-right: 15px;" @click="handleDetails(scope.row)">
查看 查看
</el-button> </span>
<el-button type="primary" link @click="handleConfig(scope.row)" size="small"> <span style="color: #666666; cursor: pointer; margin-right: 15px;" @click="handleConfig(scope.row)">
配置 配置
</el-button> </span>
<el-button type="primary" link @click="handleDelete(scope.row)" size="small"> <span style="color: #666666; cursor: pointer;" @click="handleDelete(scope.row)">
删除 删除
</el-button> </span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -42,15 +98,26 @@
<!-- 分页区域 --> <!-- 分页区域 -->
<div class="pagination-container"> <div class="pagination-container">
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" <el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" layout="prev, pager, next, jumper"
:total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive } from 'vue'; import { Search, CirclePlus, Setting } from '@element-plus/icons-vue';
import { ref, reactive, watch } from 'vue';
// 定义props接收数据
const props = defineProps({
tableData: {
type: Object,
default: () => ({
list: [],
total: 0
})
}
});
// 搜索表单数据 // 搜索表单数据
const searchForm = reactive({ const searchForm = reactive({
@ -68,112 +135,17 @@ const loading = ref(false);
const pagination = reactive({ const pagination = reactive({
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
total: 545 total: 0
}); });
// 设备列表数据 // 设备列表数据
const deviceList = ref([ const deviceList = ref([]);
{
deviceId: 'WO-2023-0620-056', // 监听props变化并更新设备列表
deviceName: '逆变器-01', watch(() => props.tableData, (newData) => {
deviceType: '逆变器', deviceList.value = newData.list || [];
station: '兴电基站1', pagination.total = newData.total || 0;
protocol: 'Modbus TCP', }, { immediate: true, deep: true });
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-057',
deviceName: '温度传感器-45',
deviceType: '传感器',
station: '兴电基站2',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'interrupt'
},
{
deviceId: 'WO-2023-0620-058',
deviceName: '智能电表-03',
deviceType: '电表',
station: '兴电基站3',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'abnormal'
},
{
deviceId: 'WO-2023-0620-059',
deviceName: '监控摄像头-02',
deviceType: '摄像头',
station: '兴电基站4',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-060',
deviceName: '控制器-07',
deviceType: '控制器',
station: '兴电基站5',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-061',
deviceName: '逆变器-02',
deviceType: '逆变器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-062',
deviceName: '电流传感器-08',
deviceType: '传感器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-063',
deviceName: '多功能电表-12',
deviceType: '电表',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-064',
deviceName: '门禁摄像头-05',
deviceType: '摄像头',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-065',
deviceName: '开关控制器-15',
deviceType: '控制器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
}
]);
// 获取状态文本 // 获取状态文本
const getStatusText = (status) => { const getStatusText = (status) => {
@ -195,11 +167,7 @@ const getStatusTagType = (status) => {
return typeMap[status] || 'default'; return typeMap[status] || 'default';
}; };
// 获取状态标签效果
const getStatusTagEffect = (status) => {
// 正常状态使用浅色效果,其他状态使用深色效果
return status === 'normal' ? 'light' : 'dark';
};
// 获取设备类型标签类型 // 获取设备类型标签类型
const getDeviceTypeTagType = (deviceType) => { const getDeviceTypeTagType = (deviceType) => {
@ -216,6 +184,7 @@ const getDeviceTypeTagType = (deviceType) => {
// 处理搜索 // 处理搜索
const handleSearch = () => { const handleSearch = () => {
loading.value = true; loading.value = true;
pagination.currentPage = 1;
// 模拟搜索请求 // 模拟搜索请求
setTimeout(() => { setTimeout(() => {
loading.value = false; loading.value = false;
@ -239,19 +208,19 @@ const handleBatchConfig = () => {
// 处理查看详情 // 处理查看详情
const handleDetails = (row) => { const handleDetails = (row) => {
// 实际项目中这里应该打开设备详情的弹窗或跳转到详情页面 // 实际项目中这里应该打开设备详情的弹窗或跳转到详情页面
ElMessage.success(`查看设备${row.deviceCode}详情`); ElMessage.success(`查看设备${row.deviceId}详情`);
}; };
// 处理配置 // 处理配置
const handleConfig = (row) => { const handleConfig = (row) => {
// 实际项目中这里应该打开设备配置的弹窗 // 实际项目中这里应该打开设备配置的弹窗
ElMessage.success(`配置设备${row.deviceCode}`); ElMessage.success(`配置设备${row.deviceId}`);
}; };
// 处理删除 // 处理删除
const handleDelete = (row) => { const handleDelete = (row) => {
ElMessageBox.confirm( ElMessageBox.confirm(
`确定要删除设备${row.deviceCode}吗?`, `确定要删除设备${row.deviceId}吗?`,
'提示', '提示',
{ {
confirmButtonText: '确定', confirmButtonText: '确定',
@ -289,12 +258,60 @@ const handleCurrentChange = (current) => {
min-height: 100%; min-height: 100%;
} }
.search-form { .search-filter-section {
margin-bottom: 20px; margin-bottom: 24px;
display: flex; padding: 16px;
align-items: center; background-color: #f5f7fa;
flex-wrap: wrap; border-radius: 8px;
gap: 10px; }
/* 下拉选择框样式优化 */
:deep(.el-select) {
width: 100%;
}
:deep(.el-input__wrapper) {
border-radius: 6px;
}
/* 按钮样式优化 */
:deep(.el-button) {
border-radius: 6px;
font-size: 14px;
transition: all 0.3s ease;
}
:deep(.el-button--primary) {
background-color: #1890ff;
border-color: #1890ff;
}
:deep(.el-button--primary:hover) {
background-color: #40a9ff;
border-color: #40a9ff;
}
/* 响应式设计 */
@media screen and (max-width: 1200px) {
.search-filter-section .el-col {
margin-bottom: 12px;
}
}
@media screen and (max-width: 768px) {
.search-filter-section {
padding: 12px;
}
.search-filter-section .el-row {
display: flex;
flex-direction: column;
}
.search-filter-section .el-col {
width: 100% !important;
margin-bottom: 12px;
}
} }
.action-buttons { .action-buttons {

View File

@ -10,6 +10,16 @@
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
// 定义props接收数据
const props = defineProps({
trendData: {
type: Object,
default: () => ({
dates: [],
series: []
})
}
});
// 图表DOM引用 // 图表DOM引用
const chartRef = ref(null); const chartRef = ref(null);
@ -54,7 +64,7 @@ const initChart = () => {
color: '#EAEBF0' color: '#EAEBF0'
} }
}, },
data: ['9-12', '9-13', '9-14', '9-15', '9-16', '9-17', '9-18'] data: props.trendData.dates || []
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
@ -73,48 +83,20 @@ const initChart = () => {
} }
} }
}, },
series: [ series: props.trendData.series.map((item, index) => ({
{ name: item.name,
name: '正常', data: item.data,
data: [20, 10, 50, 80, 70, 10, 30], type: 'bar',
type: 'bar', stack: 'one',
stack: 'one', color: item.color,
color: '#7339F5', itemStyle: {
itemStyle: { borderWidth: 1,
// borderWidth: 1, borderColor: 'rgba(255, 255, 255, 1)',
borderColor: 'rgba(255, 255, 255, 1)', //同背景色一样 barBorderRadius: 8
barBorderRadius: 8
},
barWidth: '20',
}, },
{ barWidth: index === 0 ? '20' : index === 2 ? '12' : '20',
name: '中断', }))
data: [80, 30, 50, 80, 70, 10, 30], }
type: 'bar',
stack: 'one', //堆叠
color: '#FF8A00',
itemStyle: {
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 1)', //同背景色一样
barBorderRadius: 8
},
},
{
name: '异常',
data: [50, 30, 50, 80, 70, 10, 30],
type: 'bar',
stack: 'one', //堆叠
color: '#DE4848',
itemStyle: {
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 1)', //同背景色一样
barBorderRadius: 8
},
barWidth: '12',
}
]
};
chartInstance.setOption(option); chartInstance.setOption(option);
}; };

View File

@ -9,6 +9,36 @@
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
// 定义props接收数据
const props = defineProps({
pieData: {
type: Object,
default: () => ({
normal: {
value: 28,
name: '提示信息',
displayName: '设备正常'
},
interrupt: {
value: 45,
name: '一般告警',
displayName: '设备中断'
},
abnormal: {
value: 55,
name: '重要告警',
displayName: '设备异常'
}
})
}
});
// 默认的三种颜色
const defaultColors = {
normal: '#43CF7C',
interrupt: '#00B3FF',
abnormal: '#FB3E7A'
};
// 图表DOM引用 // 图表DOM引用
const chartRef = ref(null); const chartRef = ref(null);
@ -24,16 +54,10 @@ const initChart = () => {
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: function(params) { formatter: function(params) {
// 定义名称映射关系
const nameMap = {
'提示信息': '设备正常',
'一般告警': '设备中断',
'重要告警': '设备异常'
};
// 使用ECharts提供的百分比值 // 使用ECharts提供的百分比值
const percentage = params.percent.toFixed(1); const percentage = params.percent.toFixed(1);
// 返回格式化后的文本 // 返回格式化后的文本
return `${nameMap[params.name]}: ${params.value}台 (${percentage}%)`; return `${params.data.displayName}: ${params.value}台 (${percentage}%)`;
} }
}, },
grid: { grid: {
@ -50,26 +74,18 @@ const initChart = () => {
itemWidth: 15, itemWidth: 15,
itemHeight: 15, itemHeight: 15,
formatter: function(name) { formatter: function(name) {
// 定义名称映射关系 const data = props.pieData.normal.name === name ? props.pieData.normal :
const nameMap = { props.pieData.interrupt.name === name ? props.pieData.interrupt :
'提示信息': '设备正常', props.pieData.abnormal;
'一般告警': '设备中断',
'重要告警': '设备异常'
};
// 定义数值映射关系
const valueMap = {
'提示信息': 28,
'一般告警': 45,
'重要告警': 55
};
// 返回格式化后的文本 // 返回格式化后的文本
return `${nameMap[name] || name}(${valueMap[name]})`; return `${data.displayName}(${data.value})`;
} }
}, },
series: [ series: [
{ {
type: 'pie', type: 'pie',
radius: ['40%', '70%'], radius: ['40%', '70%'],
center:['40%','50%'],
label: { label: {
show: false show: false
}, },
@ -77,14 +93,26 @@ const initChart = () => {
show: true show: true
}, },
color: [ color: [
'#43CF7C', // 设备正常 defaultColors.normal,
'#00B3FF', // 设备中断 defaultColors.interrupt,
'#FB3E7A', // 设备异常 defaultColors.abnormal
], ],
data: [ data: [
{ value: 28, name: '提示信息' }, {
{ value: 45, name: '一般告警' }, value: props.pieData.normal.value,
{ value: 55, name: '重要告警' }, name: props.pieData.normal.name,
displayName: props.pieData.normal.displayName
},
{
value: props.pieData.interrupt.value,
name: props.pieData.interrupt.name,
displayName: props.pieData.interrupt.displayName
},
{
value: props.pieData.abnormal.value,
name: props.pieData.abnormal.name,
displayName: props.pieData.abnormal.displayName
}
], ],
emphasis: { emphasis: {
itemStyle: { itemStyle: {
@ -130,7 +158,7 @@ onMounted(() => {
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
height: 150px; height: 250px;
width: 100%; width: 100%;
padding: 5px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;

View File

@ -0,0 +1,174 @@
<template>
<div class="chart-container">
<el-row style="padding: 0 0 0 20px;box-sizing: border-box;">
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/rebot.png" alt="">
</div>
<div class="item-title">设备总数</div>
<div class="item-value">{{ totalData.deviceCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/up.png" alt="">
<span class="trend-num">+{{ totalData.increase || 8 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/wifi.png" alt="">
</div>
<div class="item-title">正常设备</div>
<div class="item-value">{{ totalData.normalCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/up.png" alt="">
<span class="trend-num">+{{ totalData.normalIncrease || 5 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/wifiwarn.png" alt="">
</div>
<div class="item-title">异常设备</div>
<div class="item-value">{{ totalData.abnormalCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/down.png" alt="">
<span class="trend-num">-{{ totalData.abnormalDecrease || 3 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/nowifi.png" alt="">
</div>
<div class="item-title">中断设备</div>
<div class="item-value">{{ totalData.interruptCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/down.png" alt="" class="trend-icon">
<span class="trend-num">-{{ totalData.interruptDecrease || 2 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 定义props接收数据
const props = defineProps({
totalData: {
type: Object,
default: () => ({
deviceCount: 0,
normalCount: 0,
abnormalCount: 0,
interruptCount: 0,
increase: 0,
normalIncrease: 0,
abnormalDecrease: 0,
interruptDecrease: 0
})
}
});
</script>
<style scoped lang="scss">
.item-box {
padding: 20px;
width: 216px;
height: 282px;
border-radius: 10px;
// border: 1px solid red;
background: rgba(255, 255, 255, 1);
display: flex;
flex-direction: column;
justify-content: space-between;
.item-icon {
width: 49.94px;
height: 49.91px;
border-radius: 8px;
background: rgba(24, 109, 245, 1);
display: flex;
align-items: center;
justify-content: center;
}
.item-title {
width: 129.85px;
height: 21.21px;
/** 文本1 */
font-size: 16px;
font-weight: 500;
letter-spacing: 0px;
line-height: 23.17px;
color: rgba(182, 182, 182, 1);
text-align: left;
vertical-align: top;
}
.item-value {
border-radius: 10px;
background: rgba(255, 255, 255, 1);
font-size: 24px;
font-weight: 400;
letter-spacing: 0px;
line-height: 34.75px;
color: rgba(0, 0, 0, 1);
text-align: left;
vertical-align: top;
}
.item-unit {
width: 38.71px;
height: 21.21px;
opacity: 1;
/** 文本1 */
font-size: 16px;
font-weight: 500;
letter-spacing: 0px;
line-height: 23.17px;
color: rgba(182, 182, 182, 1);
text-align: left;
vertical-align: top;
}
.item-trend {
.trend-num {
font-size: 14px;
font-weight: 500;
letter-spacing: 0px;
line-height: 14.48px;
color: rgba(0, 184, 122, 1);
text-align: center;
vertical-align: middle;
margin-right: 10px;
margin-left: 10px;
}
.trend-des {
font-size: 14px;
font-weight: 500;
letter-spacing: 0px;
line-height: 14.48px;
color: rgba(154, 154, 154, 1);
text-align: left;
vertical-align: middle;
}
}
}
</style>

View File

@ -1,45 +1,54 @@
<template> <template>
<div class="model"> <div class="model">
<!-- 标题栏 --> <!-- 标题栏 -->
<el-row> <el-row :gutter="24">
<el-col :span="12"> <el-col :span="12">
<TitleComponent title="报警管理" subtitle="配置新能源厂站的报警级别、类型及相关规则" /> <TitleComponent title="设备状态管理" subtitle="监控和管理所有设备的运行状态" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<el-col :span="4">
<el-button type="primary">
导出数据
<el-icon class="el-icon--right">
<UploadFilled />
</el-icon>
</el-button>
</el-col>
</el-col> </el-col>
</el-row> </el-row>
<!-- 第一行设备统计和状态分布 -->
<!-- 第一行报警管理和报警级别分布 --> <el-row :gutter="20" class="content-row equal-height-row">
<el-row :gutter="20" class="content-row">
<el-col :span="16"> <el-col :span="16">
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警管理" :font-level="2" /> <totalView :totalData="totalData" />
<totalView />
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<!-- 报警级别分布 --> <!-- 设备状态分布 -->
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警级别分布" :font-level="2" /> <TitleComponent title="设备状态分布" :font-level="2" />
<statusPie /> <statusPie :pieData="pieData" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<!-- 第二行报警趋势分析 --> <!-- 第二行设备状态趋势 -->
<el-row :gutter="20" class="content-row"> <el-row :gutter="20" class="content-row">
<el-col :span="24"> <el-col :span="24">
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警趋势分析" :font-level="2" /> <TitleComponent title="设备状态趋势" :font-level="2" />
<stateTrend /> <stateTrend :trendData="trendData" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<!-- 第三行报警管理表单 --> <!-- 第三行设备管理表单 -->
<el-row :gutter="20" class="content-row"> <el-row :gutter="20" class="content-row">
<el-col :span="24"> <el-col :span="24">
<el-card shadow="hover" class="custom-card"> <el-card shadow="hover" class="custom-card">
<TitleComponent title="报警管理表单" :font-level="2" /> <TitleComponent title="设备管理表单" :font-level="2" />
<manageForm /> <manageForm :tableData="tableData" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -47,11 +56,175 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue';
import TitleComponent from '@/components/TitleComponent/index.vue'; import TitleComponent from '@/components/TitleComponent/index.vue';
import totalView from '@/views/integratedManage/alarmManage/components/totalView.vue'; import totalView from '@/views/integratedManage/stateManage/components/totalView.vue';
import stateTrend from '@/views/integratedManage/stateManage/components/stateTrend.vue' import stateTrend from '@/views/integratedManage/stateManage/components/stateTrend.vue'
import statusPie from '@/views/integratedManage/stateManage/components/statusPie.vue' import statusPie from '@/views/integratedManage/stateManage/components/statusPie.vue'
import manageForm from '@/views/integratedManage/stateManage/components/manageForm.vue'; import manageForm from '@/views/integratedManage/stateManage/components/manageForm.vue';
// Mock数据 - 设备统计数据
const totalData = ref({
deviceCount: 545,
normalCount: 436,
abnormalCount: 65,
interruptCount: 44,
increase: 8,
normalIncrease: 5,
abnormalDecrease: 3,
interruptDecrease: 2
});
// Mock数据 - 饼图数据
const pieData = ref({
normal: {
value: 436,
name: 'normal',
displayName: '设备正常',
percent: '80%'
},
abnormal: {
value: 65,
name: 'abnormal',
displayName: '设备异常',
percent: '12%'
},
interrupt: {
value: 44,
name: 'interrupt',
displayName: '设备中断',
percent: '8%'
}
});
// Mock数据 - 趋势图数据
const trendData = ref({
dates: ['9-12', '9-13', '9-14', '9-15', '9-16', '9-17', '9-18'],
series: [
{
name: '正常',
data: [20, 10, 50, 80, 70, 10, 30],
color: '#7339F5'
},
{
name: '中断',
data: [80, 30, 50, 80, 70, 10, 30],
color: '#FF8A00'
},
{
name: '异常',
data: [50, 30, 50, 80, 70, 10, 30],
color: '#DE4848'
}
]
});
// Mock数据 - 表格数据
const tableData = ref({
list: [
{
deviceId: 'WO-2023-0620-056',
deviceName: '逆变器-01',
deviceType: '逆变器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-057',
deviceName: '温度传感器-45',
deviceType: '传感器',
station: '兴电基站2',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'interrupt'
},
{
deviceId: 'WO-2023-0620-058',
deviceName: '智能电表-03',
deviceType: '电表',
station: '兴电基站3',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'abnormal'
},
{
deviceId: 'WO-2023-0620-059',
deviceName: '监控摄像头-02',
deviceType: '摄像头',
station: '兴电基站4',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-060',
deviceName: '控制器-07',
deviceType: '控制器',
station: '兴电基站5',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-061',
deviceName: '逆变器-02',
deviceType: '逆变器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-062',
deviceName: '电流传感器-08',
deviceType: '传感器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-063',
deviceName: '多功能电表-12',
deviceType: '电表',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-064',
deviceName: '门禁摄像头-05',
deviceType: '摄像头',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-065',
deviceName: '开关控制器-15',
deviceType: '控制器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
}
],
total: 545
});
</script> </script>
<style scoped> <style scoped>
@ -74,11 +247,41 @@ import manageForm from '@/views/integratedManage/stateManage/components/manageFo
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.equal-height-row {
display: flex;
align-items: stretch;
}
.equal-height-row .el-col {
display: flex;
}
.equal-height-row .custom-card {
flex: 1;
display: flex;
flex-direction: column;
}
.equal-height-row .el-card__body {
flex: 1;
display: flex;
flex-direction: column;
}
/* 响应式布局调整 */ /* 响应式布局调整 */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.content-row { .content-row {
margin-bottom: 15px; margin-bottom: 15px;
} }
.equal-height-row {
flex-direction: column;
}
.equal-height-row .el-col {
width: 100%;
margin-bottom: 20px;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@ -0,0 +1,225 @@
<template>
<div class="detaildata-container">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
stripe
border
>
<el-table-column prop="datetime" label="日期" align="center" />
<el-table-column prop="prbs" label="发电量(Kwh)" align="center">
<template #default="scope">
<span>{{ scope.row.prbs }}</span>
</template>
</el-table-column>
<el-table-column prop="prz" label="同比(%)" align="center">
<template #default="scope">
<span :class="{
'text-red': scope.row.prz < 0,
'text-green': scope.row.prz > 0
}">
{{ scope.row.prz > 0 ? '+' : '' }}{{ scope.row.prz }}
</span>
</template>
</el-table-column>
<el-table-column prop="prz2" label="环比(%)" align="center">
<template #default="scope">
<span :class="{
'text-red': scope.row.prz2 < 0,
'text-green': scope.row.prz2 > 0
}">
{{ scope.row.prz2 > 0 ? '+' : '' }}{{ scope.row.prz2 }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="设备状态" align="center">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'warning'"
size="small"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="text"
size="small"
@click="handleDetail(scope.row)"
class="text-blue"
>
详情
</el-button>
<el-button
type="text"
size="small"
@click="handleExport(scope.row)"
class="text-blue"
>
导出
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
// 定义表格数据类型
interface TableRow {
datetime: string
prbs: string | number
prz: number
prz2: number
status: string
}
// 响应式数据
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(293)
const tableData = ref<TableRow[]>([])
// 模拟数据生成函数
const generateMockData = (page: number, size: number): TableRow[] => {
const data: TableRow[] = []
const startIndex = (page - 1) * size
// 生成不同的日期
const baseDate = new Date(2023, 5, 30)
for (let i = 0; i < size; i++) {
const index = startIndex + i
if (index >= total.value) break
// 生成不同的日期
const currentDate = new Date(baseDate)
currentDate.setDate(baseDate.getDate() - index)
const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`
// 随机生成正负数,展示不同颜色效果
const randomValue1 = (Math.random() - 0.5) * 10
const randomValue2 = (Math.random() - 0.5) * 10
data.push({
datetime: dateStr,
prbs: (Math.random() * 100 + 150).toFixed(1), // 150-250之间的随机数
prz: Number(randomValue1.toFixed(1)),
prz2: Number(randomValue2.toFixed(1)),
status: i % 8 === 2 ? '预警' : '正常'
})
}
return data
}
// 处理分页大小变化
const handleSizeChange = (size: number) => {
pageSize.value = size
loadData()
}
// 处理当前页码变化
const handleCurrentChange = (current: number) => {
currentPage.value = current
loadData()
}
// 处理详情按钮点击
const handleDetail = (row: TableRow) => {
ElMessage.info('查看详情: ' + row.datetime)
// 实际项目中这里应该跳转到详情页或显示详情对话框
}
// 处理导出按钮点击
const handleExport = (row: TableRow) => {
ElMessage.info('导出数据: ' + row.datetime)
// 实际项目中这里应该调用导出API
}
// 加载数据
const loadData = () => {
loading.value = true
// 模拟API请求延迟
setTimeout(() => {
tableData.value = generateMockData(currentPage.value, pageSize.value)
loading.value = false
}, 500)
}
// 组件挂载时加载数据
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.detaildata-container {
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.text-red {
color: #f56c6c;
}
.text-green {
color: #67c23a;
}
.text-blue {
color: #1890ff;
}
.el-button--text {
padding: 0;
height: auto;
font-size: 14px;
}
// 响应式布局
@media screen and (max-width: 1200px) {
.detaildata-container {
padding: 12px;
}
.el-table {
font-size: 13px;
}
.el-table-column {
&:not(:first-child):not(:last-child) {
width: 90px !important;
}
}
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="duibifenxi-bar-container">
<div ref="chartRef" class="chart" style="width: 100%; height: 300px;"></div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
// 定义组件props
interface CompareData {
dates: string[]
currentPeriodData: number[]
lastYearData: number[]
}
const props = defineProps<{
compareData?: CompareData
}>()
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
// 默认数据
const defaultCompareData: CompareData = {
dates: ['1号', '2号', '3号', '4号', '5号', '6号', '7号'],
currentPeriodData: [90, 80, 75, 89, 60, 76, 73],
lastYearData: [60, 53, 65, 76, 69, 52, 65]
}
// 初始化图表
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const data = props.compareData || defaultCompareData
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function(params: any) {
const current = params[0]
const lastYear = params[1]
let result = `${current.name}<br/>`
result += `${current.marker}${current.seriesName}: ${current.value}Kwh<br/>`
result += `${lastYear.marker}${lastYear.seriesName}: ${lastYear.value}Kwh`
return result
}
},
legend: {
data: ['当前周期', '去年同期'],
textStyle: {
color: '#333'
},
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.dates,
axisLine: {
lineStyle: {
color: '#d9d9d9'
}
},
axisLabel: {
color: '#666'
}
},
yAxis: {
type: 'value',
name: 'kwh',
nameTextStyle: {
color: '#666',
padding: [0, 0, 0, 40]
},
axisLine: {
show: false
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
},
series: [
{
name: '当前周期',
type: 'bar',
data: data.currentPeriodData,
itemStyle: {
color: '#1890ff'
},
barWidth: '30%',
emphasis: {
focus: 'series'
}
},
{
name: '去年同期',
type: 'bar',
data: data.lastYearData,
itemStyle: {
color: '#52c41a'
},
barWidth: '30%',
emphasis: {
focus: 'series'
}
}
]
}
chartInstance.setOption(option)
}
// 响应式处理
const handleResize = () => {
chartInstance?.resize()
}
// 组件挂载
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
// 组件卸载
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
})
</script>
<style scoped lang="scss">
.duibifenxi-bar-container {
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
display: flex;
flex-direction: column;
}
.chart {
flex: 1;
min-height: 0;
}
// 响应式调整
@media screen and (max-width: 768px) {
.duibifenxi-bar-container {
padding: 12px;
}
.chart {
height: 250px;
}
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div class="tongbifenxi-line-container">
<div id="tongbifenxiLineChart" class="chart-container"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import * as echarts from 'echarts';
const chartInstance = ref<echarts.ECharts | null>(null);
const initChart = () => {
const chartDom = document.getElementById('tongbifenxiLineChart');
if (!chartDom) return;
chartInstance.value = echarts.init(chartDom);
// 写死的数据
const dates = ['1号', '2号', '3号', '4号', '5号', '6号', '7号'];
const growthRates = ['1.50', '1.20', '0.50', '0.80', '0.90', '0.30', '-2.00'];
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderColor: '#409eff',
textStyle: {
color: '#fff'
},
formatter: (params: any) => {
const data = params[0];
return `${data.name}:\n环比增长率: ${data.value}%`;
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: dates,
axisTick: {
alignWithLabel: true
},
axisLine: {
lineStyle: {
color: '#d9d9d9'
}
},
axisLabel: {
color: '#666'
}
}
],
yAxis: [
{
type: 'value',
min: -2,
max: 2,
axisLabel: {
color: '#666',
formatter: '{value}%'
},
axisLine: {
show: true,
lineStyle: {
color: '#d9d9d9'
}
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
}
],
series: [
{
name: '环比增长率',
type: 'line',
stack: 'Total',
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(103, 194, 58, 0.3)'
},
{
offset: 1,
color: 'rgba(103, 194, 58, 0.05)'
}
])
},
emphasis: {
focus: 'series'
},
lineStyle: {
color: '#67c23a',
width: 3
},
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#67c23a',
borderColor: '#fff',
borderWidth: 2
},
data: growthRates,
smooth: true
}
]
};
chartInstance.value.setOption(option);
};
const handleResize = () => {
chartInstance.value?.resize();
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance.value?.dispose();
});
</script>
<style scoped>
.tongbifenxi-line-container {
width: 100%;
height: 100%;
min-height: 300px;
padding: 10px;
box-sizing: border-box;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.chart-container {
width: 100%;
height: 100%;
min-height: 280px;
}
@media (max-width: 768px) {
.tongbifenxi-line-container {
padding: 5px;
min-height: 250px;
}
.chart-container {
min-height: 230px;
}
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<div>
<DuibifenxiBar></DuibifenxiBar>
<tongbifenxiLine></tongbifenxiLine>
<detaildata></detaildata>
</div>
</template>
<script setup>
import detaildata from '@/views/shengchanManage/powerfenxi/components/detaildata.vue'
import tongbifenxiLine from './components/tongbifenxiLine.vue';
import DuibifenxiBar from './components/duibifenxiBar.vue';
</script>