Vue3父子组件
图
Vue中组件的概念让项目耦合度更低,在复用性上得到了更好的提上,彻底理解组件及其组件间的通信问题,能更好的处理复杂问题,能更好的较少重复性劳动,例如文件的上传、表格的处理封装为组件等

父组件向子组件传递数据

定义子组件:

<template>
  <h1>{{ msg }}</h1>
</template>

<script setup>
defineProps({  // 使用defineEmits和defineProps不需要导入
  msg: String
})
</script>

注意:defineProps里的数据是只读的,无法进行更改,如需修改需要重新指定

const props = defineProps({
  imageUrl: String
})

let imgUrl = ref(props.imageUrl)

// 图片上传成功
function imageUploadSuccess(res) {
  imgUrl.value = res.url
}

在父组件中使用:

<template>
  <HelloWorld msg="Hello Vue 3 + Vite" />
</template>

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

子组件向父组件传值

子组件无法直接修改父组件的变量,只能利用通知父组件,让父组件协助修改相应的值

子组件:

<template>
  <button @click="$emit('changeContentEmit', '内容已发生更改')">更改内容</button>
</template>

<script setup>
defineEmits(['changeContentEmit']) // 使用defineEmits和defineProps不需要导入
</script>

父组件:

<template>
  <h1>{{ content }}</h1>
  <HelloWorld @changeContentEmit="changeContent" />
</template>

<script setup>
import { ref } from 'vue';
import HelloWorld from './components/HelloWorld.vue'

let content = ref('这里是内容')
function changeContent(value) {
  content.value = value
}
</script>

在方法中调用

const emit = defineEmits(['uploadSuccess'])

// 图片上传成功
function imageUploadSuccess(res) {
  imgUrl.value = res.url
  // 通知父组件
  emit('uploadSuccess', imgUrl)
  loadClose()
}

单图片上传示例

<!-- 单图片上传 -->
<template>
  <div>
    <el-upload
        class="avatar-uploader"
        :action="api.file_upload"
        :show-file-list="false"
        :on-success="imageUploadSuccess"
        :before-upload="imageUploadBefore">
      <img v-if="imgUrl" :src="imgUrl" class="avatar"/>
      <el-icon v-else class="avatar-uploader-icon">
        <Plus/>
      </el-icon>
    </el-upload>
  </div>
</template>

<script setup>
import api from '../../http/api'
import {loadClose, loadOpen} from '../../utils/elementUtil'
import {ref} from 'vue'

const props = defineProps({
  imageUrl: String
})

// 用变量接收props的值,便于在下面的代码中进行操作
let imgUrl = ref(props.imageUrl)

// 图片上传前
function imageUploadBefore() {
  loadOpen('上传中')
}

const emit = defineEmits(['uploadSuccess'])

// 图片上传成功
function imageUploadSuccess(res) {
  imgUrl.value = res.url
  emit('uploadSuccess', imgUrl)
  loadClose()
}
</script>

<style scoped>

</style>

父数据改变子组件不变问题

import {ref} from 'vue'

const props = defineProps({
  imageUrl: String
})

// 用变量接收props,便于在下面的代码中进行操作
let imgUrl = ref(props.imageUrl)

// 图片上传成功
function imageUploadSuccess(res) {
  imgUrl.value = res.url
}

父组件向子组件传值不是响应式的,绑定的值在子组件创建后再更不对子组件没有影响。原因是ref是对传入数据的拷贝,原始值的改变并不影响

解决办法:使用roReftoRefs,其本质是对传入数据的引用

// 单个数据引用
let value = toRef(props, 'value')
// 对象引用
let {data} = toRefs(props)

v-model子组件定义

<!--区域级联选择,任意选一个的情况-->
<template>
  <el-cascader
      v-model="mv"
      ref="areaCascadeRef"
      placeholder="请选择地区"
      :options="areaData" :props="{label:'name',value:'code',children:'child',checkStrictly: true}"
      @change="areaCascadeChange(areaCascadeRef)"
      clearable/>
</template>

<script setup>
import {ref, getCurrentInstance, watch} from 'vue'
import areaData from '../../utils/areaData'

const props = defineProps({
  modelValue: {
    type: [Number, String],
    default: '',
  },
})

// 上面模板的值使用该数据
const mv = ref(props.modelValue)

// 该ref用于组件,无关因素
let areaCascadeRef = ref()

// 获取emit
const {emit} = getCurrentInstance()

// 如果父组件传过来的数据是异步获取的,则需要进行监听,解决父数据更改子数据不响应问题
watch(() => props.modelValue, () => {
  mv.value = props.modelValue
})

function areaCascadeChange(refEl) {
  let selectValue = ''
  if (refEl.getCheckedNodes().length > 0) {
    selectValue = refEl.getCheckedNodes()[0].value
  }
  // 通过update更新值
  emit('update:modelValue', selectValue)
  // 选中后关闭
  refEl.togglePopperVisible()
}
</script>

<style scoped>

</style>

v-model常用组件

单图片上传

<!-- 单图片上传 -->
<template>
  <div>
    <el-upload
        class="avatar-uploader"
        :action="api.file_upload"
        :show-file-list="false"
        :on-success="imageUploadSuccess"
        :before-upload="imageUploadBefore">
      <img v-if="mv" :src="mv" class="avatar"/>
      <el-icon v-else class="avatar-uploader-icon">
        <Plus/>
      </el-icon>
    </el-upload>
  </div>
</template>

<script setup>
import {ref, getCurrentInstance, watch} from 'vue'
import api from '../../http/api'
import {loadClose, loadOpen} from '../../utils/elementUtil'

const props = defineProps({
  modelValue: {
    type: String,
    default: '',
  },
})
const mv = ref(props.modelValue)
const {emit} = getCurrentInstance()
watch(() => props.modelValue, () => {
  mv.value = props.modelValue
})

// 图片上传前
function imageUploadBefore() {
  loadOpen('上传中')
}

// 图片上传成功
function imageUploadSuccess(res) {
  emit('update:modelValue', res.data)
  loadClose()
}
</script>

<style scoped>

</style>

Excel解析

需要npm install xlsx支持

<!-- 表格解析 -->
<template>
  <div>
    <el-upload
        action=""
        :file-list="fileList"
        :http-request="uploadHttp">
      <el-button type="primary">请选择上传文件</el-button>
    </el-upload>
  </div>
</template>

<script setup>
import {ref, getCurrentInstance, watch} from 'vue'
import {read, utils} from 'xlsx'

const props = defineProps({
  modelValue: {
    type: Array,
    default: [],
  },
})
const mv = ref(props.modelValue)
const {emit} = getCurrentInstance()
watch(() => props.modelValue, () => {
  mv.value = props.modelValue
})

// 文件列表,在上次第二个时删除第一个,该方式能解决覆盖不刷新问题
let fileList = ref([])

// 上传时读取表格
async function uploadHttp(file) {
  let dataBinary = await readFile(file.file)
  let workBook = read(dataBinary, {type: 'binary', cellDates: true})
  let workSheet = workBook.Sheets[workBook.SheetNames[0]]
  const data = utils.sheet_to_json(workSheet)
  emit('update:modelValue', data)
  // 保留最新的显示
  if (fileList.value.length > 1) {
    fileList.value.splice(0, 1)
  }
}

// 读取文件
const readFile = (file) => {
  return new Promise(resolve => {
    let reader = new FileReader()
    reader.readAsBinaryString(file)
    reader.onload = ev => {
      resolve(ev.target.result)
    }
  })
}
</script>

<style scoped>

</style>

分页组件

<!-- 分页组件 -->
<template>
  <div class="page-container">
    <el-pagination
        v-model:currentPage="pageIndexPro"
        v-model:page-size="pageSizePro"
        :total="countPro"
        @current-change="pageIndexChange"
        @size-change="pageSizeChange"
        layout="total, sizes, prev, pager, next, jumper"
        :page-sizes="[15, 30, 50, 100]"
        background/>
  </div>
</template>

<script setup>
import {ref, getCurrentInstance, watch} from 'vue'

const props = defineProps({
  pageIndex: {
    type: Number,
    default: 1,
  },
  pageSize: {
    type: Number,
    default: 15,
  },
  count: {
    type: Number,
    default: 0,
  },
})

const pageIndexPro = ref(props.pageIndex)
const pageSizePro = ref(props.pageSize)
const countPro = ref(props.count)

const {emit} = getCurrentInstance()

watch(() => props.pageIndex, () => {
  pageIndexPro.value = props.pageIndex
})
watch(() => props.pageSize, () => {
  pageSizePro.value = props.pageSize
})
watch(() => props.count, () => {
  countPro.value = props.count
})

// 分页页码改变
function pageIndexChange(value) {
  emit('update:pageIndex', value)
  emit('change', '')
}

// 分页页数改变
function pageSizeChange(value) {
  emit('update:pageSize', value)
  emit('change', '')
}

</script>

<style scoped>

</style>

使用方式:

<!--分页-->
  <pageComponent
      v-model:pageIndex="listData.req.pageIndex"
      v-model:pageSize="listData.req.pageSize"
      v-model:count="listData.count"
      @change="getList()"/>

地区级联

注意一定要保证value的值唯一,否则控件会出现问题

<!--区域级联选择,任意选一个的情况-->
<template>
  <el-cascader
      v-model="mv"
      ref="areaCascadeRef"
      placeholder="请选择地区"
      :options="areaData" :props="{label:'name',value:'name',children:'child',checkStrictly: true}"
      @change="areaCascadeChange(areaCascadeRef)"
      popper-class="cascade-click"
      clearable/>
</template>

<script setup>
import {ref} from 'vue'
import areaData from './areaAll.json'

let mv = ref()
let areaCascadeRef = ref()
const emit = defineEmits(['changeContentEmit'])

function areaCascadeChange(refEl) {
  let labels = []
  let values = []
  if (refEl.getCheckedNodes().length > 0) {
    labels = refEl.getCheckedNodes()[0].pathLabels
    values = refEl.getCheckedNodes()[0].pathValues
  } else {
    console.log('清空')
  }
  emit('changeContentEmit', labels, values)
}

</script>

<style scoped>
</style>