Skip to content

vue3工作积累

vue3 模板

vue3 模板 1

js
<template>
  <section>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{ counter }}</h2>
    <button @click="increment">+1</button>
    <br />

    {{ name }} - {{ age }}
    <button @click="changeAge">+age</button>
    <br />

    <h2>{{ fullName }}</h2>
    <button @click="changeName">修改firstName</button>
  </section>
</template>

<script>
import { ref, toRefs, toRef, reactive, computed } from 'vue'

export default {
  props: {
    /* message: {
      type: String,
      default: '',
      required: true
    } */
  },
  setup() {
    // counter编程一个ref的可响应式的引用
    // counter = 100;
    let counter = ref(100)
    const firstName = ref('Kobe')
    const lastName = ref('Bryant')

    // 响应式数据
    const state = reactive({})
    const info = reactive({ name: 'why', age: 18 })
    // let { name, age } = toRefs(info)  // 对所有属性转换成响应式
    let { name } = info
    let age = toRef(info, 'age') // 对一个属性转换成响应式

    // 计算属性 - 数据需要响应式
    // const fullName = computed(() => firstName.value + ' ' + lastName.value)  // 只读,需要重写set方法
    const fullName = computed({
      get: () => firstName.value + ' ' + lastName.value,
      set(newValue) {
        const names = newValue.split(' ')
        firstName.value = names[0]
        lastName.value = names[1]
      }
    })

    // 局部函数
    const increment = () => {
      counter.value++
      console.log(counter.value)
    }
    const changeAge = () => {
      age.value++
      // info.age++
      console.log(age.value)
    }
    const changeName = () => {
      // firstName.value = "James"
      fullName.value = 'test ddd'
    }

    return {
      // reactive数据
      state,

      // 数据
      counter,
      name,
      age,
      fullName,

      // 方法
      increment,
      changeAge,
      changeName
    }
  }
}
</script>

<style lang="scss" scoped>
</style>
<template>
  <section>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{ counter }}</h2>
    <button @click="increment">+1</button>
    <br />

    {{ name }} - {{ age }}
    <button @click="changeAge">+age</button>
    <br />

    <h2>{{ fullName }}</h2>
    <button @click="changeName">修改firstName</button>
  </section>
</template>

<script>
import { ref, toRefs, toRef, reactive, computed } from 'vue'

export default {
  props: {
    /* message: {
      type: String,
      default: '',
      required: true
    } */
  },
  setup() {
    // counter编程一个ref的可响应式的引用
    // counter = 100;
    let counter = ref(100)
    const firstName = ref('Kobe')
    const lastName = ref('Bryant')

    // 响应式数据
    const state = reactive({})
    const info = reactive({ name: 'why', age: 18 })
    // let { name, age } = toRefs(info)  // 对所有属性转换成响应式
    let { name } = info
    let age = toRef(info, 'age') // 对一个属性转换成响应式

    // 计算属性 - 数据需要响应式
    // const fullName = computed(() => firstName.value + ' ' + lastName.value)  // 只读,需要重写set方法
    const fullName = computed({
      get: () => firstName.value + ' ' + lastName.value,
      set(newValue) {
        const names = newValue.split(' ')
        firstName.value = names[0]
        lastName.value = names[1]
      }
    })

    // 局部函数
    const increment = () => {
      counter.value++
      console.log(counter.value)
    }
    const changeAge = () => {
      age.value++
      // info.age++
      console.log(age.value)
    }
    const changeName = () => {
      // firstName.value = "James"
      fullName.value = 'test ddd'
    }

    return {
      // reactive数据
      state,

      // 数据
      counter,
      name,
      age,
      fullName,

      // 方法
      increment,
      changeAge,
      changeName
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

vue3 模板 2

js
<template>
  <section>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{ counter }}</h2>
    <button @click="increment">+1</button>
    <br />

    {{ name }} - {{ age }}
    <button @click="changeAge">+age</button>
    <br />

    <h2>{{ fullName }}</h2>
    <button @click="changeName">修改firstName</button>
  </section>
</template>

<script setup>
import { ref, toRefs, toRef, reactive, computed, defineProps } from 'vue'

const props = defineProps({
  /* message: {
    type: String,
    default: '',
    required: true
  } */
})

// counter编程一个ref的可响应式的引用
// counter = 100;
let counter = ref(100)
const firstName = ref('Kobe')
const lastName = ref('Bryant')

// 响应式数据
const state = reactive({})
const info = reactive({ name: 'why', age: 18 })
// let { name, age } = toRefs(info)  // 对所有属性转换成响应式
let { name } = info
let age = toRef(info, 'age') // 对一个属性转换成响应式

// 计算属性 - 数据需要响应式
// const fullName = computed(() => firstName.value + ' ' + lastName.value)  // 只读,需要重写set方法
const fullName = computed({
  get: () => firstName.value + ' ' + lastName.value,
  set(newValue) {
    const names = newValue.split(' ')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

// 局部函数
const increment = () => {
  counter.value++
  console.log(counter.value)
}
const changeAge = () => {
  age.value++
  // info.age++
  console.log(age.value)
}
const changeName = () => {
  // firstName.value = "James"
  fullName.value = 'test ddd'
}
</script>

<style lang="scss" scoped>
</style>
<template>
  <section>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{ counter }}</h2>
    <button @click="increment">+1</button>
    <br />

    {{ name }} - {{ age }}
    <button @click="changeAge">+age</button>
    <br />

    <h2>{{ fullName }}</h2>
    <button @click="changeName">修改firstName</button>
  </section>
</template>

<script setup>
import { ref, toRefs, toRef, reactive, computed, defineProps } from 'vue'

const props = defineProps({
  /* message: {
    type: String,
    default: '',
    required: true
  } */
})

// counter编程一个ref的可响应式的引用
// counter = 100;
let counter = ref(100)
const firstName = ref('Kobe')
const lastName = ref('Bryant')

// 响应式数据
const state = reactive({})
const info = reactive({ name: 'why', age: 18 })
// let { name, age } = toRefs(info)  // 对所有属性转换成响应式
let { name } = info
let age = toRef(info, 'age') // 对一个属性转换成响应式

// 计算属性 - 数据需要响应式
// const fullName = computed(() => firstName.value + ' ' + lastName.value)  // 只读,需要重写set方法
const fullName = computed({
  get: () => firstName.value + ' ' + lastName.value,
  set(newValue) {
    const names = newValue.split(' ')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

// 局部函数
const increment = () => {
  counter.value++
  console.log(counter.value)
}
const changeAge = () => {
  age.value++
  // info.age++
  console.log(age.value)
}
const changeName = () => {
  // firstName.value = "James"
  fullName.value = 'test ddd'
}
</script>

<style lang="scss" scoped>
</style>

vue3 定义全局事件总线

  • main.js
js
// eventbus.js
import mitt from "mitt";
const emitter = mitt();
export default emitter;

// 引入事件总线库
import emitter from "@/utils/eventbus";

app.config.globalProperties.$emitter = emitter; // 定义全局事件总线
// eventbus.js
import mitt from "mitt";
const emitter = mitt();
export default emitter;

// 引入事件总线库
import emitter from "@/utils/eventbus";

app.config.globalProperties.$emitter = emitter; // 定义全局事件总线

vue3 的 elementPlus 中 el-dropdown 传参

html
<el-dropdown
  @command="
    (command) => {
    handleCommand(command, slotProps.row)
    }
"
>
  <span class="el-dropdown-link">
    更多
    <el-icon class="el-icon--right">
      <arrow-down />
    </el-icon>
  </span>
  <template #dropdown>
    <el-dropdown-menu>
      <el-dropdown-item command="create">xxx</el-dropdown-item>
    </el-dropdown-menu>
  </template>
</el-dropdown>

<script setup>
  const handleCommand = (command, row) => {};
</script>
<el-dropdown
  @command="
    (command) => {
    handleCommand(command, slotProps.row)
    }
"
>
  <span class="el-dropdown-link">
    更多
    <el-icon class="el-icon--right">
      <arrow-down />
    </el-icon>
  </span>
  <template #dropdown>
    <el-dropdown-menu>
      <el-dropdown-item command="create">xxx</el-dropdown-item>
    </el-dropdown-menu>
  </template>
</el-dropdown>

<script setup>
  const handleCommand = (command, row) => {};
</script>

el-tree 中不能全选

css
:deep(.el-tree) {
  .el-icon.el-tree-node__expand-icon {
    &.is-leaf {
      display: none;
    }

    &:not(.is-leaf) {
      & + .el-checkbox {
        display: none;
      }
    }
  }
}
:deep(.el-tree) {
  .el-icon.el-tree-node__expand-icon {
    &.is-leaf {
      display: none;
    }

    &:not(.is-leaf) {
      & + .el-checkbox {
        display: none;
      }
    }
  }
}

vite 跨域配置

js
proxy: {
  '/proxyApi1': {
    target: loadEnv(mode, process.cwd()).VITE_APP_BASE_URL1,
    ws: true, // 是否启用websockets
    changeOrigin: true, // 运行跨域
    rewrite: (path) => path.replace(/^\/proxyApi1/, '') // 重写路径
  }
}
proxy: {
  '/proxyApi1': {
    target: loadEnv(mode, process.cwd()).VITE_APP_BASE_URL1,
    ws: true, // 是否启用websockets
    changeOrigin: true, // 运行跨域
    rewrite: (path) => path.replace(/^\/proxyApi1/, '') // 重写路径
  }
}

vue3 日期选择器截止昨天

html
<el-date-picker
  v-model="form.endTime"
  style="width: 238px"
  type="date"
  placeholder="请选择时间"
  :disabled-date="disabledDate"
  format="YYYY-MM-DD"
  value-format="YYYY-MM-DD"
></el-date-picker>

<script setup>
  const disabledDate = (time) => {
    return time.getTime() < Date.now() - 8.64e7;
  };
</script>
<el-date-picker
  v-model="form.endTime"
  style="width: 238px"
  type="date"
  placeholder="请选择时间"
  :disabled-date="disabledDate"
  format="YYYY-MM-DD"
  value-format="YYYY-MM-DD"
></el-date-picker>

<script setup>
  const disabledDate = (time) => {
    return time.getTime() < Date.now() - 8.64e7;
  };
</script>

el-tree 以及 checkbox 单选

js
// @check-change="handleSelectedTreeUser"
const handleSelectedTreeUser = (val, selected) => {
  if (val.type === 2 && selected) {
    refMyTree.value.setCheckedKeys([]);
    refMyTree.value.setChecked(val, true);
    selectedUsers.value = [val];
    // console.log(selectedUsers.value)
  }
};

// @change="getSingleUser(item)"
const getSingleUser = (val) => {
  console.log(val);

  selectedUsers.value = selectedUsers.value.includes(val) ? [val] : [];
};
// @check-change="handleSelectedTreeUser"
const handleSelectedTreeUser = (val, selected) => {
  if (val.type === 2 && selected) {
    refMyTree.value.setCheckedKeys([]);
    refMyTree.value.setChecked(val, true);
    selectedUsers.value = [val];
    // console.log(selectedUsers.value)
  }
};

// @change="getSingleUser(item)"
const getSingleUser = (val) => {
  console.log(val);

  selectedUsers.value = selectedUsers.value.includes(val) ? [val] : [];
};

form 表单中自定义校验

js
const validDataBank = (rule, value, callback) => {
  // const pattern = /[`~!-\·\.@#$^&*()=|{}':;',\\\[\]\.<>\/\+?~!@#¥……&*()——|{}【】';:""'。,、?\s]/g
  const pattern1 = /([\u4e00-\u9fa5])/g // 中文
  // const pattern2 = /[`~,.<>;':"\/\[\]\|{}()-=_+]/g // 特殊字符
  const pattern2 = /[`@&<>~\+\-=_,.\/\[\]\|{}()]/g // 特殊字符
  // const pattern2 = /[~,.<>;':"\/\[\]\|{}()-=_+]/g // 特殊字符
  if (value) {
    var newValue = value.replace(pattern1, '').replace(pattern2, '')
    if (value !== newValue) {
      callback(new Error('不可使用特殊字符'))
    }
    callback()
  }
}

{ validator: validDataBank, trigger: 'blur' }
const validDataBank = (rule, value, callback) => {
  // const pattern = /[`~!-\·\.@#$^&*()=|{}':;',\\\[\]\.<>\/\+?~!@#¥……&*()——|{}【】';:""'。,、?\s]/g
  const pattern1 = /([\u4e00-\u9fa5])/g // 中文
  // const pattern2 = /[`~,.<>;':"\/\[\]\|{}()-=_+]/g // 特殊字符
  const pattern2 = /[`@&<>~\+\-=_,.\/\[\]\|{}()]/g // 特殊字符
  // const pattern2 = /[~,.<>;':"\/\[\]\|{}()-=_+]/g // 特殊字符
  if (value) {
    var newValue = value.replace(pattern1, '').replace(pattern2, '')
    if (value !== newValue) {
      callback(new Error('不可使用特殊字符'))
    }
    callback()
  }
}

{ validator: validDataBank, trigger: 'blur' }

el-table 动态合并单元格

vue
<template>
  <el-table
    :data="tableData"
    :border="true"
    show-summary
    height="800"
    :summary-method="getSummaries"
    style="width: 100%"
    :span-method="arraySpanMethod"
    :cell-style="cellStyleMethod"
  >
    <el-table-column
      type="index"
      width="82"
      label="序号"
      fixed
      min-width="200"
    />
    ......
  </el-table>
</template>

<script setup>
import { ref, toRefs, toRef, reactive, computed, nextTick } from "vue";

let tableData = ref([]);
let tableColumn = ref([]);
let mergeDate = [];
let temData = {};
let spanNameArr = []; // 处理要合并的数据

const getSummaries = (param) => {
  const { columns, data } = param;
  let temData = data.filter((item) => item.date === "汇总");
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "汇总计算";
      return;
    }
    const values = temData.map((item) => Number(item[column.property]));
    if (!values.every((value) => isNaN(value))) {
      sums[index] = values.reduce((prev, curr) => {
        const value = Number(curr);
        if (!isNaN(value)) {
          let temValue1 = prev + curr;
          let temValue2 = temValue1.toString().match(/^\d+(?:\.\d{0,2})?/);
          return Number(temValue2);
        } else {
          return prev;
        }
      }, 0);
      sums[index];
    } else {
      sums[index] = "-,--";
    }
  });
  mergeDate = sums;
  return sums;
};

function arraySpanMethod({ row, column, rowIndex, columnIndex }) {
  if (row.date === "汇总") {
    if (columnIndex === 1) {
      return [0, 0];
    } else if (columnIndex === 2) {
      return [1, 1];
    }
  } else {
    if (columnIndex == 1) {
      const _row = spanNameArr[rowIndex];
      const _col = _row > 0 ? 1 : 0;
      return {
        // [0,0] 表示这一行不显示, [2,1]表示行的合并数
        rowspan: _row,
        colspan: _col,
      };
    }
  }
}

function cellStyleMethod({ row, column, rowIndex, columnIndex }) {
  if (row.date === "汇总" || row.name === "汇总计算") {
    if (columnIndex !== 0) {
      return {
        backgroundColor: "var(--el-table-row-hover-bg-color)",
        fontWeight: 500,
      };
    }
  }
}
</script>
<template>
  <el-table
    :data="tableData"
    :border="true"
    show-summary
    height="800"
    :summary-method="getSummaries"
    style="width: 100%"
    :span-method="arraySpanMethod"
    :cell-style="cellStyleMethod"
  >
    <el-table-column
      type="index"
      width="82"
      label="序号"
      fixed
      min-width="200"
    />
    ......
  </el-table>
</template>

<script setup>
import { ref, toRefs, toRef, reactive, computed, nextTick } from "vue";

let tableData = ref([]);
let tableColumn = ref([]);
let mergeDate = [];
let temData = {};
let spanNameArr = []; // 处理要合并的数据

const getSummaries = (param) => {
  const { columns, data } = param;
  let temData = data.filter((item) => item.date === "汇总");
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "汇总计算";
      return;
    }
    const values = temData.map((item) => Number(item[column.property]));
    if (!values.every((value) => isNaN(value))) {
      sums[index] = values.reduce((prev, curr) => {
        const value = Number(curr);
        if (!isNaN(value)) {
          let temValue1 = prev + curr;
          let temValue2 = temValue1.toString().match(/^\d+(?:\.\d{0,2})?/);
          return Number(temValue2);
        } else {
          return prev;
        }
      }, 0);
      sums[index];
    } else {
      sums[index] = "-,--";
    }
  });
  mergeDate = sums;
  return sums;
};

function arraySpanMethod({ row, column, rowIndex, columnIndex }) {
  if (row.date === "汇总") {
    if (columnIndex === 1) {
      return [0, 0];
    } else if (columnIndex === 2) {
      return [1, 1];
    }
  } else {
    if (columnIndex == 1) {
      const _row = spanNameArr[rowIndex];
      const _col = _row > 0 ? 1 : 0;
      return {
        // [0,0] 表示这一行不显示, [2,1]表示行的合并数
        rowspan: _row,
        colspan: _col,
      };
    }
  }
}

function cellStyleMethod({ row, column, rowIndex, columnIndex }) {
  if (row.date === "汇总" || row.name === "汇总计算") {
    if (columnIndex !== 0) {
      return {
        backgroundColor: "var(--el-table-row-hover-bg-color)",
        fontWeight: 500,
      };
    }
  }
}
</script>

vue3 数组应用

  • 过滤 sizeOptions 对象数组,判断 val 数组中是否包含 id,将包含该 id 的对象数组返回
  • 改造对象数组,用 map 获取值,改造直接返回
js
// val是一个[1,2,3]
sizeTableData.value = sizeOptions.value
  .filter((item) => val.includes(item.id))
  .map((item) => ({
    sizeTypeId: item.id,
    sizeTypeName: item.name,
    price: "",
    sequence: "",
  }));
// val是一个[1,2,3]
sizeTableData.value = sizeOptions.value
  .filter((item) => val.includes(item.id))
  .map((item) => ({
    sizeTypeId: item.id,
    sizeTypeName: item.name,
    price: "",
    sequence: "",
  }));

vue3 下拼音插件的使用

  • "js-pinyin": "^0.2.4",
js
import pinyin from "js-pinyin";
pinyin.setOptions({ checkPolyphone: false, charCase: 0 });

watch(ruleForm, (newValue) => {
  if (newValue) {
    ruleForm.value.janeSearch = pinyin.getCamelChars(newValue.name);
  }
});
import pinyin from "js-pinyin";
pinyin.setOptions({ checkPolyphone: false, charCase: 0 });

watch(ruleForm, (newValue) => {
  if (newValue) {
    ruleForm.value.janeSearch = pinyin.getCamelChars(newValue.name);
  }
});

vue3 富文本使用

  • 安装
json
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
  • 运行
js
<div style="width: 1000px; border: 1px solid #ccc">
  <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :default-config="toolbarConfig" />
  <Editor
    v-model="ruleForm.description"
    style="height: 120px; overflow-y: hidden"
    :default-config="editorConfig"
    :mode="mode"
    @onCreated="onCreated" />
</div>

import { Editor, Toolbar } from '@wangeditor/editor-for-vue'

const editor = ref(null)
const toolbarConfig = ref({})
const goodUploadImg = ref('')
const editorConfig = ref({
  placeholder: '请输入内容...',
  MENU_CONF: {
    uploadImage: {
      customUpload: goodUploadImg.value
    }
  }
})
const mode = ref('default') // or 'simple'

const onCreated = (val) => {
  editor.value = Object.seal(val) // 一定要用 Object.seal() ,否则会报错
}


<style src="@wangeditor/editor/dist/css/style.css"></style>
<div style="width: 1000px; border: 1px solid #ccc">
  <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :default-config="toolbarConfig" />
  <Editor
    v-model="ruleForm.description"
    style="height: 120px; overflow-y: hidden"
    :default-config="editorConfig"
    :mode="mode"
    @onCreated="onCreated" />
</div>

import { Editor, Toolbar } from '@wangeditor/editor-for-vue'

const editor = ref(null)
const toolbarConfig = ref({})
const goodUploadImg = ref('')
const editorConfig = ref({
  placeholder: '请输入内容...',
  MENU_CONF: {
    uploadImage: {
      customUpload: goodUploadImg.value
    }
  }
})
const mode = ref('default') // or 'simple'

const onCreated = (val) => {
  editor.value = Object.seal(val) // 一定要用 Object.seal() ,否则会报错
}


<style src="@wangeditor/editor/dist/css/style.css"></style>

vue3 下拉框与表格联动

js
const setSize = (val) => {
  // if (val.length !== 0) {
  //  表格数据增加判断
  if (val.length > sizeTableData.value.length) {
    // console.log('select选择数量大于表格数目,说明此时是新增操作')

    /**
     * 添加:
     * 当前的选项是val数组的最后一项,即获取最后一项的id
     * 根据最后一项的id,匹配下拉列表,获取规格名称
     * 将匹配的规格名称添加到表格中
     */

    // 获取当前添加项
    let currentSelect = sizeOptions.value.filter(
      (item) => item.id === val[val.length - 1]
    )[0];
    sizeTableData.value.push({
      sizeTypeId: currentSelect.id,
      sizeTypeName: currentSelect.name,
      price: "",
      sequence: "",
    });
  }

  //  减少数据表格判断
  if (val.length < sizeTableData.value.length) {
    // console.log('select选择数量小于表格数目,说明此时是减少操作')

    /**
     * 删除:
     * 减少之后,val数组中数据会少一项,判断进入减少操作
     * 遍历表格数据,与val数组做比较,找到减少的那一项
     * 在表格中删除那一项
     */
    /* let currentDelete = sizeTableData.value.filter((item) => !val.includes(item.sizeTypeId))
    console.log(currentDelete) */
    sizeTableData.value.forEach((item, index) => {
      if (!val.includes(item.sizeTypeId)) {
        sizeTableData.value.splice(index, 1);
      }
    });
  }
  // }

  // 将表格数据赋值到表单数据
  ruleForm.value.sizeList = sizeTableData.value;
};
const setSize = (val) => {
  // if (val.length !== 0) {
  //  表格数据增加判断
  if (val.length > sizeTableData.value.length) {
    // console.log('select选择数量大于表格数目,说明此时是新增操作')

    /**
     * 添加:
     * 当前的选项是val数组的最后一项,即获取最后一项的id
     * 根据最后一项的id,匹配下拉列表,获取规格名称
     * 将匹配的规格名称添加到表格中
     */

    // 获取当前添加项
    let currentSelect = sizeOptions.value.filter(
      (item) => item.id === val[val.length - 1]
    )[0];
    sizeTableData.value.push({
      sizeTypeId: currentSelect.id,
      sizeTypeName: currentSelect.name,
      price: "",
      sequence: "",
    });
  }

  //  减少数据表格判断
  if (val.length < sizeTableData.value.length) {
    // console.log('select选择数量小于表格数目,说明此时是减少操作')

    /**
     * 删除:
     * 减少之后,val数组中数据会少一项,判断进入减少操作
     * 遍历表格数据,与val数组做比较,找到减少的那一项
     * 在表格中删除那一项
     */
    /* let currentDelete = sizeTableData.value.filter((item) => !val.includes(item.sizeTypeId))
    console.log(currentDelete) */
    sizeTableData.value.forEach((item, index) => {
      if (!val.includes(item.sizeTypeId)) {
        sizeTableData.value.splice(index, 1);
      }
    });
  }
  // }

  // 将表格数据赋值到表单数据
  ruleForm.value.sizeList = sizeTableData.value;
};

emit 执行组件方法

js
import { ref, getCurrentInstance } from "vue";
const instance = getCurrentInstance();

// 定义
instance?.proxy?.emitter.on("refreshReportTable", () => {
  getTableData(setProxy(queryParams.value));
});

// 执行
instance?.proxy?.emitter.emit("refreshReportTable");
import { ref, getCurrentInstance } from "vue";
const instance = getCurrentInstance();

// 定义
instance?.proxy?.emitter.on("refreshReportTable", () => {
  getTableData(setProxy(queryParams.value));
});

// 执行
instance?.proxy?.emitter.emit("refreshReportTable");

判断对象数组中是否包含某个值

js
let isHasId = businessOptions.value.some(
  (item) => item.id === res.content.businessId
);
let isHasId = businessOptions.value.some(
  (item) => item.id === res.content.businessId
);

校验正数,保留一位小数

js
@input='checkInputData'
const checkInputData = () => {
  let reg = /^\d+(\.\d{1,1})?$/

  if (!reg.test(formData.value.promotionY) || parseFloat(formData.value.promotionY) <= 0 || parseFloat(formData.value.promotionY) > 10) {
    formData.value.promotionY = formData.value.promotionY.slice(0, formData.value.promotionY.length - 1)
  }
}
@input='checkInputData'
const checkInputData = () => {
  let reg = /^\d+(\.\d{1,1})?$/

  if (!reg.test(formData.value.promotionY) || parseFloat(formData.value.promotionY) <= 0 || parseFloat(formData.value.promotionY) > 10) {
    formData.value.promotionY = formData.value.promotionY.slice(0, formData.value.promotionY.length - 1)
  }
}

获取对象数组中某个字段,转换成字符串

js
data.userRoles = data.roleDTOS.map((item) => item.name).join(",");
data.userRoles = data.roleDTOS.map((item) => item.name).join(",");

js 如何判断小数点后有几位

js
<script>var n=3.143423423; alert(n.toString().split(".")[1].length);</script>
<script>var n=3.143423423; alert(n.toString().split(".")[1].length);</script>

cascader 获取 name 值

js
industryRef.value.getCheckedNodes()[0].pathLabels.join("-");
industryRef.value.getCheckedNodes()[0].pathLabels.join("-");

vue3 中跳转不刷新

js
import { useRoute } from 'vue-router'
const route = useRoute()
 <router-view :key="route.fullPath"></router-view>
import { useRoute } from 'vue-router'
const route = useRoute()
 <router-view :key="route.fullPath"></router-view>

pina 的使用

js
// store/index.js
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

// pinia persist
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

export default pinia;

// store/modules/demo-store-data.js
import { defineStore } from "pinia";

export const demoStoreData = defineStore({
  id: "demo-store-data",

  state: () => ({
    currentBunkList: [], // 过滤后的店铺数据
  }),

  actions: {
    setCurrentBunkList(currentBunkList) {
      this.currentBunkList = currentBunkList;
    },
  },
});

// 使用 - 设置值
// store
import { demoStoreData } from "@/store/modules/demo-store-data.js";
let { setCurrentBunkList } = demoStoreData();
setCurrentBunkList(bunkData.content.filter((item) => item.type === 1));

// 使用 - 获取值
import { demoStoreData } from "@/store/modules/demo-store-data.js";
const demoDataStore = demoStoreData();
const currentBunkList = demoDataStore.currentBunkList;
// store/index.js
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

// pinia persist
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

export default pinia;

// store/modules/demo-store-data.js
import { defineStore } from "pinia";

export const demoStoreData = defineStore({
  id: "demo-store-data",

  state: () => ({
    currentBunkList: [], // 过滤后的店铺数据
  }),

  actions: {
    setCurrentBunkList(currentBunkList) {
      this.currentBunkList = currentBunkList;
    },
  },
});

// 使用 - 设置值
// store
import { demoStoreData } from "@/store/modules/demo-store-data.js";
let { setCurrentBunkList } = demoStoreData();
setCurrentBunkList(bunkData.content.filter((item) => item.type === 1));

// 使用 - 获取值
import { demoStoreData } from "@/store/modules/demo-store-data.js";
const demoDataStore = demoStoreData();
const currentBunkList = demoDataStore.currentBunkList;

mitt 事件总线的使用

js
// util/mittBus
import mitt from "mitt";
const mittBus = mitt();
export default mittBus;

// 使用 - 定义事件
import mittBus from "@/util/mitt-bus.js"; // mitt
mittBus.emit("clickArea", tipNum);

// 使用 - 执行事件
import mittBus from "@/util/mitt-bus.js"; // mitt
mittBus.on("clickArea", (tipNum) => {
  // console.log(tipNum)

  showDetailDialog(tipNum);
  // xxxxxx
});
// util/mittBus
import mitt from "mitt";
const mittBus = mitt();
export default mittBus;

// 使用 - 定义事件
import mittBus from "@/util/mitt-bus.js"; // mitt
mittBus.emit("clickArea", tipNum);

// 使用 - 执行事件
import mittBus from "@/util/mitt-bus.js"; // mitt
mittBus.on("clickArea", (tipNum) => {
  // console.log(tipNum)

  showDetailDialog(tipNum);
  // xxxxxx
});

星期选择

js
<el-select style="width: 240px" v-model="ruleForm.week" multiple placeholder="请选择">
  <el-option :label="item" :value="item" v-for="item in 7" :key="item" :disabled="isHasWeek(item)">
    {{ item }}
  </el-option>
</el-select>

const isHasWeek = (item) => {
  if (ruleForm.value) {
    if (ruleForm.value.week.length !== 0) {
      return ruleForm.value.week.map((jItem) => parseInt(jItem, 10)).includes(item)
    }
  }
}
<el-select style="width: 240px" v-model="ruleForm.week" multiple placeholder="请选择">
  <el-option :label="item" :value="item" v-for="item in 7" :key="item" :disabled="isHasWeek(item)">
    {{ item }}
  </el-option>
</el-select>

const isHasWeek = (item) => {
  if (ruleForm.value) {
    if (ruleForm.value.week.length !== 0) {
      return ruleForm.value.week.map((jItem) => parseInt(jItem, 10)).includes(item)
    }
  }
}

v-model遇上props

  • 所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
    • v-model cannot be used on a prop, because local prop bindings are not writable
  • 创建一个中间值,v-model绑定中间值,监听传过来的数据,当prop数据发生改变,立即改变v-model的数据
html
<le-dropdown
v-model:menuList="currentMenuList"
@onConfirm="onConfirm"
/>

<script setup>
const props = defineProps({
  // 导航数据
  menuList: {
    type: Array,
    default: () => DropdownList
  }
})

const currentMenuList = ref(props.menuList)

watch(
  () => props.menuList,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
    currentMenuList.value = newVal
  }
)
  </script>
<le-dropdown
v-model:menuList="currentMenuList"
@onConfirm="onConfirm"
/>

<script setup>
const props = defineProps({
  // 导航数据
  menuList: {
    type: Array,
    default: () => DropdownList
  }
})

const currentMenuList = ref(props.menuList)

watch(
  () => props.menuList,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
    currentMenuList.value = newVal
  }
)
  </script>

vue3的v-model

  • v-model 默认绑定的属性名为:modelValue
  • v-model 默认绑定的事件名为:update:modelValue
js
// 所以我们需要使用 modelValue 和 update:modelValue 来接收
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
// 所以我们需要使用 modelValue 和 update:modelValue 来接收
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}

element-plus中date-picker组件属性default-time不生效解决办法

js
<el-date-picker
    v-model="ruleForm.datePicker"
    type="datetimerange"
    range-separator="—"
    start-placeholder="开始日期"
    end-placeholder="结束日期"
    :default-time="defaultTime"
    format="YYYY-MM-DD HH:mm:ss"
    value-format="YYYY-MM-DD HH:mm:ss"></el-date-picker>

const defaultTime = reactive([new Date(0, 0, 0, 0, 0, 0), new Date(0, 0, 0, 23, 59, 59)])
<el-date-picker
    v-model="ruleForm.datePicker"
    type="datetimerange"
    range-separator="—"
    start-placeholder="开始日期"
    end-placeholder="结束日期"
    :default-time="defaultTime"
    format="YYYY-MM-DD HH:mm:ss"
    value-format="YYYY-MM-DD HH:mm:ss"></el-date-picker>

const defaultTime = reactive([new Date(0, 0, 0, 0, 0, 0), new Date(0, 0, 0, 23, 59, 59)])

el-table中使用el-image的预览属性导致样式重叠

html
<el-image src={scope.row.picUrl} preview-src-list={[scope.row.picUrl]} style="width:60px;height:60px"></el-image>

<style>
  /* 单元格样式 */
.el-table__cell {
   position: static !important; /* 解决el-image 和 el-table冲突层级冲突问题 */
}
</style>
<el-image src={scope.row.picUrl} preview-src-list={[scope.row.picUrl]} style="width:60px;height:60px"></el-image>

<style>
  /* 单元格样式 */
.el-table__cell {
   position: static !important; /* 解决el-image 和 el-table冲突层级冲突问题 */
}
</style>
  • 选项卡
js
<div  class="xny_part1_items" @click="itemTitle = '光伏'" :class="itemTitle === '光伏' && 'active'"></div>
...

<div class="lgcb_sub_cont" v-if="itemTitle === '光伏'"></div>

let itemTitle = ref("光伏");
<div  class="xny_part1_items" @click="itemTitle = '光伏'" :class="itemTitle === '光伏' && 'active'"></div>
...

<div class="lgcb_sub_cont" v-if="itemTitle === '光伏'"></div>

let itemTitle = ref("光伏");

proxy.$modal相关方法

js
// poxy对象
const { proxy } = getCurrentInstance();

proxy.$modal.msgError('msg');
proxy.$modal.alert('msg');
proxy.$modal.msgWarning('msg');
proxy.$modal.msgSuccess('msg');
proxy.$modal.loading("正在提交,请稍候...");
proxy.$modal.closeLoading();

proxy.$modal.confirm(
    `确定移除 ${file.name} ?`
).then(
    () => true,
    () => false
)
// poxy对象
const { proxy } = getCurrentInstance();

proxy.$modal.msgError('msg');
proxy.$modal.alert('msg');
proxy.$modal.msgWarning('msg');
proxy.$modal.msgSuccess('msg');
proxy.$modal.loading("正在提交,请稍候...");
proxy.$modal.closeLoading();

proxy.$modal.confirm(
    `确定移除 ${file.name} ?`
).then(
    () => true,
    () => false
)

封装api

js
// api 封装
export const apiCommon = (api, params, header = undefined) => {
  return new Promise((resolve, reject) => {
    api(params, header).then(res => {
      resolve(res)
    }).catch(err => {
      reject(err)
      return
    })
  })
}
// api 封装
export const apiCommon = (api, params, header = undefined) => {
  return new Promise((resolve, reject) => {
    api(params, header).then(res => {
      resolve(res)
    }).catch(err => {
      reject(err)
      return
    })
  })
}

模糊搜索封装

vue
<template>
  <el-form ref="ruleFormRef" :inline="true" :model="ruleForm" label-width="80px">
    <!-- <xxxxxx -->

    <el-form-item label="" style="margin-left: 13px">
      <el-autocomplete v-model="ruleForm.dynamicFieldsValue" :fetch-suggestions="querySearch"
        @select="selectGisSearchSubmit" style="width: 400px" placeholder="请输入" clearable :trigger-on-focus="true"
        :debounce="100" :disabled="renderFlag">
        <template #prepend>
          <el-select v-model="ruleForm.searchType" style="width: 115px" @change="selectSearchCriteria">
            <el-option v-for="(item, index) in searchCriteriaList" :key="index" :label="item.label"
              :value="item.prop" />
          </el-select>
        </template>
        <template #default="{ item }">
          <div class="auto_cellname_item_wrap">
            <!-- 自定义搜索列表 -->
            <!-- ...... -->
          </div>
        </template>
        <template #append>
          <el-button type="primary" @click="setGisSearchSubmit">
            <el-icon>
              <Search />
            </el-icon>
          </el-button>
        </template>
      </el-autocomplete>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { ref, computed } from "vue";
// 组件传参
import mittBus from "@/utils/mittBus"; // mitt
// api相关
import { apiCommon } from "@/utils/index.js";
import * as gisApi from "@/api/gis/gis";

const ruleFormRef = ref(null);
let currentZoom = ref(0);
let currentMinRenderZoom = 0;

let renderFlag = computed(() => {
  return parseInt(currentZoom.value) < currentMinRenderZoom;
});

// 搜索条件
const searchCriteriaList = ref([
  {
    label: "xxx",
    prop: "name",
    eventName: "searchByName",
    isShowSelect: true,
  },
  {
    label: "xxx",
    prop: "name",
    eventName: "searchByName1",
    isShowSelect: false,
  },
]);

const ruleForm = ref({
  searchType: searchCriteriaList.value[0].prop, // 搜索条件
  dynamicFieldsValue: "", // 动态字段结果
});

/**
 * 接收其他组件派发的方法
 */
// 获取当前zoom
mittBus.on("getCurrentZoom", ({ zoom, minRenderZoom }) => {
  // console.log("当前缩放级别为:" + zoom, minRenderZoom);

  currentZoom.value = zoom;
  currentMinRenderZoom = minRenderZoom;
});

/**
 * 业务
 */
// 选择搜索条件
const selectSearchCriteria = () => {
  // console.log('搜索类型', ruleForm.value.searchType, ruleForm.value.dynamicFieldsValue)

  ruleForm.value.dynamicFieldsValue = "";
};

// gis查询
const setGisSearchSubmit = () => {
  // console.log('gis查询', ruleForm.value.searchType, ruleForm.value.dynamicFieldsValue)

  // 当输入内容时才派发事件
  if (ruleForm.value.dynamicFieldsValue !== "") {
    searchCriteriaList.value.forEach((item) => {
      switch (ruleForm.value.searchType) {
        case item.prop:
          // 根据定义的名称派发事件
          mittBus.emit(item.eventName, ruleForm.value.dynamicFieldsValue);
          break;
      }
    });
  }
};

// 获取下拉框数据接口
const getSelectDatyAsync = async (queryString) => {
  let gisData = null;

  // 小区名称
  if (ruleForm.value.searchType === "name") {
    let params = {
      name: queryString,
    };
    gisData = await apiCommon(gisApi.queryCellListByName, params);

    gisData.data.forEach((item) => {
      item.value = item.name;
    });
  }
  // 站点名称名称
  if (ruleForm.value.searchType === "name1") {
    let params = {
      name1: queryString,
    };
    gisData = await apiCommon(gisApi.queryCellListByName1, params);

    gisData.data.forEach((item) => {
      item.value = item.name1;
    });
  }

  return gisData.data;
};

// 获取下拉框数据
const querySearch = async (queryString, cb) => {
  searchCriteriaList.value.forEach(async (item) => {
    // 识别当前搜索条件
    if (item.prop === ruleForm.value.searchType) {
      // 如果不需要模糊搜索不显示下拉框
      if (!item.isShowSelect) {
        cb([]);
      } else {
        if (queryString !== "") {
          getSelectDatyAsync(queryString).then((gisData) => {
            // console.log(gisData)
            cb(gisData);
          });
        } else {
          cb([]);
        }
      }
    }
  });
};

// 搜索框下拉选择
const selectGisSearchSubmit = (val) => {
  // console.log('搜索框下拉选择', val)

  mittBus.emit("selectGisSearchSubmit", val);
};
</script>

<style lang="scss" scoped>
.el-popper.is-pure {
  .auto_cellname_item_wrap {
    // background: #f00;
    border-bottom: solid 1px #efefef;
    // padding-bottom: 10px;

    span {
      display: block;
      margin-top: -15px;
    }
  }
}
</style>
<template>
  <el-form ref="ruleFormRef" :inline="true" :model="ruleForm" label-width="80px">
    <!-- <xxxxxx -->

    <el-form-item label="" style="margin-left: 13px">
      <el-autocomplete v-model="ruleForm.dynamicFieldsValue" :fetch-suggestions="querySearch"
        @select="selectGisSearchSubmit" style="width: 400px" placeholder="请输入" clearable :trigger-on-focus="true"
        :debounce="100" :disabled="renderFlag">
        <template #prepend>
          <el-select v-model="ruleForm.searchType" style="width: 115px" @change="selectSearchCriteria">
            <el-option v-for="(item, index) in searchCriteriaList" :key="index" :label="item.label"
              :value="item.prop" />
          </el-select>
        </template>
        <template #default="{ item }">
          <div class="auto_cellname_item_wrap">
            <!-- 自定义搜索列表 -->
            <!-- ...... -->
          </div>
        </template>
        <template #append>
          <el-button type="primary" @click="setGisSearchSubmit">
            <el-icon>
              <Search />
            </el-icon>
          </el-button>
        </template>
      </el-autocomplete>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { ref, computed } from "vue";
// 组件传参
import mittBus from "@/utils/mittBus"; // mitt
// api相关
import { apiCommon } from "@/utils/index.js";
import * as gisApi from "@/api/gis/gis";

const ruleFormRef = ref(null);
let currentZoom = ref(0);
let currentMinRenderZoom = 0;

let renderFlag = computed(() => {
  return parseInt(currentZoom.value) < currentMinRenderZoom;
});

// 搜索条件
const searchCriteriaList = ref([
  {
    label: "xxx",
    prop: "name",
    eventName: "searchByName",
    isShowSelect: true,
  },
  {
    label: "xxx",
    prop: "name",
    eventName: "searchByName1",
    isShowSelect: false,
  },
]);

const ruleForm = ref({
  searchType: searchCriteriaList.value[0].prop, // 搜索条件
  dynamicFieldsValue: "", // 动态字段结果
});

/**
 * 接收其他组件派发的方法
 */
// 获取当前zoom
mittBus.on("getCurrentZoom", ({ zoom, minRenderZoom }) => {
  // console.log("当前缩放级别为:" + zoom, minRenderZoom);

  currentZoom.value = zoom;
  currentMinRenderZoom = minRenderZoom;
});

/**
 * 业务
 */
// 选择搜索条件
const selectSearchCriteria = () => {
  // console.log('搜索类型', ruleForm.value.searchType, ruleForm.value.dynamicFieldsValue)

  ruleForm.value.dynamicFieldsValue = "";
};

// gis查询
const setGisSearchSubmit = () => {
  // console.log('gis查询', ruleForm.value.searchType, ruleForm.value.dynamicFieldsValue)

  // 当输入内容时才派发事件
  if (ruleForm.value.dynamicFieldsValue !== "") {
    searchCriteriaList.value.forEach((item) => {
      switch (ruleForm.value.searchType) {
        case item.prop:
          // 根据定义的名称派发事件
          mittBus.emit(item.eventName, ruleForm.value.dynamicFieldsValue);
          break;
      }
    });
  }
};

// 获取下拉框数据接口
const getSelectDatyAsync = async (queryString) => {
  let gisData = null;

  // 小区名称
  if (ruleForm.value.searchType === "name") {
    let params = {
      name: queryString,
    };
    gisData = await apiCommon(gisApi.queryCellListByName, params);

    gisData.data.forEach((item) => {
      item.value = item.name;
    });
  }
  // 站点名称名称
  if (ruleForm.value.searchType === "name1") {
    let params = {
      name1: queryString,
    };
    gisData = await apiCommon(gisApi.queryCellListByName1, params);

    gisData.data.forEach((item) => {
      item.value = item.name1;
    });
  }

  return gisData.data;
};

// 获取下拉框数据
const querySearch = async (queryString, cb) => {
  searchCriteriaList.value.forEach(async (item) => {
    // 识别当前搜索条件
    if (item.prop === ruleForm.value.searchType) {
      // 如果不需要模糊搜索不显示下拉框
      if (!item.isShowSelect) {
        cb([]);
      } else {
        if (queryString !== "") {
          getSelectDatyAsync(queryString).then((gisData) => {
            // console.log(gisData)
            cb(gisData);
          });
        } else {
          cb([]);
        }
      }
    }
  });
};

// 搜索框下拉选择
const selectGisSearchSubmit = (val) => {
  // console.log('搜索框下拉选择', val)

  mittBus.emit("selectGisSearchSubmit", val);
};
</script>

<style lang="scss" scoped>
.el-popper.is-pure {
  .auto_cellname_item_wrap {
    // background: #f00;
    border-bottom: solid 1px #efefef;
    // padding-bottom: 10px;

    span {
      display: block;
      margin-top: -15px;
    }
  }
}
</style>

vue3设置全局变量

js
app.config.globalProperties.msg = 'hello'

onMounted(() => {
    // 通过getCurrentInstance().appContext访问全局属性
    const { msg } = getCurrentInstance().appContext.config.globalProperties
    console.log('msg', msg)
})
app.config.globalProperties.msg = 'hello'

onMounted(() => {
    // 通过getCurrentInstance().appContext访问全局属性
    const { msg } = getCurrentInstance().appContext.config.globalProperties
    console.log('msg', msg)
})

vue3解析excel

  • uploadtools
vue
<template>
    <el-upload class="upload_wrap" multiple :limit="limitNum" :file-list="fileList" :auto-upload="false"
        :before-remove="beforeRemove" :on-exceed="handleExceed" :on-preview="handlePreview" :on-change="changeFile">
        <el-button>点击上传</el-button>
        <template #tip>
            <div class="el-upload__tip">
                <slot name="tip"></slot>
            </div>
        </template>
    </el-upload>
</template>

<script setup>
import { ref, getCurrentInstance } from 'vue';
import { ElMessage } from 'element-plus';

// poxy对象
const { proxy } = getCurrentInstance();

// 子组件自定义事件
const emit = defineEmits(['verifyFileType', 'getFile'])

const fileList = ref([])
const limitNum = ref(1)

// 删除文件之前的钩子
const beforeRemove = (file) => {
    return proxy.$modal.confirm(
        `确定移除 ${file.name} ?`
    ).then(
        () => true,
        () => false
    )
}

// 限制提示
const handleExceed = () => {
    ElMessage.warning(`当前限制选择 ${limitNum.value} 个文件`)
}

// 点击文件
const handlePreview = file => {
    console.log("点击文件", file)
}

// 校验文件类型
const verifyFileType = (file, next) => {
    emit('verifyFileType', file, flag => {
        if (!flag) {
            fileList.value = fileList.value.pop()
        }

        next(flag)
    })
}

// 文件改变后触发
const changeFile = file => {
    let flag = verifyFileType(file, flag => {
        if (flag) {
            emit('getFile', file)
        }
    })
}
</script>

<style lang="scss" scoped>
.upload_wrap {
    width: 275px;

    :deep .el-upload-list {
        max-height: 100px;
        overflow-y: auto;
    }
}
</style>
<template>
    <el-upload class="upload_wrap" multiple :limit="limitNum" :file-list="fileList" :auto-upload="false"
        :before-remove="beforeRemove" :on-exceed="handleExceed" :on-preview="handlePreview" :on-change="changeFile">
        <el-button>点击上传</el-button>
        <template #tip>
            <div class="el-upload__tip">
                <slot name="tip"></slot>
            </div>
        </template>
    </el-upload>
</template>

<script setup>
import { ref, getCurrentInstance } from 'vue';
import { ElMessage } from 'element-plus';

// poxy对象
const { proxy } = getCurrentInstance();

// 子组件自定义事件
const emit = defineEmits(['verifyFileType', 'getFile'])

const fileList = ref([])
const limitNum = ref(1)

// 删除文件之前的钩子
const beforeRemove = (file) => {
    return proxy.$modal.confirm(
        `确定移除 ${file.name} ?`
    ).then(
        () => true,
        () => false
    )
}

// 限制提示
const handleExceed = () => {
    ElMessage.warning(`当前限制选择 ${limitNum.value} 个文件`)
}

// 点击文件
const handlePreview = file => {
    console.log("点击文件", file)
}

// 校验文件类型
const verifyFileType = (file, next) => {
    emit('verifyFileType', file, flag => {
        if (!flag) {
            fileList.value = fileList.value.pop()
        }

        next(flag)
    })
}

// 文件改变后触发
const changeFile = file => {
    let flag = verifyFileType(file, flag => {
        if (flag) {
            emit('getFile', file)
        }
    })
}
</script>

<style lang="scss" scoped>
.upload_wrap {
    width: 275px;

    :deep .el-upload-list {
        max-height: 100px;
        overflow-y: auto;
    }
}
</style>
  • 使用
vue
<template>
  <section>
    <upload-tool @verifyFileType="verifyFileType" @getFile="getFile">
      <template #tip>
        <span>请上传xls或者xlsx格式</span>
      </template>
    </upload-tool>

    <div class="detail_wrap">
      <div class="detail_cont" v-if="JSON.stringify(excelResData) !== '[]'" v-for="(item, index) in excelResData"
        :key="index">
        <h3>{{ item.sheetName }}</h3>
        <div class="table_detail" v-html="dealExcel(item.sheetList)"></div>
      </div>
    </div>
  </section>
</template>

<script setup>
import { ref } from 'vue';
import * as XLSX from 'xlsx';

const excelResData = ref([])

const readFile = (file) => {
  return new Promise((resolve) => {
    let reader = new FileReader()
    reader.readAsBinaryString(file)
    reader.onload = (ev) => {
      resolve(ev.target?.result)
    }
  })
}

const sheetToTable = sheet => {
  // 创建一个<table>元素
  var table = document.createElement('table');
  // table.setAttribute('border', '1');
  // 遍历XLSX.utils.sheet_to_json的结果
  sheet.forEach(function (row) {
    // 创建一个<tr>元素
    var tr = document.createElement('tr');
    // 遍历每一行的键值对
    for (var key in row) {
      // 创建一个<td>元素
      var td = document.createElement('td');
      // 设置<td>元素的文本
      td.appendChild(document.createTextNode(row[key]));
      // 将<td>元素添加到<tr>元素中
      tr.appendChild(td);
    }
    // 将<tr>元素添加到<table>元素中
    table.appendChild(tr);
  });
  // 将<table>元素添加到HTML文档中
  return table
  // document.body.appendChild(table);
}

const verifyFileType = (file, next) => {
  if (!/\.(xls|xlsx)$/.test(file.name.toLowerCase())) {
    ElMessage({
      message: "上传格式不正确,请上传xls或者xlsx格式!",
      type: "error",
    });
    next(false)
    return
  }
  next(true)
}

const getFile = async (file) => {
  let dataBinary = await readFile(file.raw);
  let workbook = XLSX.read(dataBinary, { type: 'binary', cellDates: true });

  workbook.SheetNames.forEach(SheetName => {
    // workbook.Sheets[SheetName] = XLSX.utils.sheet_to_json(workbook.Sheets[SheetName], { range: 1, header: 1, defval: '' })
    workbook.Sheets[SheetName] = XLSX.utils.sheet_to_json(workbook.Sheets[SheetName], { header: 1, defval: '' })
  })

  for (let key in workbook.Sheets) {
    excelResData.value.push({
      sheetName: key,
      sheetList: workbook.Sheets[key]
    })
  }
};

const dealExcel = (sheetList) => {
  return sheetToTable(sheetList).outerHTML
}
</script>

<style lang="scss" scoped>
.detail_wrap {
  padding: 20px;
  width: 95%;
  height: 80vh;
  border: solid 1px #ccc;
  margin: 0 auto;
  overflow: auto;

  .detail_cont {
    &:not(:last-child) {
      margin-bottom: 50px;
    }

    h3 {
      margin-bottom: 10px;
    }

    :deep .table_detail {
      table {
        tbody {
          tr {
            &:first-child {
              background-color: #a3a3a3;
            }

            &:not(:first-child) {
              &:nth-child(odd) {
                background-color: rgb(206, 206, 206);
              }
            }
          }
        }

        td {
          padding: 5px;
          min-width: 100px;
          border: solid 1px #efefef;
          text-align: center;
          cursor: pointer;
        }
      }
    }
  }
}
</style>
<template>
  <section>
    <upload-tool @verifyFileType="verifyFileType" @getFile="getFile">
      <template #tip>
        <span>请上传xls或者xlsx格式</span>
      </template>
    </upload-tool>

    <div class="detail_wrap">
      <div class="detail_cont" v-if="JSON.stringify(excelResData) !== '[]'" v-for="(item, index) in excelResData"
        :key="index">
        <h3>{{ item.sheetName }}</h3>
        <div class="table_detail" v-html="dealExcel(item.sheetList)"></div>
      </div>
    </div>
  </section>
</template>

<script setup>
import { ref } from 'vue';
import * as XLSX from 'xlsx';

const excelResData = ref([])

const readFile = (file) => {
  return new Promise((resolve) => {
    let reader = new FileReader()
    reader.readAsBinaryString(file)
    reader.onload = (ev) => {
      resolve(ev.target?.result)
    }
  })
}

const sheetToTable = sheet => {
  // 创建一个<table>元素
  var table = document.createElement('table');
  // table.setAttribute('border', '1');
  // 遍历XLSX.utils.sheet_to_json的结果
  sheet.forEach(function (row) {
    // 创建一个<tr>元素
    var tr = document.createElement('tr');
    // 遍历每一行的键值对
    for (var key in row) {
      // 创建一个<td>元素
      var td = document.createElement('td');
      // 设置<td>元素的文本
      td.appendChild(document.createTextNode(row[key]));
      // 将<td>元素添加到<tr>元素中
      tr.appendChild(td);
    }
    // 将<tr>元素添加到<table>元素中
    table.appendChild(tr);
  });
  // 将<table>元素添加到HTML文档中
  return table
  // document.body.appendChild(table);
}

const verifyFileType = (file, next) => {
  if (!/\.(xls|xlsx)$/.test(file.name.toLowerCase())) {
    ElMessage({
      message: "上传格式不正确,请上传xls或者xlsx格式!",
      type: "error",
    });
    next(false)
    return
  }
  next(true)
}

const getFile = async (file) => {
  let dataBinary = await readFile(file.raw);
  let workbook = XLSX.read(dataBinary, { type: 'binary', cellDates: true });

  workbook.SheetNames.forEach(SheetName => {
    // workbook.Sheets[SheetName] = XLSX.utils.sheet_to_json(workbook.Sheets[SheetName], { range: 1, header: 1, defval: '' })
    workbook.Sheets[SheetName] = XLSX.utils.sheet_to_json(workbook.Sheets[SheetName], { header: 1, defval: '' })
  })

  for (let key in workbook.Sheets) {
    excelResData.value.push({
      sheetName: key,
      sheetList: workbook.Sheets[key]
    })
  }
};

const dealExcel = (sheetList) => {
  return sheetToTable(sheetList).outerHTML
}
</script>

<style lang="scss" scoped>
.detail_wrap {
  padding: 20px;
  width: 95%;
  height: 80vh;
  border: solid 1px #ccc;
  margin: 0 auto;
  overflow: auto;

  .detail_cont {
    &:not(:last-child) {
      margin-bottom: 50px;
    }

    h3 {
      margin-bottom: 10px;
    }

    :deep .table_detail {
      table {
        tbody {
          tr {
            &:first-child {
              background-color: #a3a3a3;
            }

            &:not(:first-child) {
              &:nth-child(odd) {
                background-color: rgb(206, 206, 206);
              }
            }
          }
        }

        td {
          padding: 5px;
          min-width: 100px;
          border: solid 1px #efefef;
          text-align: center;
          cursor: pointer;
        }
      }
    }
  }
}
</style>

vue3的计算属性

js
import { computed } from 'vue'
const indicator = computed(() => {
  if (JSON.stringify(props.chartData) === '[]') {
    return [  //配置各个维度的最大值
      { name: '', max: 0 },
    ]
  }
  return props.chartData.map(item => {
    return {
      name: item.name,
      max: 100
    }
  })
})
import { computed } from 'vue'
const indicator = computed(() => {
  if (JSON.stringify(props.chartData) === '[]') {
    return [  //配置各个维度的最大值
      { name: '', max: 0 },
    ]
  }
  return props.chartData.map(item => {
    return {
      name: item.name,
      max: 100
    }
  })
})

vue3的监听

js
// setoption解构传参用这种监听
/* watch(
  props.chartData,
  (val) => {
    setOption(val)
  },
  { deep: true }
) */
watch(() => props.chartData, val => {
  setOption(val)
})
// setoption解构传参用这种监听
/* watch(
  props.chartData,
  (val) => {
    setOption(val)
  },
  { deep: true }
) */
watch(() => props.chartData, val => {
  setOption(val)
})

全屏

js
let isFullscreen = false  // 是否全屏

export const setFullScreen = (falg, next) => {
  if (!falg) {
    // 全屏
    if (document.documentElement.requestFullscreen) {
      document.documentElement.requestFullscreen();
    } else if (document.documentElement.mozRequestFullScreen) {
      document.documentElement.mozRequestFullScreen();
    } else if (document.documentElement.webkitRequestFullscreen) {
      document.documentElement.webkitRequestFullscreen();
    } else if (element.msRequestFullscreen) {
      document.documentElement.msRequestFullscreen();
    }
  } else {
    // 退出全屏
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }

  next()
}

watch(() => isFullscreen.value, () => {
  setTimeout(() => {
    nextTick(() => {
      refLeftPage.value && refLeftPage.value.resetChart()
      refCenterPage.value && refCenterPage.value.resetChart()
      refRightPage.value && refRightPage.value.resetChart()
    })
  }, 500)
})

// 监听全屏状态修改标识
const setFullScreenFlag = (e) => {
  window.addEventListener('fullscreenchange', (event) => {
    if (document.fullscreenElement) {
      // console.log('进入全屏模式');
      isFullscreen.value = true
    } else {
      // console.log('退出全屏模式');
      isFullscreen.value = false
    }
  });
}

<div class="title" @click="setFullScreen(isFullscreen, () => { isFullscreen = !isFullscreen })">
let isFullscreen = false  // 是否全屏

export const setFullScreen = (falg, next) => {
  if (!falg) {
    // 全屏
    if (document.documentElement.requestFullscreen) {
      document.documentElement.requestFullscreen();
    } else if (document.documentElement.mozRequestFullScreen) {
      document.documentElement.mozRequestFullScreen();
    } else if (document.documentElement.webkitRequestFullscreen) {
      document.documentElement.webkitRequestFullscreen();
    } else if (element.msRequestFullscreen) {
      document.documentElement.msRequestFullscreen();
    }
  } else {
    // 退出全屏
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }

  next()
}

watch(() => isFullscreen.value, () => {
  setTimeout(() => {
    nextTick(() => {
      refLeftPage.value && refLeftPage.value.resetChart()
      refCenterPage.value && refCenterPage.value.resetChart()
      refRightPage.value && refRightPage.value.resetChart()
    })
  }, 500)
})

// 监听全屏状态修改标识
const setFullScreenFlag = (e) => {
  window.addEventListener('fullscreenchange', (event) => {
    if (document.fullscreenElement) {
      // console.log('进入全屏模式');
      isFullscreen.value = true
    } else {
      // console.log('退出全屏模式');
      isFullscreen.value = false
    }
  });
}

<div class="title" @click="setFullScreen(isFullscreen, () => { isFullscreen = !isFullscreen })">

在el-dialog中添加锚点

vue
 <!-- 内容 -->
<template #DialogContainer>
    <div class="dialog_wrap">
        <div class="steps">
            <ul>
                <li v-for="(item, index) in title_list" :key="index">
                    <span ref="spans" :style="{ color: activeStep === index ? '#1987e1' : '#000000' }"
                        @click="jump(index)">
                        <i class="el-icon-thumb"></i>{{ item.title }}
                    </span>
                </li>
            </ul>
        </div>

        <div class="result" @scroll="onScroll">
            <div class="scroll-item"><span>第一项项</span></div>
            <div class="scroll-item"><span>第二项项</span></div>
            <div class="scroll-item"><span>第三项项</span></div>
            <div class="scroll-item"><span>第四项项</span></div>
            <div class="scroll-item"><span>第五项项</span></div>
            <div class="scroll-item"><span>第六项项</span></div>
        </div>
    </div>
</template>

<script setup>
let activeStep = 0
let title_list = [
    { title: '第一项项' },
    { title: '第二项项' },
    { title: '第三项项' },
    { title: '第四项项' },
    { title: '第五项项' },
    { title: '第六项项' },
]

const jump = (index) => {
    var items = document.querySelectorAll(".scroll-item");
    for (var i = 0; i < items.length; i++) {
        if (index === i) {
            items[i].scrollIntoView({
                block: "start",//默认跳转到顶部
                behavior: "smooth"//滚动的速度
            });
        }
    }
}
const onScroll = (e) => {
    let scrollItems = document.querySelectorAll(".scroll-item");
    for (let i = scrollItems.length - 1; i >= 0; i--) {
        // 判断滚动条滚动距离是否大于当前滚动项可滚动距离
        let judge =
            e.target.scrollTop >= scrollItems[i].offsetTop - scrollItems[0].offsetTop;
        if (judge) {
            activeStep = i;
            break;
        }
    }
}
</script>

<style lang="scss" scoped>
.el-dialog {
  overflow: hidden;

  .steps {
    background-color: #fff;
    max-height: calc(-16px + 100vh);
    position: fixed;
    width: 98px;
    top: 90px;
    right: 2%;

    ul {
      list-style: none;
      padding-left: 5px;
      margin: 12px 0;
    }

    li {
      margin: 7px 5px;

      span {
        cursor: pointer;

        &:hover {
          color: #88bcec !important;
        }

        i {
          margin-right: 5px;
        }
      }
    }
  }

  .result {
    position: absolute;
    left: 10px;
    top: 54px;
    bottom: 70px;
    right: 0;
    padding: 0;
    overflow-y: scroll;

    .scroll-item {
      width: 100%;
      height: 500px;
      margin-top: 20px;
      background: rgb(137, 238, 238);

      >span {
        font-size: 20px;
      }

      &:first-child {
        margin-top: 0;
      }

      &:last-child {
        height: 500px;
      }
    }
  }

  .el-dialog__footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
  }
}
</style>
 <!-- 内容 -->
<template #DialogContainer>
    <div class="dialog_wrap">
        <div class="steps">
            <ul>
                <li v-for="(item, index) in title_list" :key="index">
                    <span ref="spans" :style="{ color: activeStep === index ? '#1987e1' : '#000000' }"
                        @click="jump(index)">
                        <i class="el-icon-thumb"></i>{{ item.title }}
                    </span>
                </li>
            </ul>
        </div>

        <div class="result" @scroll="onScroll">
            <div class="scroll-item"><span>第一项项</span></div>
            <div class="scroll-item"><span>第二项项</span></div>
            <div class="scroll-item"><span>第三项项</span></div>
            <div class="scroll-item"><span>第四项项</span></div>
            <div class="scroll-item"><span>第五项项</span></div>
            <div class="scroll-item"><span>第六项项</span></div>
        </div>
    </div>
</template>

<script setup>
let activeStep = 0
let title_list = [
    { title: '第一项项' },
    { title: '第二项项' },
    { title: '第三项项' },
    { title: '第四项项' },
    { title: '第五项项' },
    { title: '第六项项' },
]

const jump = (index) => {
    var items = document.querySelectorAll(".scroll-item");
    for (var i = 0; i < items.length; i++) {
        if (index === i) {
            items[i].scrollIntoView({
                block: "start",//默认跳转到顶部
                behavior: "smooth"//滚动的速度
            });
        }
    }
}
const onScroll = (e) => {
    let scrollItems = document.querySelectorAll(".scroll-item");
    for (let i = scrollItems.length - 1; i >= 0; i--) {
        // 判断滚动条滚动距离是否大于当前滚动项可滚动距离
        let judge =
            e.target.scrollTop >= scrollItems[i].offsetTop - scrollItems[0].offsetTop;
        if (judge) {
            activeStep = i;
            break;
        }
    }
}
</script>

<style lang="scss" scoped>
.el-dialog {
  overflow: hidden;

  .steps {
    background-color: #fff;
    max-height: calc(-16px + 100vh);
    position: fixed;
    width: 98px;
    top: 90px;
    right: 2%;

    ul {
      list-style: none;
      padding-left: 5px;
      margin: 12px 0;
    }

    li {
      margin: 7px 5px;

      span {
        cursor: pointer;

        &:hover {
          color: #88bcec !important;
        }

        i {
          margin-right: 5px;
        }
      }
    }
  }

  .result {
    position: absolute;
    left: 10px;
    top: 54px;
    bottom: 70px;
    right: 0;
    padding: 0;
    overflow-y: scroll;

    .scroll-item {
      width: 100%;
      height: 500px;
      margin-top: 20px;
      background: rgb(137, 238, 238);

      >span {
        font-size: 20px;
      }

      &:first-child {
        margin-top: 0;
      }

      &:last-child {
        height: 500px;
      }
    }
  }

  .el-dialog__footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
  }
}
</style>

选项卡点击恢复到初始状态

js
const jump = (index, e) => {
    // 侧边栏样式
    let titleDom = document.querySelectorAll(".anchor_wrap ul li")
    titleDom.forEach((item, i) => {
        item.classList.remove('selected')
    })
    if (index && e) {
        e.target.parentNode.classList.add('selected')
    } else {
        titleDom[0].classList.add('selected')
    }

    // 滚动
    let items = document.querySelectorAll(".scroll-item");
    if (!index) {
        items[0].scrollIntoView({
            block: "start", // 默认跳转到顶部
            behavior: "smooth" // 滚动的速度
        });

        return
    }
    for (var i = 0; i < items.length; i++) {
        if (index === i) {
            items[i].scrollIntoView({
                block: "start", // 默认跳转到顶部
                behavior: "smooth" // 滚动的速度
            });
        }
    }
}

const show = (val) => {
    nextTick(() => {
        setTimeout(() => {
            jump()
        }, 300);
    })
};
const jump = (index, e) => {
    // 侧边栏样式
    let titleDom = document.querySelectorAll(".anchor_wrap ul li")
    titleDom.forEach((item, i) => {
        item.classList.remove('selected')
    })
    if (index && e) {
        e.target.parentNode.classList.add('selected')
    } else {
        titleDom[0].classList.add('selected')
    }

    // 滚动
    let items = document.querySelectorAll(".scroll-item");
    if (!index) {
        items[0].scrollIntoView({
            block: "start", // 默认跳转到顶部
            behavior: "smooth" // 滚动的速度
        });

        return
    }
    for (var i = 0; i < items.length; i++) {
        if (index === i) {
            items[i].scrollIntoView({
                block: "start", // 默认跳转到顶部
                behavior: "smooth" // 滚动的速度
            });
        }
    }
}

const show = (val) => {
    nextTick(() => {
        setTimeout(() => {
            jump()
        }, 300);
    })
};

插槽传值

  • 父组件
vue
<template>
    <section class="overview_num_box">
        <h3>{{ title }}</h3>

        <slot :scoreNum="scoreNum"></slot>
    </section>
</template>

<script setup>
const scoreNum = ref(0)
</script>
<template>
    <section class="overview_num_box">
        <h3>{{ title }}</h3>

        <slot :scoreNum="scoreNum"></slot>
    </section>
</template>

<script setup>
const scoreNum = ref(0)
</script>
  • 子组件
js
<overview-num-box :title="'今日平均分'">
    <template v-slot:default="slotProps">
        <p>{{ slotProps.scoreNum }}分</p>
    </template>
</overview-num-box>
<overview-num-box :title="'今日平均分'">
    <template v-slot:default="slotProps">
        <p>{{ slotProps.scoreNum }}分</p>
    </template>
</overview-num-box>

dialog-info

vue
<template>
  <el-dialog :width="dialogWidth" v-model="showDialog" :close-on-click-modal="false" :modal-append-to-body="false"
    :close-on-press-escape="false" @closed="closeDialog">
    <template #header>
      <slot name="DialogTitle"></slot>
    </template>
    <div class="container">
      <slot name="DialogContainer"></slot>
    </div>
    <template #footer v-if="isShowFotter">
      <span class="dialog-footer">
        <slot name="extendBtn"></slot>
        <el-button @click="showDialog = false">关闭</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script setup>
import { ref } from "vue";

// 自定义事件
const emit = defineEmits(["closeDialog"]);

const props = defineProps({
  isShowFotter: {
    type: Boolean,
    default: true,
  },
});

let dialogWidth = '75%'

const showDialog = ref(false);

// 可以通过这个方法动态设置宽度
const setDialogWidth = (width) => {
  dialogWidth = width;
}
const closeDialog = () => {
  // console.log('closeDialog')
  emit('closeDialog')
}

/**
 * 父组件调弹框显示方法
 */
const show = () => {
  showDialog.value = true;
};
const hide = () => {
  showDialog.value = false;
};

defineExpose({ show, hide, setDialogWidth });
</script>
<template>
  <el-dialog :width="dialogWidth" v-model="showDialog" :close-on-click-modal="false" :modal-append-to-body="false"
    :close-on-press-escape="false" @closed="closeDialog">
    <template #header>
      <slot name="DialogTitle"></slot>
    </template>
    <div class="container">
      <slot name="DialogContainer"></slot>
    </div>
    <template #footer v-if="isShowFotter">
      <span class="dialog-footer">
        <slot name="extendBtn"></slot>
        <el-button @click="showDialog = false">关闭</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script setup>
import { ref } from "vue";

// 自定义事件
const emit = defineEmits(["closeDialog"]);

const props = defineProps({
  isShowFotter: {
    type: Boolean,
    default: true,
  },
});

let dialogWidth = '75%'

const showDialog = ref(false);

// 可以通过这个方法动态设置宽度
const setDialogWidth = (width) => {
  dialogWidth = width;
}
const closeDialog = () => {
  // console.log('closeDialog')
  emit('closeDialog')
}

/**
 * 父组件调弹框显示方法
 */
const show = () => {
  showDialog.value = true;
};
const hide = () => {
  showDialog.value = false;
};

defineExpose({ show, hide, setDialogWidth });
</script>
vue
<template>
    <dialog-info ref="refDialogInfo" class="dialog_info_wrap" :isShowFotter="false" @closeDialog="closeDialog">
        <!-- 标题 -->
        <template #DialogTitle>
            <span>{{ title }}</span>
        </template>
        <!-- 内容 -->
        <template #DialogContainer>
            <div class="dialog_wrap">
                <slot></slot>
            </div>
        </template>
    </dialog-info>
</template>

<script setup>
import { ref, nextTick, defineProps, onMounted } from "vue";
import { ElMessage } from "element-plus";

// 自定义事件
const emit = defineEmits(["closeDialog"]);

const props = defineProps({
    title: {
        type: String,
        default: '',
    }
})

const refDialogInfo = ref(null)

/**
 * 业务
 */
const show = (val) => {
    refDialogInfo.value.show();
    // refDialogInfo.value.setDialogWidth('30%');
};
const closeDialog = () => {
    // console.log('关闭');
    emit('closeDialog');
}

defineExpose({
    show,
});
</script>
<template>
    <dialog-info ref="refDialogInfo" class="dialog_info_wrap" :isShowFotter="false" @closeDialog="closeDialog">
        <!-- 标题 -->
        <template #DialogTitle>
            <span>{{ title }}</span>
        </template>
        <!-- 内容 -->
        <template #DialogContainer>
            <div class="dialog_wrap">
                <slot></slot>
            </div>
        </template>
    </dialog-info>
</template>

<script setup>
import { ref, nextTick, defineProps, onMounted } from "vue";
import { ElMessage } from "element-plus";

// 自定义事件
const emit = defineEmits(["closeDialog"]);

const props = defineProps({
    title: {
        type: String,
        default: '',
    }
})

const refDialogInfo = ref(null)

/**
 * 业务
 */
const show = (val) => {
    refDialogInfo.value.show();
    // refDialogInfo.value.setDialogWidth('30%');
};
const closeDialog = () => {
    // console.log('关闭');
    emit('closeDialog');
}

defineExpose({
    show,
});
</script>