充电桩项目

686 阅读9分钟

公司的项目笔记,持续更新中

公共部分

业务整理

账号体系:

  1. 平台方添加员工账号:平台添加账号信息之后,为账号添加角色、分配权限,然后选择部门、选择岗位。
  2. 平台方添加运营商账号:添加运营商(管理员)账号之后,然后为账号分配权限。
  3. 运营商添加子账号:新增账号信息之后,为账号添加角色、分配权限,然后选择部门、选择岗位。

业务流程:

  • 站点管理:新增站点时,选择站点类型(二轮车、四轮车) → 选择运营模式(平台自营模式、平台服务模式、多方合作分润模式)→ 根据二轮车、四轮车分别设置计费规则。
  • 设备管理:新增设备时,选择设备类型 → 选择站点 → 下发计费规则。
  • 用户/商户提现流程:客户端发起提现 → 扣除余额,提现金额冻结,生成提现记录(待审核) → 平台审核如果通过,提现微信钱包或者商务账户。如果提现未通过或者失败,提现金额解冻,同时返还余额。
  • 用户开发票流程:平台或者商户给用户开发票是手动开的。用户完善开发票的信息之后提交开发票,首先会系统会判断场站运营商开发票的运营主体,如果是运营商,就由运营端开发票。如果是平台方,就由平台方开发票。

UI组件库

UI组件库的官网地址:wot-design-uni.netlify.app

web端

组件通信

父传子通信

父组件:

ParentComponent.vue

<template>
    <ChildComponent :message="parentMessage"/>
</template>

<script lang="ts" setup>
import ChildComponent from './ChildComponent.vue'
    
const parentMessage = ref('Hello from parent')
</script>

子组件:

ChildComponent.vue

在使用 TypeScript 的情况下,可以利用类型注解来增强类型安全,这里是使用 Composition API 的例子

<template>
{{message}}
</template>

<script lang="ts" setup>
import { defineProps } from 'vue'
    
const props = defineProps<{
    message: string;
}>()
// 在JS或TS中,可以直接使用props.message
console.log(props.message) // 这将输出从父组件传递过来的字符串
</script>

子传父通信

使用$emit的方式触发事件

子组件可以通过$emit方法触发事件,并且将需要更新的信息传递会父组件。父组件需要在子组件标签上监听这些事件,并且在事件处理器中更新自己的状态。

子组件:

在子组件中定义 emits 选项并使用 $emit

<template>
    <button @click="updateParent('main')">update Parent status</button>

</template>

<script lang="ts" setup>
import { defineEmits } from 'vue';

const emits = defineEmits(['update-counter']);
    
function updateParent(newValue){
    emits('update-counter',newValue)
}
</script>

父组件:

父组件监听这个事件并更新状态

<template>
    <ChildComponent :counter="counter" @update-counter="handleCounterUpdate"/>
</template>

<script lang="ts" setup>
    const counter = ref(0)
    
    function handleCounterUpdate(newCounterValue){
        counter.value = newCounterValue
    }
</script>

其他

如果状态保存在子组件,父组件点击按钮,就要修改子组件的状态

思路:

  1. 子组件暴露一个方法:子组件可以暴露一个方法,允许外部调用这个方法来修改它的状态。
  2. 父组件调用子组件的方法:父组件通过引用子组件实例来调用这个方法。

子组件:

<template>
  <Dialog 
   v-model="dialogVisible"
   :title="dialogTitle"
   @open="openFn">

    <el-form
      ref="formRef"
      :model="formData"
      label-width="120px"
    >

      <el-form-item label="字典名称" prop="name">
        <el-input v-model="formData.name" placeholder="请输入" />
      </el-form-item>

    </el-form>

    
  </Dialog>

</template>

<script lang="ts" setup>
    const dialogVisible = ref(false)
    
    const toggleDialog = ()=>{
      dialogVisible.value = !dialogVisible.value
    }
    // 暴露方法
    defineExpose({ toggleDialog });
</script>

父组件:

<template>
    <el-button type="primary" @click="addFn">新增</el-button>

    <ModifyDialog ref="dialogRef" :dialogType="dialogType"/>
</template>

<script lang="ts" setup>
    import ModifyDialog from './ModifyDialog.vue'
    const dialogRef = ref()
    
    const addFn = ()=>{
      dialogRef.value.toggleDialog()
    }
</script>

路由跳转与参数接收

路由跳转

import { useRouter } from 'vue-router'

const router = useRouter()

// 导航到"/foo"
router.push('/foo')

// 也可以使用对象的方式来指定完整路径
router.push({ path: '/foo', query: { message: 'hello' } })

// 如果你的路由是带有参数的(即有动态段),可以这样:
router.push({ name: 'user', params: { userId: 123 } })

参数接收

import { useRoute } from 'vue-router';

export default {
  setup() {
    const route = useRoute();
    const userId = route.params.id; // 获取路由参数

    return { userId };
  }
};

图片上传(后端)

element ui的icon全局引入

  1. 安装
# 选择一个你喜欢的包管理器

# NPM
npm install @element-plus/icons-vue
# Yarn
yarn add @element-plus/icons-vue
# pnpm
pnpm install @element-plus/icons-vue
  1. 注册到入口文件(main.ts)
// main.ts

// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

图片上传业务代码

<template>
    <el-upload
        action="#"
        v-model:file-list="addRichForm2.imageList"
        list-type="picture-card"
        :before-upload="beforeAvatarUpload"
        :on-remove="handleRemove"
        :http-request="uploadFunction"
        >
        <el-icon><Plus /></el-icon>

    </el-upload>

</template>

<script lang="ts" setup>
    import { ElMessage } from 'element-plus'
    import type { UploadProps, FormInstance, FormRules } from 'element-plus'
    import { uploadImageAPI } from '@/api/baseData/richText'
    
    const addRichForm2 = ref({
        imageList:[]
    })
    
    // 图片上传之前,对文件大小、类型的校验
    const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
      const whiteList = ['image/png','image/jpeg']
      if(!whiteList.includes(rawFile.type)){
        ElMessage.error('只支持图片类型文件')
      }
      const maxSize = 1 * 1024 * 1024
      if (rawFile.size > maxSize) {
        ElMessage.error('文件大小不能超过1M')
      }
      return whiteList.includes(rawFile.type) && rawFile.size < maxSize
    }
    
    // 删除图片的事件触发
    const handleRemove = ()=>{
        console.log(addRichForm2.value.imageList);
    }
    
    // 上传图片
    const uploadFunction = async(option)=>{
        try {
            const res = await uploadImageAPI({ file: option.file })
            addForm.value.imageList.pop()
            addForm.value.imageList.push({
                name: res.data.fileName,
                url: res.data
            })
        } catch (error) {
            console.error(error);
        }
    }
</script>

富文本编辑器

points:

  1. 安装并且引入插件tinymce
  2. 图片上传的处理(文件类型转换)
  3. ts报错处理
  4. 添加域名,解除外网访问tinymac插件的限制

使用的插件是tinymce

安装依赖

npm install --save "@tinymce/tinymce-vue@^5"

代码

<template>
    <Editor
      :api-key="apiKey"
      :init="initConfig"
       v-model="editeContent"
     />
</template>

<script lang="ts" setup>
    import Editor from '@tinymce/tinymce-vue'
    
    const apiKey = ref('lg9kxl5afdfwpfcrogrv5ewn9vdhm6q4vwhu6d1qyiebrhn5')
    const initConfig = ref({
      menubar: false, // 不显示任何菜单栏
      language: 'zh_CN',
      plugins: 'anchor autolink charmap codesample emoticons image link lists media searchreplace table visualblocks wordcount checklist mediaembed casechange export formatpainter pageembed linkchecker a11ychecker tinymcespellchecker permanentpen powerpaste advtable advcode editimage advtemplate mentions tableofcontents footnotes mergetags autocorrect typography inlinecss markdown fullscreen',
      toolbar: [
        'blocks blockquote | bold underline italic forecolor backcolor | fontsize fontfamily lineheight | bullist numlist checklist | alignleft',
        'alignright | emoticons link image | table codesample | undo redo | fullscreen '
      ],
      line_height_formats: '1 1.2 1.4 1.6 2',
      /*
       * 以下代码是和图片上传相关的逻辑
      */
      images_file_types: 'jpeg,png,jpg,svg,webp',
      file_picker_types: 'image',
      automatic_uploads: true,
      // 用于自定义处理文件选择和上传的逻辑
      file_picker_callback: (cb) => {
        const input = document.createElement('input');
        input.setAttribute('type', 'file');
        input.setAttribute('accept', 'image/*');

        input.addEventListener('change', (e) => {
          const file = e.target?.files[0];

          const reader = new FileReader();
          reader.addEventListener('load', () => {
            /*
              Note: Now we need to register the blob in TinyMCEs image blob
              registry. In the next release this part hopefully won't be
              necessary, as we are looking to handle it internally.
            */
            const id = 'blobid' + (new Date()).getTime();
            const blobCache =  tinymce.activeEditor.editorUpload.blobCache;
            const base64 = (reader.result as string)?.split(',')[1];
            const blobInfo = blobCache.create(id, file, base64);
            blobCache.add(blobInfo);

            /* call the callback and populate the Title field with the file name */
            cb(blobInfo.blobUri(), { title: file.name });
          });
          reader.readAsDataURL(file);
        });

        input.click();
      }
    })
    const editeContent = ref('')
    
    const submitFn = ()=>{
      console.log(editeContent.value); 
    }
</script>

ts报错处理

\ 由于tinymce变量是被npm挂载到window下面的,所以ts找不到。

解决方法:

在项目的 src 目录下创建一个 typings.d.ts 文件:

// global.d.ts 或 typings.d.ts
declare var tinymce: any; // 或者更具体的类型定义

图片上传改为oss上传

上面的上传图片配置是将图片转为base64格式,存储到数据库。如果图片很大,回显的时候就会读取的很慢。

import { getAccessToken } from '@/utils/auth'

const imagesUploadHandler = (blobInfo,progress) => new Promise((resolve, reject)=>{
  const xhr = new XMLHttpRequest();
  xhr.withCredentials = false;
  xhr.open('POST', 'http://58.251.5.22:32757/admin-api/system/file/files-anon');
       const token = getAccessToken();
       if (token) {
         xhr.setRequestHeader('Authorization', `Bearer ${token}`);
       }

  xhr.upload.onprogress = (e) => {
    progress(e.loaded / e.total * 100);
  };

  xhr.onload = () => {
    if (xhr.status === 403) {
      reject({ message: 'HTTP Error: ' + xhr.status, remove: true });
      return;
    }

    if (xhr.status < 200 || xhr.status >= 300) {
      reject('HTTP Error: ' + xhr.status);
      return;
    }

    const json = JSON.parse(xhr.responseText);
    

    if (!json || typeof json.data.fileUrl != 'string') {
      reject('Invalid JSON: ' + xhr.responseText);
      return;
    }

    resolve(json.data.fileUrl);
  };

  xhr.onerror = () => {
    reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
  };

  const formData = new FormData();
  formData.append('file', blobInfo.blob(), blobInfo.filename());

  xhr.send(formData);
})

const initConfig = ref({
  menubar: false,
  language: 'zh_CN',
  plugins: 'anchor autolink hr charmap codesample emoticons image link lists media searchreplace table visualblocks wordcount linkchecker fullscreen',
  toolbar: [
    'blocks blockquote | bold underline italic forecolor backcolor | fontsize fontfamily lineheight | bullist numlist checklist | aligncenter alignleft',
    'alignright | emoticons link image | table codesample hr | undo redo | fullscreen '
  ],
  line_height_formats: '1 1.2 1.4 1.6 2',
  images_file_types: 'jpeg,png,jpg,svg,webp',
  file_picker_types: 'file image',
  images_upload_url: 'http://58.251.5.22:32757/admin-api/system/file/files-anon',
  automatic_uploads: true,
  images_reuse_filename: true,
  images_upload_handler: imagesUploadHandler
})

添加域名

添加外部网站的域名:www.tiny.cloud/my-account/…

css选择器,选择除了最后一个类

.searchWrapper {
  width: 690rpx;
  background: #ffffff;
  border-radius: 20rpx;
  margin-top: 30rpx;
  margin-left: 30rpx;
  .searchItem {
    padding: 30rpx 30rpx 40rpx 30rpx;
    .top {
      display: flex;
      justify-content: space-between;
      align-items: center;
      .name {
      }
    }
  }
  :not(:last-child).searchItem {
    border-bottom: 1rpx solid #f3f3f3;
  }
}

绘制右边有尖脚的“流程图”

效果图:

代码:

<span>故障流程:</span>
<div class="step-container">
    <div class="step">用户申报</div>
    <div class="step">申报派发</div>
    <div class="step">故障处理</div>
</div>

.step-container {
    display: flex;
    width: 400px;
}

.step {
    flex: 1;
    padding: 5px 20px;
    text-align: center;
    color: white;
    font-weight: bold;
    margin: 0;
    position: relative;
}

.step:first-child {
    clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 50%, calc(100% - 20px) 100%, 0 100%);
    background-color: #ffa566;
}

.step:nth-child(2) {
    clip-path: polygon(0px 0px, calc(100% - 20px) 0, 100% 50%, calc(100% - 20px) 100%, 0px 100%);
    background-color: #c4ce4d;
}

.step:last-child {
    clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 50%, calc(100% - 20px) 100%, 0 100%);
    background-color: #35a955;
}

添加静态路由“/home”

{
    path: '/',
    component: Layout,
    redirect: '/index',
    name: 'Home',
    meta: {},
    children: [
      {
        path: 'index',
        component: () => import('@/views/mall/home/index.vue'),
        name: 'Index',
        meta: {
          title: t('router.home'),
          icon: 'ep:home-filled',
          noCache: false,
          affix: true
        }
      }
    ]
  },

全局注册所有组件

方法:使用import.meta.globEager进行批量注册

此方法适用于希望一次性加载所有组件的场景,它会立即解析所有的模块,适合开发环境或组件数量不多的情况。首先,在src/components文件夹下创建一个入口文件index.js,用于获取并注册所有.vue文件作为全局组件。

src\components\index.ts

import type { App } from 'vue'
import { Icon } from './Icon'

// src/components/index.ts
export function globalComponents(app) {
  const components = import.meta.globEager('@/components/**/*.vue'); // 获取components文件夹及其嵌套子文件夹中的所有.vue文件
  for (const [key, value] of Object.entries(components)) {
    try {
      const nameParts = key.replace(/(\.\/|\/index\.vue)/g, '').split('/');
      const componentName = nameParts.pop();
      if (componentName) {
        const formattedName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
        app.component(formattedName || value.default.__name, value.default); // 注册组件
      } else {
        console.warn(`无法解析组件名称: ${key}`);
      }
    } catch (error) {
      console.error(`注册组件失败: ${key}`, error);
    }
  }
}

src\main.ts

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { globalComponents } from '@/components/index';

const app = createApp(App);
globalComponents(app); // 执行批量注册
app.mount('#app');

小程序端

获取当前用户的定位,同时计算距离

获取当前用户的定位,同时计算给定经纬度之间的距离

进入manifest.json:

微信小程序配置 → “AppID” + “微信小程序权限配置”

调用<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">uni.getLocation</font>

src/pages-sub/twoWheelerDetail/twoWheelerDetail.vue

onLoad(()=>{
    toGetLocationFn()
})
// 获取用户当前的经纬度
const toGetLocationFn = () => {
  uni.getLocation({
    type: 'gcj02', // 使用国测局坐标系
    isHighAccuracy: true,
    success(res) {
      const latitude = res.latitude
      const longitude = res.longitude
      currentLat.value = latitude // 维度
      currentLong.value = longitude // 精度
      // 在这里你可以继续处理下一步
      const distance = calculateDistance(
        currentLat.value,
        currentLong.value,
        pointDetail.value.latitude,
        pointDetail.value.longitude,
      )
      console.log('距离', distance)
      pointDistance.value = distance
    },
    fail(error) {
      console.error('获取位置失败:', error)
      currentLat.value = '113.82410888671875'
      currentLong.value = '22.732921820746526'
      const distance = calculateDistance(
        currentLat.value,
        currentLong.value,
        pointDetail.value.latitude,
        pointDetail.value.longitude,
      )
      pointDistance.value = distance.toFixed(2)
    },
  })
}
// 计算用户离充电桩的距离,单位是千米
function calculateDistance(lat1, lon1, lat2, lon2) {
  // 地球平均半径,单位为公里
  const R = 6371.0
  // 将角度转换为弧度
  const radLat1 = (lat1 * Math.PI) / 180
  const radLon1 = (lon1 * Math.PI) / 180
  const radLat2 = (lat2 * Math.PI) / 180
  const radLon2 = (lon2 * Math.PI) / 180

  // 计算经纬度的差值
  const dLat = radLat2 - radLat1
  const dLon = radLon2 - radLon1

  // Haversine 公式
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))

  // 计算结果距离
  const distance = R * c
  return distance
}

点击触发导航功能

效果图:

代码:

<template>
	<div class="navigation" @tap="triggerNavigate">
      <image
        src="https://educt-files.oss-cn-shenzhen.aliyuncs.com/e2b24740-80b6-4adc-8727-fb6931c0206d.png"
        mode="widthFix"
      ></image>
      <span>导航</span>
    </div>
</template>

<script lang="ts" setup>
const triggerNavigate = () => {
  uni.authorize({
    scope: 'scope.userLocation',
    success: function (_res) {
      // 用户已授权,可以继续使用uni.openLocation
      uni.openLocation({
        latitude: 39.905024,
        longitude: 116.393823,
        name: '人民大会堂',
        address: '北京市东城区西长安街',
      })
    },
    fail: function (err) {
      // 用户拒绝授权,处理拒绝授权的情况
      console.log('用户拒绝授权', err)
    },
  })
}
</script>

注意:

  1. 正确设置经纬度,否则空白页面
  2. 权限问题:
    1. 授权位置权限,同时设置添加位置权限描述
    2. 当需要使用地理位置功能时,一定要先请求用户授权,这里使用 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">uni.authorize</font> 方法来请求授权

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">manifest.json</font>中:

在微信开发者工具编译之后, <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">app.json</font> 是这样的:

拨打电话

// 假设这是在一个页面的Page函数中的定义
callPhone(phoneNumber) {
    // 检查电话号码是否为空或未定义
    if (!phoneNumber) {
      uni.showToast({
        title: '电话号码不能为空',
        icon: 'none'
      });
      return;
    }
    
    // 使用uni.navigateTo进行拨号
    uni.makePhoneCall({
      phoneNumber: phoneNumber, // 电话号码
      success: function () {
        console.log('成功拨打:' + phoneNumber);
      },
      fail: function (err) {
        console.error('拨号失败:', err);
        uni.showToast({
          title: '无法拨打该电话',
          icon: 'none'
        });
      }
    });
  }

富文本在uniapp中渲染

原理:首先,你需要准备一段富文本数据。这段数据可以是一个HTML字符串,也可以是一个包含了富文本节点的JSON对象。rich-text组件接受两种类型的输入:一种是直接插入HTML字符串,另一种是插入由节点组成的数组。

插入HTML字符串

<template>
  <view>
    <rich-text :nodes="htmlString"></rich-text>
  </view>
</template>

const htmlString = '<p>Hello, world!</p><strong>This is strong text.</strong>'

插入节点数组

<template>
  <view>
    <rich-text :nodes="richTextNodes"></rich-text>
  </view>
</template>

const richTextNodes = ref([
    { type: 'p', text: 'Hello, world!' },
    { type: 'strong', text: 'This is strong text.' }
])

在这个例子中,我们创建了一个节点数组,每个节点都描述了文本的一部分以及它的类型(如段落p或加粗strong)。

uniapp跳转页面传参

const params = {
  id: 123,
  name: '张三'
};
uni.navigateTo({
  url: `/path/to/B?${Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')}`
});

扫码识别二维码读取文本

出现的报错:

图片暂时丢失了

出现这种报错的原因是没有添加权限。

第一步,添加权限

manifest.json

"mp-weixin" : {
  "appid" : "wx94c681e8db170782",
  "setting" : {
    "urlCheck" : false
  },
  "usingComponents" : true,
  "permission" : {
    "scope.userLocation" : {
      "desc" : "用于获取用户当前的位置"
    },
    "scope.camera": {
      "desc": "我们需要访问您的相机来扫码"
    }
  }
}

登录微信小程序管理后台,来到“设置”。

第二步,上代码

src/pages-sub/userFeedback/userFeedback.vue

/* 扫码 */
const scanQD = () => {
  uni.authorize({
    scope: 'scope.camera',
    success() {
      // 用户同意授权
      scanQRCode()
    },
    fail(error) {
      console.log(error)
      errorMsg.value = JSON.stringify(error)
      // 用户拒绝授权
      uni.showToast({
        title: '您拒绝了授权',
        icon: 'none',
      })
    },
  })
}
function scanQRCode() {
  uni.scanCode({
    success(res) {
      if (res.result) {
        // 扫描成功,处理二维码内容
        console.log('扫描结果:', res.result)
        // 这里可以进行后续操作,如跳转页面、显示信息等
      } else {
        // 扫描失败
        uni.showToast({
          title: '扫描失败',
          icon: 'none',
        })
      }
    },
    fail() {
      // 调用相机失败
      uni.showToast({
        title: '调用相机失败',
        icon: 'none',
      })
    },
  })
}

小程序页面绘制折线图

引入lime-echart

效果图:

代码:

<view style="width: 750rpx; height: 950rpx"><l-echart ref="chartRef"></l-echart></view>

const echarts = require('../../uni_modules/lime-echart/static/echarts.min')

const chartRef = ref(null)
const option = {
    grid: {
    left: '5%',
    right: '5%', // 通过调整left和right使图表内容在容器内居中
    top: '17%',
    containLabel: true, // 确保标签不被裁剪
    height: '80%',
  },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      // 坐标轴指示器,坐标轴触发有效
      type: 'line', // 默认为直线,可选为:'none' | 'shadow' | 'line'
    },
  },
  xAxis: [
    {
      name: '时间',
      data: ['8:00', '9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00'],
      // boundaryGap: [0, 0],
      nameLocation: 'middle',
      nameGap: -50, // 增加这个值,使“时间”两个字离X轴更远(水平距离)
      nameTextStyle: {
        // 可选:自定义名称的样式
        fontSize: 14,
        color: '#333',
        padding: [20, 0, 0, 0], // 上、右、下、左的填充,这里只设置了上边距
      },
      axisLabel: {
        interval: 0,
        rotate: 0,
        color: '#4E4C56',
        fontStyle: 'normal', // 字体风格,默认为normal,可选'oblique'或'italic'
        fontWeight: 'normal', // 字体粗细,默认为normal,可选'bold'、'bolder'、'lighter'或具体的磅值
        fontFamily: 'Microsoft YaHei', // 字体系列,默认为sans-serif
        fontSize: 14, // 字体大小
      },
    },
  ],
  yAxis: [
    {
      name: '功率(KW)',
      nameTextStyle: {
        // 专门用于设置轴名称的文本样式
        color: '#4E4C56', // 将轴名称的字体颜色设置为 #07FEFF
        // 可以在这里添加更多文本样式配置,如fontSize, fontStyle等
      },
      axisLabel: {
        // 同样地,为y轴添加样式配置
        color: '#4E4C56',
        fontStyle: 'normal',
        fontWeight: 'normal',
        fontFamily: 'sans-serif',
        fontSize: 14,
      },
    },
  ],
  series: [
    {
      name: '功率(KW)',
      type: 'line',
      areaStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: '#c68b24' },
          { offset: 1, color: '#1a2c47' },
        ]),
      },
      data: [100, 150, 100, 150, 100, 250, 300, 200, 100],
      symbolSize: 8,
      itemStyle: {
        normal: {
          color: '#33ccff',
        },
      },
      lineStyle: {
        width: 2,
        type: 'solid',
      },
      markPoint: {
        label: {
          normal: {
            formatter: function (param) {
              return param != null ? Math.round(param.value) + '个' : ''
            },
          },
        },
      },
      markLine: {
        label: {
          normal: {
            formatter: function (param) {
              return param != null ? Math.round(param.value) + '个' : ''
            },
          },
        },
      },
    },
  ],
}

onMounted(() => {
  // 组件能被调用必须是组件的节点已经被渲染到页面上
  setTimeout(async () => {
    if (!chartRef.value) return
    const myChart = await chartRef.value.init(echarts)
    myChart.setOption(option)
  }, 300)
})