Skip to content

设备列表

背景图的样式

css
background-image: url('@/assets/drawer-bg.jpg'); /* 使用本地图片 */
background-size: cover; /* 背景填充整个容器 */
background-position: center; /* 居中显示 */
background-repeat: no-repeat; /* 不重复 */

::v-deep(.el-drawer__body)  {
    background-image: url(@/assets/nbBg.svg);
}

正则表达式

ts
^8986(11|06)\d{13}$|^898604\d{14}$
const regex = /^8986(11|06)\d{13}$|^898604\d{14}$/;

^8986:匹配必须以 8986 开头的字符串。

(11|06):匹配 8986 后面的部分,如果是 1106,则表示后面必须跟 13 位数字(总共 19 位)。

  • \d{13}:表示跟随 13 位数字。

|:表示或者的关系。

898604:匹配以 898604 开头的字符串,后面必须跟 14 位数字(总共 20 位)。

  • \d{14}:表示跟随 14 位数字。

$:表示字符串的结尾。

ts
const regex = /^8986(11|06)\d{13}$|^898604\d{14}$/;

console.log(regex.test("8986111234567890123"));  // true
console.log(regex.test("8986061234567890123"));  // true
console.log(regex.test("89860412345678901234")); // true
console.log(regex.test("8986121234567890123"));  // false

数据库删除表的数据

ts
databae Ctrl+y 然后点击提交 ctrl+enter

Echart Y轴拐点

ts
{
    name: '高温',
    type: 'line',
    symbol: 'circle',
    symbolSize: 8,
    emphasis: {
        focus: 'series',
    },
    itemStyle: {
        borderColor: 'rgba(255,255,255)',
        borderWidth: 2,
    },
    data: yAxisData.value[0].data,
    smooth: true,
    markPoint: {
        symbol: 'diamond', // 标记样式,比如菱形
        symbolSize: 12, // 标记大小
        label: {
            show: true,
            formatter: (params: any) => `断点`, // 标注文字
            color: '#fff',
        },
        itemStyle: {
            color: 'rgba(255, 0, 0, 0.8)', // 标注的颜色
        },
        data: xAxisDataInterrupt.value.map((xValue: any) => {
            // 找到 x 值对应的 y 值
            const xIndex = xAxisData.value.indexOf(xValue);
            if (xIndex !== -1) {
                return {
                    coord: [xValue, yAxisData.value[0].data[xIndex]], // 坐标点
                };
            }
            return null; // 如果未找到,忽略该点
        }).filter((point: any) => point !== null), // 过滤掉无效点
    },
}

xAxisData.value.indexOf(xValue)

  • 查找 xValuexAxisData.value 中的索引位置。
  • 确保找到断点在 x 轴上的位置。

yAxisData.value[0].data[xIndex]

  • 根据 xIndex 获取对应的 y 值,用于标记点的 coord

filter

  • 如果 xValue 不存在于 xAxisData.value,则忽略该点。

markPoint.data

  • 动态生成符合条件的标记点。

表格导出Excel

使用 npm 安装 xlsx 和文件下载工具 file-saver

ts
npm install xlsx file-saver
npm install --save-dev @types/file-saver

以下是一个使用 xlsx 导出表格数据到 Excel 的完整实现

vue
<template>
  <div>
    <el-table :data="tableData" border style="width: 100%">
      <el-table-column prop="name" label="姓名" width="120"></el-table-column>
      <el-table-column prop="age" label="年龄" width="80"></el-table-column>
      <el-table-column prop="address" label="地址"></el-table-column>
    </el-table>
    <el-button type="primary" @click="exportToExcel">导出到 Excel</el-button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import * as XLSX from "xlsx";
import { saveAs } from "file-saver";

export default defineComponent({
  name: "ExportTable",
  data() {
    return {
      tableData: [
        { name: "张三", age: 25, address: "北京市" },
        { name: "李四", age: 30, address: "上海市" },
        { name: "王五", age: 28, address: "广州市" },
      ],
    };
  },
  methods: {
    exportToExcel() {
      // 1. 将表格数据转换为工作表
      const worksheet = XLSX.utils.json_to_sheet(this.tableData);

      // 2. 创建工作簿并附加工作表
      const workbook = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(workbook, worksheet, "表格数据");

      // 3. 生成 Excel 文件并触发下载
      const excelBuffer = XLSX.write(workbook, {
        bookType: "xlsx",
        type: "array",
      });

      // 4. 使用 file-saver 保存文件
      const blob = new Blob([excelBuffer], {
        type: "application/octet-stream",
      });
      saveAs(blob, "表格数据.xlsx");
    },
  },
});
</script>

<style scoped>
.el-table {
  margin-bottom: 20px;
}
</style>

代码解析

1. 安装依赖

  • xlsx: 用于生成 Excel 文件。
  • file-saver: 用于触发文件下载。

2. 步骤说明

  1. 将 JSON 数据转换为工作表XLSX.utils.json_to_sheet(data)
  2. 创建工作簿并附加工作表:使用 XLSX.utils.book_new()XLSX.utils.book_append_sheet()
  3. 导出 Excel 文件:使用 XLSX.write() 生成文件,类型为 array
  4. 触发文件下载:将文件通过 Blob 格式保存并使用 saveAs() 下载。

优化建议

  1. 支持字段映射: 如果你需要将字段名映射为更友好的表头名称,可以自定义字段映射。例如
ts
const fields = {
  name: "姓名",
  age: "年龄",
  address: "地址",
};
const mappedData = tableData.map(item => {
  const mappedItem: any = {};
  for (const key in fields) {
    mappedItem[fields[key]] = item[key];
  }
  return mappedItem;
});
const worksheet = XLSX.utils.json_to_sheet(mappedData);

动态文件名

ts复制编辑const filename = `表格数据_${new Date().toLocaleDateString()}.xlsx`;
saveAs(blob, filename);

样式支持: 如果需要更丰富的样式,可以结合更高级的库,如 exceljs

父组件使用子组件中的数据

父组件

vue
<el-button color="#626aef" :dark="false" plain style="padding: 0 10px;" @click="handleChangeScene" v-show="sceneNumber === 1"
    class="btn-pue">
    <ele-Promotion style="width: 1em; height: 1em; margin-right: 2px;"></ele-Promotion>
    <span>PUE查询</span>
</el-button>
<el-button color="#626aef" :dark="false" plain style="padding: 0 10px;" @click="handleChangeSceneBack" v-show="sceneNumber === 2"
    class="btn-pue">
    <ele-Promotion style="width: 1em; height: 1em; margin-right: 2px;"></ele-Promotion>
    <span>返回</span>
</el-button>

<TablePower v-if="idNumber === '3'" :tableDataPower="tableDataPower" :tableLoading="tableLoading" ref="tableDataPowerRef"></TablePower>

const tableDataPowerRef = ref()

const sceneNumber = computed(()=>{
    return tableDataPowerRef.value?.scene ?? 1; // 加上 ?. 和默认值处理
})

子组件

vue
<div class="table" v-show="scene === 1">
<div class="pue" v-show="scene === 2">
import { onMounted, ref, computed, markRaw, nextTick } from 'vue'
const scene = ref(2)
defineExpose({scene})

导出Excel工具类

准备所有数据

确保后端或接口支持返回完整的表格数据。分页通常只返回当前页的数据,所以需要额外的接口或参数来获取全部数据。

ts
async function fetchAllData() {
  try {
    const response = await api.get('/your-api-endpoint', {
      params: {
        page: 1, // 默认请求第一页
        pageSize: totalCount, // 请求所有数据
      },
    });
    return response.data; // 返回所有数据
  } catch (error) {
    console.error('获取所有数据失败:', error);
  }
}

添加导出功能

在组件中添加一个按钮,点击后获取所有数据并触发导出逻辑。

ts
<template>
  <el-button type="primary" @click="exportAllData">导出全部数据</el-button>
  <el-table :data="tableDataWind" style="width: 100%" max-height="680" highlight-current-row>
    <el-table-column prop="sn" label="设备编号" width="240"></el-table-column>
    <el-table-column prop="winds" label="风速(Hz)" width="200"></el-table-column>
    <el-table-column prop="etemp" label="环境温度(℃)" width="200"></el-table-column>
    <el-table-column prop="sigQlty" width="200" label="信号强度(dBm)" />
    <el-table-column prop="time" label="采集时间" align="center" />
  </el-table>
</template>

<script setup>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import { exportToExcel } from './utils'; // 假设这里有一个工具函数用于导出 Excel

const tableDataWind = ref([]);
const totalCount = 1000; // 假设总数据条目数

const exportAllData = async () => {
  try {
    // 获取所有数据
    const allData = await fetchAllData();

    // 检查是否获取成功
    if (!allData || allData.length === 0) {
      ElMessage.error('未能获取到数据');
      return;
    }

    // 调用工具函数导出为 Excel
    exportToExcel({
      data: allData,
      filename: '表格导出.xlsx',
      columns: [
        { header: '设备编号', key: 'sn' },
        { header: '风速(Hz)', key: 'winds' },
        { header: '环境温度(℃)', key: 'etemp' },
        { header: '信号强度(dBm)', key: 'sigQlty' },
        { header: '采集时间', key: 'time' },
      ],
    });

    ElMessage.success('数据导出成功');
  } catch (error) {
    console.error('导出数据失败:', error);
    ElMessage.error('导出失败');
  }
};
</script>

创建导出工具函数

ts
npm install xlsx file-saver

工具函数(utils.js

ts
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';

export function exportToExcel({ data, filename, columns }) {
  const sheetData = data.map((row) => {
    const sheetRow = {};
    columns.forEach((col) => {
      sheetRow[col.header] = row[col.key];
    });
    return sheetRow;
  });

  const worksheet = XLSX.utils.json_to_sheet(sheetData);
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
  const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });

  const blob = new Blob([excelBuffer], {
    type: 'application/octet-stream',
  });
  saveAs(blob, filename);
}

附加优化

  1. 加载状态: 添加加载动画或按钮禁用逻辑,避免用户多次点击按钮:

    ts
    <el-button :loading="loading" @click="exportAllData">导出全部数据</el-button>
  2. 分批加载数据: 如果数据量特别大,可以在前端使用分页多次请求并合并数据。

确保在点击导出按钮时,能够成功获取所有数据并正确生成文件。

如果接口数据量较大,可以考虑后端直接生成文件并提供下载地址。

时间任务

使用 vue3-cron 之类的第三方库来实现 Cron 表达式选择

ts
npm install vue3-cron
ts
<template>
  <vue3-cron v-model="cronValue" />
</template>

<script setup>
import { ref } from "vue";
import Vue3Cron from "vue3-cron";

const cronValue = ref("* * * * *");
</script>

使用 Element Plus 现有组件自定义 Cron 选择器 可以组合 el-selectel-time-pickerel-date-picker 来手动创建一个 Cron 表达式选择器。

时间字符串转时区

使用 toLocaleString()

ts
const dateStr = "2025-02-07T13:00:00.000+00:00";
const date = new Date(dateStr);
const beijingTime = date.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });

console.log(beijingTime); // "2025/2/7 21:00:00"

toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }) 会自动转换到东八区时间。

如果你用 moment-timezone

ts
import moment from "moment-timezone";

const dateStr = "2025-02-07T13:00:00.000+00:00";
const beijingTime = moment.utc(dateStr).tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss");

console.log(beijingTime); // "2025-02-07 21:00:00"

使用dayjs

ts
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";

dayjs.extend(utc);
dayjs.extend(timezone);

const dateStr = "2025-02-07T13:00:00.000+00:00";
const beijingTime = dayjs.utc(dateStr).tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss");

console.log(beijingTime); // "2025-02-07 21:00:00"

svg全局图标

/src/components/SvgIcon/index.vue

vue
<template>
	<i v-if="isShowIconSvg" class="el-icon" :style="setIconSvgStyle">
		<component :is="getIconName" />
	</i>
	<div v-else-if="isShowLocalSvg" :style="setIconImgOutStyle">
		<component :is="getIconName" :style="setIconSvgStyle" />
	</div>
	<div v-else-if="isShowIconImg" :style="setIconImgOutStyle">
		<img :src="getIconName" :style="setIconSvgInsStyle" />
	</div>
	<i v-else :class="getIconName" :style="setIconSvgStyle" />
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue';
// 本地svg
const customSvgIcons = import.meta.glob('@/assets/svg/*.svg', { eager: true });
export default defineComponent({
	name: 'svgIcon',
	props: {
		// svg 图标组件名字
		name: {
			type: String,
		},
		// svg 大小
		size: {
			type: Number,
			default: () => 14,
		},
		// svg 颜色
		color: {
			type: String,
		},
	},
	setup(props) {
		// 在线链接、本地引入地址前缀
		const linesString = ['https', 'http', '/src', '/assets'];

		// 获取 icon 图标名称
		const getIconName = computed(() => {
			if (props?.name?.startsWith('ele-')) {
				return props.name; // Element Plus 图标
			}
			if (customSvgIcons[`/src/assets/svg/${props.name}.svg`]) {
				return customSvgIcons[`/src/assets/svg/${props.name}.svg`].default;
			}
			return props.name; // 可能是 URL
		});
		// 用于判断 element plus 自带 svg 图标的显示、隐藏
		const isShowIconSvg = computed(() => {
			return props?.name?.startsWith('ele-');
		});
		// 判断是否为本地 SVG 组件
		const isShowLocalSvg = computed(() => {
			return !!customSvgIcons[`/src/assets/svg/${props.name}.svg`];
		});
		// 用于判断在线链接、本地引入等图标显示、隐藏
		const isShowIconImg = computed(() => {
			return linesString.find((str) => props.name?.startsWith(str));
		});
		// 设置图标样式
		const setIconSvgStyle = computed(() => {
			return `font-size: ${props.size}px;color: ${props.color};`;
		});
		// 设置图片样式
		const setIconImgOutStyle = computed(() => {
			return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
		});
		// 设置图片样式
		// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
		const setIconSvgInsStyle = computed(() => {
			const filterStyle: string[] = [];
			const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
			compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
			return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
		});
		return {
			getIconName,
			isShowLocalSvg,
			isShowIconSvg,
			isShowIconImg,
			setIconSvgStyle,
			setIconImgOutStyle,
			setIconSvgInsStyle,
		};
	},
});
</script>

/src/components/SvgIcon/ELSvg.ts

ts
import type { App} from 'vue'
import * as svg from '@element-plus/icons-vue';
import SvgIcon from '@/components/SvgIcon/index.vue'

export default function (app: App) {
  app.component('svg-icon', SvgIcon)

  // import.meta.glob('./svg/*.svg')
  // import.meta.globEager('./svg/*.svg')

  const icons = svg as any;
	for (const i in icons) {
		app.component(`ele-${icons[i].name}`, icons[i]);
	}
	app.component('SvgIcon', SvgIcon);
}

main.ts

ts
import { createApp } from 'vue'
import pinia from './stores'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import './styles/index.scss'
import ElSvg from './components/SvgIcon/ElSvg'
import './permission'
import VueEcharts from 'vue-echarts'
import VueAMap, { initAMapApiLoader } from '@vuemap/vue-amap';
import '@vuemap/vue-amap/dist/style.css';
import '@/utils/resizeObserver'
const app = createApp(App)
ElSvg(app)
app.component('v-chart', VueEcharts);
// 初始化vue-amap
initAMapApiLoader({
    // 高德的key
    key: '00266de077c5944c71bc80c4d339c1c9',
    securityJsCode: 'a8de0fa258d29c6d039fc65e2c158e99', // 新版key需要配合安全密钥使用
    //Loca:{
    //  version: '2.0.0'
    //} // 如果需要使用loca组件库,需要加载Loca
});
app.use(pinia)
    .use(router)
    .use(ElementPlus, {
        locale: zhCn,
    })
    .use(VueAMap)
    .mount('#app')

使用

ts
使用 Element Plus 图标
<SvgIcon name="ele-Edit" size="20" color="red" />
    
使用本地 SVG 组件
你的 @/assets/svg/ 目录下有 urgent.svg 文件:
<SvgIcon name="urgent" size="20" color="blue" />
    
使用 URL 方式
<SvgIcon name="https://example.com/icon.svg" size="20" />