当前位置:   article > 正文

Vue - Vue3 封装组件(更新中...)_vue3封装组件

vue3封装组件

1、穿梭框 DataTransfer

  • 示例
    在这里插入图片描述
  • 应用示例
<DataTransfer 
v-model="value" 
	:data="data" 
	:titles="['左侧数据','右侧数据']" 
	:buttonTexts="['To Left', 'To Right']" 
	:search="true"/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 组件代码
<script setup>
import {ArrowLeft, ArrowRight, Search} from "@element-plus/icons-vue";
import {computed, getCurrentInstance, onMounted, ref, watch, watchEffect} from "vue";

const emit = defineEmits(["update:modelValue"])
const props = defineProps({
  data: {
    type: Array,
    default: []
  },
  titles: {
    type: Array,
    default: ['Source', 'Target']
  },
  modelValue: {
    type: Array,
    default: []
  },
  buttonTexts: {
    type: Array,
    default: ['', '']
  },
  search: {
    type: Boolean,
    default: false
  }
})
const {proxy} = getCurrentInstance()

/**目标数据*/
const targetValue = computed({
  get() {
    return props.data
        .filter(item => props.modelValue.includes(item.key))
        .filter(item => {
          if (targetSearchValue.value === '') return true
          return item.label.toLowerCase().includes(targetSearchValue.value.toLowerCase())
        })
        .filter(item => item !== '')
  },
  set(value) {
    emit("update:modelValue", value)
  }
})

/**源数据数据*/
const sourceData = computed(() => {
  if (sourceSearchValue.value !== '') {
    return props.data.filter(item => {
      return item.label.toLowerCase().includes(sourceSearchValue.value.toLowerCase())
    })
  } else {
    return props.data
  }
})

/**左表头统计数据*/
const sourceStatistics = computed(() => `${getSelectData()?.leftSelectData?.length}/${sourceData.value.length}`)

/**右表头统计数据*/
const targetStatistics = computed(() => `${getSelectData()?.rightSelectData?.length}/${targetValue.value.length}`)

const allData = ref()
const selectedData = ref()
const leftDisabled = ref(true)
const rightDisabled = ref(true)
const sourceSearchValue = ref('')
const targetSearchValue = ref('')

/**获取选择行数据
 * @return {Object}
 * */
const getSelectData = () => {
  const leftSelectData = allData.value?.getSelectionRows()
  const rightSelectData = selectedData.value?.getSelectionRows()
  return {leftSelectData, rightSelectData}
}

/**控制按钮禁用状态*/
watchEffect(() => {
  leftDisabled.value = getSelectData()?.rightSelectData?.length === 0
  rightDisabled.value = getSelectData()?.leftSelectData?.length === 0
      || getSelectData()?.leftSelectData?.filter(item => !props.modelValue.includes(item.key))?.length === 0
})


/**监控Target、Source数据,控制Source数据选中状态*/
watch([targetValue, sourceData], () => {
  Promise.resolve(1).then(() => {
    checkData()
  })
})

/**当页面初始加载时控制数据选中状态*/
onMounted(() => {
  // 禁止文字选中
  document.onselectstart = function () {
    return false
  }
  checkData()
})

/**选中Source当中与Target对应的数据*/
const checkData = () => {
  sourceData.value.filter(item => props.modelValue.includes(item.key)).forEach(i => {
    allData.value?.toggleRowSelection(i, true)
  })
}

/**Target 减少数据*/
const toLeft = () => {
  getSelectData().rightSelectData.map(item => item.key).forEach(i => {
    const _index = props.modelValue.indexOf(i)
    if (_index !== -1) props.modelValue.splice(_index, 1)
  })
}

/**Target 增加数据*/
const toRight = () => {
  getSelectData().leftSelectData.filter(item => !props.modelValue.includes(item.key)).forEach(i => {
    props.modelValue.push(i.key)
  })
  rightDisabled.value = true
}

/**根据Target数据判断Source对应数据是否可选中*/
const canSelectable = (row) => {
  return !props.modelValue.includes(row.key)
}

/**Source 行点击事件*/
const leftSourceRowClick = (row) => {
  if (!props.modelValue.includes(row.key)) {
    allData.value?.toggleRowSelection(row, undefined)
  }
}

/**Target 行点击事件*/
const rightTargetRowClick = (row) => {
  selectedData.value?.toggleRowSelection(row, undefined)
}

</script>

<template>
  <div class="transfer-area">
    <div class="source-area">
      <el-table ref="allData" :data="sourceData" @row-click="leftSourceRowClick" cell-mouse-enter="hover">
        <el-table-column type="selection" :selectable="canSelectable"/>
        <el-table-column>
          <template #header>
            <div class="table-head">
              <div>{{ titles[0] }}</div>
              <div class="statistics">{{ sourceStatistics }}</div>
            </div>
          </template>
          <template #default="scope"><p class="change-style">{{ scope.row.label }}</p></template>
        </el-table-column>
      </el-table>
      <el-input v-show="search" v-model="sourceSearchValue" class="input-with-select" placeholder="输入以查询"
                :prefix-icon="Search"
                clearable/>
    </div>

    <el-button type="primary" @click="toLeft" :disabled="leftDisabled">
      <el-icon class="el-icon--left">
        <ArrowLeft/>
      </el-icon>
      {{ buttonTexts[0] }}
    </el-button>
    <el-button type="primary" @click="toRight" :disabled="rightDisabled">
      {{ buttonTexts[1] }}
      <el-icon class="el-icon--right">
        <ArrowRight/>
      </el-icon>
    </el-button>

    <div class="target-area">
      <el-table ref="selectedData" :data="targetValue" @row-click="rightTargetRowClick">
        <el-table-column type="selection"/>
        <el-table-column>
          <template #header>
            <div class="table-head">
              <div>{{ titles[1] }}</div>
              <div class="statistics">{{ targetStatistics }}</div>
            </div>
          </template>
          <template #default="scope"><p class="change-style">{{ scope.row.label }}</p></template>
        </el-table-column>
      </el-table>
      <el-input v-show="search" v-model="targetSearchValue" class="input-with-select" placeholder="输入以查询"
                :prefix-icon="Search"
                clearable/>
    </div>
  </div>
</template>

<style scoped lang="less">
.transfer-area {
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
}

.source-area, .target-area {
  width: 30%;
}

.el-table {
  --el-table-border: none;
  border: #E6E8EB 1px solid;
  border-radius: 7px;
  width: 100%;
  height: 60vh;
}

.el-table::v-deep(th.el-table__cell) {
  background: #F5F7FA;
}

.el-table::v-deep(.el-table__header-wrapper) {
  background: #F5F7FA;
  border-bottom: #E6E8EB 1px solid;
}

.el-table::v-deep(.el-table__inner-wrapper::before) {
  background: none;
}

.input-with-select {
  margin: 5px 0 5px 0;
}

.change-style {
  cursor: pointer;
  padding: 0;
  margin: 0;
}

.change-style:hover {
  color: #409EFF;
}

.table-head {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}

.statistics {
  color: #409EFF;
  font-size: smaller;
  font-weight: lighter;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257

2、按钮组

  • 示例
    在这里插入图片描述
    在这里插入图片描述
  • 应用示例
<template>
<ButtonGroup :buttonArray="Buttons"/>
</template>
<script setup>
import {getCurrentInstance, ref} from "vue";
const {proxy} = getCurrentInstance()
const Buttons=ref([
	{
		name: '文件管理',
		    type: "primary",
		    plain: true,
		    round: false,
		    circle: false,
		    color: "#cb966a",
		    data: [
			    {
			    	text: '下载文件',
                    icon: 'el-icon-download',
                    onClick: () => {
                    	proxy.$message.warrning('下载文件')
					}
			    }{
			    	text: '删除文件',
                    icon: 'el-icon-delete',
                    onClick: () => {
                    	proxy.$message.warrning('删除文件')
					}
				}
		    ]
	}
])
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 组件代码
<script setup>
import {ArrowDown} from "@element-plus/icons-vue";

const props = defineProps({
  buttonArray: {
    type: Array,
    default: []
  },
  style: {
    type: String,
    default: "margin-left:10px;"
  }
})

function onClick(callback) {
  callback()
}
</script>

<template>
  <el-dropdown size="small" v-for="buttonGroup in props.buttonArray" :style="props.style">
    <el-button :type="buttonGroup.type" :plain="buttonGroup.plain" :round="buttonGroup.round"
               :circle="buttonGroup.circle" :color="buttonGroup.color">{{ buttonGroup.name }}
      <el-icon class="el-icon--right">
        <arrow-down/>
      </el-icon>
    </el-button>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item v-for="button in buttonGroup.data">
          <div @click="onClick(button.onClick)">
            <i :class="button.icon"></i>
            {{ button.text }}
          </div>
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>

</template>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

3、顶部导航菜单

  • 示例
    在这里插入图片描述
  • 应用示例
<template>
	<HeaderMenu :menus="menus" class="header-class"/>
</template>
<script setup>
import {ref} from "vue";
const menus = ref([
      {
        text: '头部导航栏',
        icon:'el-icon-s-order',
        items: [
          {
            itemText: '选项一', onClick: () => toBacklogWorkFlow()
          }
        ]
      }
    ])
</script>
<style lang="less" scoped>
.header-class:hover{
  background: #1a81ea;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 组件代码
<script setup>
import {ArrowDown} from "@element-plus/icons-vue";

const props = defineProps({
  menus: {
    type: Array,
    default: []
  },
  menuType: {
    type: String,
    default: "lineMenu"
  },
  style:{
    type: String,
    default: ""
  }
})
const itemClick = (onClick) => {
  onClick()
}
</script>

<template>
  <div v-if="menuType==='lineMenu'">
    <el-dropdown v-for="menu in props.menus" class="menu-class" :class="props.class" :style="props.style">
      <span>
        <i :class="menu.icon"></i>
        {{ menu.text }}
        <el-icon class="el-icon--right">
          <arrow-down/>
        </el-icon>
      </span>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item v-for="item in menu.items" @click="itemClick(item.onClick)">{{ item.itemText }}
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
  <div v-else-if="menuType==='buttonMenu'">
    <el-dropdown v-for="menu in props.menus">
      <el-button :type="menu.buttonType">
        {{ menu.text }}
        <el-icon class="el-icon--right">
          <arrow-down/>
        </el-icon>
      </el-button>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item v-for="item in menu.items" @click="itemClick(item.onClick)">{{ item.text }}
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</template>

<style scoped lang="less">
.menu-class {
  color: white;
  height: 100%;
  line-height: 59px;
  padding: 0 20px;
  font-size: 16px;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

4、弹窗组件

  • 示例
    在这里插入图片描述

  • 应用示例

<Dialog v-model="regExpModel" :lazy="true" title="图标" :width="1000" :mask="true" :draggable="false"
          icon="Warning" :footer="true" :on-close="onClose" @open="open">
    <div>
      123
    </div>
    <template #footer>
      <el-button type="primary">测试</el-button>
    </template>
  </Dialog>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 组件代码
<script setup>
import {computed, onMounted, ref} from "vue";
import {FullScreen} from "@element-plus/icons-vue";

const emit = defineEmits(["update:modelValue", "open"])

const props = defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },
  width: {
    type: Number,
    default: 500
  },
  height: {
    type: Number,
    default: 500
  },
  openDelay: {
    type: Number,
    default: 0
  },
  title: {
    type: String,
    default: 'Title'
  },
  icon: {
    type: String,
    default: 'Warning'
  },
  model: {
    type: Boolean,
    default: true
  },
  footer: {
    type: Boolean,
    default: false
  },
  draggable: {
    type: Boolean,
    default: false
  },
  onClose: {
    type: Function,
    default: () => {
    }
  }
})
const dialogModel = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit("update:modelValue", value)
  }
})
const top = ref("15vh")
const clientHeight = document.body.clientHeight
const fullscreen = ref(false)
const handleFullScreen = () => {
  fullscreen.value = !fullscreen.value
}
const open = () => {
  emit("open")
}
const close = () => {
  emit("close")
}
const handleClose = () => {
  props.onClose()
  emit("update:modelValue", false)
}
const calcHeight = () => {
  return (clientHeight - props.height - 100) / 2 + 'px';
}

onMounted(() => {
  top.value = calcHeight()
})


</script>

<template>
  <div class="dialog">
    <el-dialog v-model="dialogModel" :width="width" :model="model" :fullscreen="fullscreen" :draggable="draggable"
               :open-delay="openDelay" :before-close="handleClose" @open="open" :top="top">
      <template #header>
        <div class="header flex">
          <div class="header-left flex">
            <el-icon>
              <component :is="icon"/>
            </el-icon>
            <span class="title">{{ title }}</span>
          </div>
          <div class="header-right flex">
            <el-icon>
              <FullScreen @click="handleFullScreen"/>
            </el-icon>
          </div>
        </div>
      </template>
      <el-scrollbar class="dialog-content" :max-height="height">
        <slot/>
      </el-scrollbar>
      <template #footer>
        <div class="footer flex" v-if="footer">
          <slot name="footer"/>
        </div>
      </template>
    </el-dialog>
  </div>

</template>

<style scoped>
.header {
  justify-content: space-between;

  .title {
    margin-left: 5px;
    line-height: 20px;
  }
}

.flex {
  display: flex;
  flex-direction: row;
  align-items: center;
}

.header-left {
  justify-content: flex-start;
}

.header-right {
  cursor: pointer;
  color: #909399;
}
.header-right:hover{
  color: #409EFF;
}

.footer {
  border-top: 1px solid #EBEEF5;
  justify-content: flex-end;
  padding: 10px;
}

.dialog :deep(.el-dialog) {
  padding: 0;
  border-radius: 9px;
}

.dialog :deep(.el-dialog__header) {
  padding: 12px 50px 12px 10px;
  border-bottom: 1px solid #EBEEF5;
}

.dialog :deep(.el-dialog__footer) {
  padding: 0;
}

.dialog-content {
  padding: 10px;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168

5、特色按钮

  • 示例1
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 应用示例1
<ColorButton type="1" @click="" prefix_icon="el-icon-info">
  HOVER ME
</ColorButton>
<ColorButton type="2" @click="" prefix_icon="el-icon-info">
  HOVER ME
</ColorButton>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 示例2
    在这里插入图片描述

在这里插入图片描述

  • 应用示例2
<ColorButton type="1" @click="">
  <template #visible>
    HOVER ME
  </template>
  <template #invisible>
    EXPORT
    <i class="el-icon-top-right"></i>
  </template>
</ColorButton>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 组件代码
<script setup>
import {computed} from "vue";

defineEmits(['click'])
const props = defineProps({
  type: {
    type: String,
    default: '1'
  },
  suffix_icon: {
    type: String,
    default: ''
  },
  prefix_icon: {
    type: String,
    default: ''
  },
  doubleSided: {
    type: Boolean,
    default: false
  },
  round: {
    type: Boolean,
    default: false
  },
  circle: {
    type: Boolean,
    default: false
  }
})
const borderType = computed(() => {
  if (props.round) return 'round'
  if (props.circle) return 'circle'
  return 'normal'
})
</script>

<template>
  <div :class="['border-'+borderType]">
    <button v-if="type==='1'" class="button_1" @click="$emit('click')">
      <i v-if="prefix_icon" :class="prefix_icon" class="prefix_i"></i>
      <slot/>
      <i v-if="suffix_icon" :class="suffix_icon" class="suffix_i"></i>
      <span v-if="doubleSided" class="button_1__visible">
        <slot name="visible"/>
      </span>
      <span v-if="doubleSided" class="button_1__invisible">
        <div>
          <slot name="invisible"/>
        </div>
      </span>
    </button>

    <button v-if="type==='2'" class="button_2" @click="$emit('click')">
      <i v-if="prefix_icon" :class="prefix_icon" class="prefix_i"></i>
      <slot/>
      <i v-if="suffix_icon" :class="suffix_icon" class="suffix_i"></i>
      <div class="hoverEffect">
        <div></div>
      </div>
    </button>
  </div>
</template>

<style scoped lang="less">
.prefix_i {
  margin-right: 5px;
}

.suffix_i {
  margin-left: 5px;
}

.border-round {
  --border: 10rem;

  .button_1 {
    border-radius: var(--border);
  }

  .button_2 {
    border-radius: var(--border);
  }

  .button_1:before {
    border-radius: var(--border);
  }
}

.border-normal {
  --border: 5px;

  .button_1 {
    border-radius: var(--border);
  }

  .button_2 {
    border-radius: var(--border);
  }

  .button_1:before {
    border-radius: var(--border);
  }
}

.border-circle {
  --border: 50%;
  --size: 20px;
  --padding: 15px;

  .button_1 {
    border-radius: var(--border);
    padding: var(--padding);
    width: var(--size);
    height: var(--size);
  }

  .button_2 {
    border-radius: var(--border);
    padding: var(--padding);
    width: var(--size);
    height: var(--size);
  }

  .button_1:before {
    border-radius: var(--border);
    padding: var(--padding);
  }
}

// region button_1
.button_1 {
  text-decoration: none;
  position: relative;
  border: none;
  font-family: inherit;
  color: #fff;
  padding: 3px 20px 3px 20px;
  margin: 3px;
  text-align: center;
  background: linear-gradient(90deg, #03a9f4, #f441a5, #ffeb3b, #03a9f4);
  background-size: 300%;
  border-radius: 5px;
  z-index: 1;
}

.button_1 > * {
  display: inline-block;
  transition: all ease-in-out .5s;
}

.button_1__visible {
  text-align: center;
}

.button_1__invisible {
  width: 100%;
  margin: auto;
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  left: 0;
  top: -200%;
}

.button_1:hover {
  animation: ani 8s linear infinite;
  border: none;
  opacity: 0.8;
}

.button_1:hover .button_1__visible {
  transform: translateY(200%);
  opacity: 0;
}

.button_1:hover .button_1__invisible {
  top: 0;
  bottom: 0;
}

.button_1:focus {
  outline: none;
}

@keyframes ani {
  0% {
    background-position: 0;
  }

  100% {
    background-position: 400%;
  }
}

.button_1:before {
  content: '';
  position: absolute;
  top: -5px;
  left: -5px;
  right: -5px;
  bottom: -5px;
  z-index: -1;
  background: linear-gradient(90deg, #03a9f4, #f441a5, #ffeb3b, #03a9f4);
  background-size: 400%;
  border-radius: 5px;
  transition: 1s;
}

.button_1:hover::before {
  filter: blur(10px);
}

.button_1:active {
  background: linear-gradient(32deg, #03a9f4, #f441a5, #ffeb3b, #03a9f4);
}

// endregion

// region button_2
.button_2 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 20px;
  border: 0;
  position: relative;
  overflow: hidden;
  border-radius: 10rem;
  transition: all 0.02s;
  font-weight: bold;
  color: rgb(37, 37, 37);
  z-index: 0;
  box-shadow: 0 0 7px -5px rgba(0, 0, 0, 0.5);
}

.button_2:hover {
  background: rgb(193, 228, 248);
  color: rgb(33, 0, 85);
}

.button_2:active {
  transform: scale(0.97);
}

.hoverEffect {
  position: absolute;
  bottom: 0;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1;
}

.hoverEffect div {
  background: rgb(222, 0, 75);
  background: linear-gradient(90deg, rgba(222, 0, 75, 1) 0%, rgba(191, 70, 255, 1) 49%, rgba(0, 212, 255, 1) 100%);
  border-radius: 40rem;
  width: 10rem;
  height: 10rem;
  transition: 0.4s;
  filter: blur(20px);
  animation: effect infinite 3s linear;
  opacity: 0.5;
}

.button_2:hover .hoverEffect div {
  width: 8rem;
  height: 8rem;
}

@keyframes effect {

  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

// endregion
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288

5、PDF Word Excel 预览插件

  • 插件安装
npm install vue-demi vue-demi @vue-office/docx @vue-office/excel @vue-office/pdf
  • 1

@vue-office插件安装方式详见博客Vue-Vue 集成 pdf word excel 预览功能

  • 应用示例
<script setup>
import DocumentPreview from "@/components/DocumentPreview.vue";
import {ref} from "vue";
import {ElMessage} from "element-plus";

// 文件路径
const url = 'http://XXXXX.docx'

function onError(e) {
  ElMessage.warning(e)
}

function onRendered() {
  console.log('预览成功')
}

</script>

<template>
  <DocumentPreview :url="url" @onError="onError" @onRendered="onRendered"/>
</template>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 组件代码
<script setup>
import VueOfficePdf from "@vue-office/pdf";
import VueOfficeExcel from "@vue-office/excel";
import VueOfficeDocx from "@vue-office/docx";
import {computed, getCurrentInstance, shallowRef} from "vue";
import * as url from "url";

const emit = defineEmits(['onRendered', 'onError'])
const props = defineProps({
  url: {
    type: String,
    default: ''
  }
})
const {proxy} = getCurrentInstance()
const componentMap = shallowRef({
  pdf: VueOfficePdf,
  xlsx: VueOfficeExcel,
  docx: VueOfficeDocx
})
const type = computed(() => {
  const documentType = props.url.split('.').pop()
  if (!componentMap.value[documentType]) {
    proxy.$message.warning('仅支持 pdf、xlsx、docx 格式的文件预览')
    return
  }
  return documentType
})

/** 文件预览成功时调用 * */
const onRendered = () => {
  emit('onRendered')
}

/** 文件预览失败时调用 **/
const onError = (e) => {
  emit('onError', e)
}

</script>

<template>
  <component :is="componentMap[type]" :src="url" @rendered="onRendered" @error="onError"/>
</template>

<style scoped lang="less">
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

6、编辑器

  • 示例
    在这里插入图片描述
  • 插件安装
npm install @wangeditor/editor  @wangeditor/editor-for-vue@next --save
  • 1

wangEditor 插件安装方式详见博客 Vue-Vue3 集成编辑器功能

-应用示例

<script setup>
import Editor from "@/components/Editor.vue";
import {ref} from "vue";

const editor = ref()
const editorValue = ref('')

const print = () => {
  console.log(editor.value.getHtml())
  console.log(editor.value.getText().split(/\n/))
}
</script>

<template>
  <el-button type="primary" @click="print">测试</el-button>
  <Editor ref="editor" v-model="editorValue" placeholder="请输入内容..."/>
</template>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 组件代码
<script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import {DomEditor} from '@wangeditor/editor'
import {computed, onBeforeUnmount, ref, shallowRef} from 'vue'
import {Editor, Toolbar} from '@wangeditor/editor-for-vue'

const emit = defineEmits(["update:modelValue"])
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  },
  placeholder: {
    type: String,
    default: '请输入...'
  }
})
const inputValue = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit("update:modelValue", value)
  }
})
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
const mode = ref('default')
const test = ref(false)
const editorConfig = {placeholder: props.placeholder}
// 默认工具栏配置
const toolbarConfig = {}

/** 排除菜单组,写菜单组 key 的值即可 */
toolbarConfig.excludeKeys = [
  'group-image',
  'group-video',
  'fullScreen'
]

/** 组件销毁时,也及时销毁编辑器 */
onBeforeUnmount(() => {
  const editor = editorRef.value
  if (editor == null) return
  editor.destroy()
})

/** 记录 editor 实例,重要!*/
const handleCreated = (editor) => {
  editorRef.value = editor
}

/** 获取HTML格式内容方法 */
const getHtml = () => {
  return editorRef.value.getHtml()
}

/** 获取原始文本内容方法 */
const getText = () => {
  return editorRef.value.getText()
}

/** 暴露方法 */
defineExpose({getHtml, getText})
</script>

<template>
  <div style="border: 1px solid #ccc">
    <Toolbar
        style="border-bottom: 1px solid #ccc"
        :editor="editorRef"
        :defaultConfig="toolbarConfig"
        :mode="mode"
    />
    <Editor
        style="height: 500px; overflow-y: hidden;"
        v-model="inputValue"
        :defaultConfig="editorConfig"
        :mode="mode"
        @onCreated="handleCreated"
    />
  </div>
</template>

<style scoped lang="less">
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

7、鼠标景深效果

这次封装一个高级的东西,很炫,哈哈哈哈哈哈哈
请看示例:

  • 示例
    在这里插入图片描述
    在这里插入图片描述
    鼠标放上去有景深变换效果

上代码!!!!!!

  • 应用示例
<template>
  <HoverModel>
    <a-card class="card">
      <div class="img-container">
        <a-image width="100%" :height="200" :src="url"/>
      </div>
      <div class="operation-area">
        <div class="img-name"><span>{{ name }}</span></div>
        <div class="operation-bottom">
          <div>
            <CloudTwoTone/>
            <span class="file-dir">{{ directory }}</span>
          </div>
          <DeleteOutlined class="delete" @click="deleteImage"/>
        </div>
      </div>
    </a-card>
  </HoverModel>
</template>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

组件为<HoverModel>,将你自己的组件放入<HoverModel></HoverModel>即可实现效果
(注:a-card是我自己的内容,请忽略)

  • 组件代码
<template>
  <div ref="cardRef" class="model-container" @mousemove="onMouseMove" @mouseout="onMouseLeave">
    <slot/>
  </div>
</template>

<script setup lang="ts">
import useSetCardHover from './hooks/useSetModelHover';
import {ref} from 'vue';

const cardRef = ref()
const {onMouseMove, onMouseLeave} = useSetCardHover(cardRef)
</script>

<style scoped lang="less">
.model-container {
  width: 100%;
  overflow: hidden;
  border-radius: 10px;
  box-shadow: 0 0 15px #d1d8e28e;
  transform: perspective(500px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: 0.3s;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
export default function (cardRef) {
    const yRange = [-10, 10]
    const xRange = [-10, 10]

    function getRotateDeg(range: number[], value: number, length: number) {
        return value / length * (range[1] - range[0]) + range[0]
    }

    function onMouseMove(e: any) {
        const {offsetX, offsetY} = e
        const {offsetWidth, offsetHeight} = cardRef.value
        const ry = getRotateDeg(yRange, offsetX, offsetWidth)
        const rx = -getRotateDeg(xRange, offsetY, offsetHeight)
        cardRef.value.style.setProperty('--rx', `${rx}deg`)
        cardRef.value.style.setProperty('--ry', `${ry}deg`)
    }

    function onMouseLeave() {
        cardRef.value.style.setProperty('--rx', 0)
        cardRef.value.style.setProperty('--ry', 0)
    }

    return {onMouseMove, onMouseLeave}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

别问我为什么把 js 提取出来这么写,因为显得高级 !
在这里插入图片描述

闲得蛋疼,再来一个,高级的!!!!!!!!!!!!!!!!!!!!!

8、滚动缓慢载入滚动效果

这次是一个指令,效果自己试,我懒得录屏
在这里插入图片描述

  • 应用示例
<template>
  <div class="img-container">
    <div class="img-item">
      <slot/>
    </div>
    <div v-slide-in v-for="img in imgs" :key="img.id" class="img-item">
      <CardImage v-bind="img" @delete="onDelete"/>
    </div>
  </div>
</template>
<script setup lang="ts">
import type {PropType} from 'vue';
import vSlideIn from '/@/utils/vSlideIn'// 重点是这一句引入
import {CardImage} from '/@/components/CardImage';
import {imgType} from '/@/components/CardImage/src/types';
import useHandleIngForm from './hooks/useHandleIngForm';
import {computed} from "vue";

const props = defineProps({
  images: {
    type: Array as PropType<imgType[]>,
    default: []
  }
})

const imgs = computed(() => {
  console.log('----=----=-=', props.images)
  return props.images
})

const {onDelete} = useHandleIngForm(imgs)

</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 指令代码
const DISTANCE = 100
const DURATION = 500
const map = new WeakMap()
const ob = new IntersectionObserver(entries => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      const animation = map.get(entry.target)
      animation && animation.play()
      ob.unobserve(entry.target)
    }
  }
})

function isBelowViewport(el) {
  const rect = el.getBoundingClientRect()
  return rect.top - window.innerHeight > 0
}

export default {
  mounted(el, bindings) {
    if (!isBelowViewport(el)) {
      return
    }
    const animation = el.animate(
      [
        {
          transform: `translateY(${DISTANCE}px)`,
          opacity: 0.5
        },
        {
          transform: `translateY(0)`,
          opacity: 1
        }
      ],
      {
        duration: DURATION,
        easing: 'cubic-bezier(0,0,0,1)', // 'ease-out' | 'cubic-bezier(0,0,0,1)'
        fill: 'forwards'
      }
    )
    animation.pause()
    map.set(el, animation)
    ob.observe(el)
  },
  unmounted(el) {
    ob.unobserve(el)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/701600
推荐阅读
相关标签
  

闽ICP备14008679号