<DataTransfer
v-model="value"
:data="data"
:titles="['左侧数据','右侧数据']"
:buttonTexts="['To Left', 'To Right']"
:search="true"/>
<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>
<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>
<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>
<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>
<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>
<DialogBox :lazy="true" v-model="activityPop" title="测试" :width="1000" :draggable="false"
:padding="5" icon="el-icon-user">
<InputArea v-model="person.name" @change="changePerson" title="人员选择">
123
InputArea>
<template #footer>
<div>
<el-button type="warning" size="small" class="el-icon-check" @click="change">改变姓名el-button>
<el-button type="primary" size="small" class="el-icon-close" @click="close">关闭el-button>
div>
template>
DialogBox >
<template>
<div class="dialog-area">
<el-dialog v-model="vmodel" :close-on-click-modal="false" :close-on-press-escape="false" :width="width"
:fullscreen="fullscreen" :draggable="draggable" :modal="modal" :before-close="handleClose">
<template #header>
<i :class="icon">i> {{ title }}
<button class="el-dialog__headerbtn" type="button" style="right: 35px; color: var(--el-color-info)" @click="handleFullScreen">
<i class="el-icon el-icon-full-screen">i>
button>
template>
<el-scrollbar :max-height="contentHeight">
<div v-if="inited" style="min-height: 50px;" class="srcoll-content" :style="{ padding: padding + 'px' }">
<slot name="content">slot>
<slot>slot>
div>
el-scrollbar>
<template #footer>
<div class="dia-footer" v-if="footer">
<slot name="footer">slot>
<el-button type="primary" v-if="!footer" size="mini" @click="handleClose()"><i
class="el-icon-close">i>关闭el-button>
div>
template>
el-dialog>
div>
template>
<script>
import { defineComponent, ref, watch, watchEffect } from 'vue';
export default defineComponent({
props: {
modelValue: false,
lazy: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: "el-icon-warning-outline",
},
title: {
type: String,
default: "",
},
height: {
type: Number,
default: 200,
},
width: {
type: Number,
default: 650,
},
padding: {
type: Number,
default: 16,
},
modal: {
/遮罩层
type: Boolean,
default: true,
},
draggable: {
//可拖拽功能
type: Boolean,
default: false,
},
mask: {
type: Boolean,
default: true,
},
onModelClose: {
type: Function,
default: (iconClick) => {
return true;
}
},
footer:{ //是否显示底部按钮
type: Boolean,
default: true
}
},
setup(props, context) {
const clientHeight = document.body.clientHeight * 0.95 - 60;
const inited = ref(true);
const vmodel = ref(false);
const footer = ref(false);
const top = ref(100);
vmodel.value = props.modelValue;
footer.value = !!context.slots.footer;
const contentHeight = ref(200);
contentHeight.value = props.height;
const handleClose = (done, iconClose) => {
let result = props.onModelClose(!!iconClose);
if (result === false) return;
vmodel.value = false;
context.emit("update:modelValue", false);
done && done();
};
const calcHeight = (val) => {
contentHeight.value = clientHeight - 30;
return clientHeight / -2 + 'px';
};
top.value = calcHeight();
watch(
() => props.modelValue,
(newVal, oldVal) => {
vmodel.value = newVal;
}
);
watch(
() => props.height,
(newVal, oldVal) => {
top.value = calcHeight();
}
);
const fullscreen=ref(false);
const handleFullScreen=()=> {
fullscreen.value = !fullscreen.value;
context.emit("fullscreen", fullscreen.value);
}
return {
handleClose,
inited,
vmodel,
footer,
top,
calcHeight,
contentHeight,
fullscreen,
handleFullScreen
};
}
});
script>
<style lang="less" scoped>
.dia-footer {
text-align: right;
width: 100%;
border-top: 1px solid #e2e2e2;
text-align: right;
padding: 6px 8px;
}
style>
<style scoped lang="less">
.dialog-area ::v-deep(.el-overlay-dialog) {
display: flex !important;
}
.dialog-area ::v-deep(.el-dialog) {
margin: auto;
}
.dialog-area ::v-deep(.el-dialog) {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.dialog-area ::v-deep(.el-dialog__header) {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 0px 13px;
line-height: 53px;
border-bottom: 1px solid #e6e6e6;
height: 50px;
color: rgb(79, 79, 79);
font-weight: bold;
font-size: 14px;
margin: 0;
}
.dialog-area ::v-deep(.el-dialog__footer),
.dialog-area ::v-deep(.el-dialog__body) {
padding: 0;
}
.dialog-area ::v-deep(.el-dialog__headerbtn) {
top: 0;
padding-top: 8px;
height: 50px;
width: 0;
padding-right: 30px;
padding-left: 5px;
}
style>
<ColorButton type="1" @click="" prefix_icon="el-icon-info">
HOVER ME
ColorButton>
<ColorButton type="2" @click="" prefix_icon="el-icon-info">
HOVER ME
ColorButton>
<ColorButton type="1" @click="">
<template #visible>
HOVER ME
template>
<template #invisible>
EXPORT
<i class="el-icon-top-right">i>
template>
ColorButton>
<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>
npm install vue-demi vue-demi @vue-office/docx @vue-office/excel @vue-office/pdf
@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>
<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>
npm install @wangeditor/editor @wangeditor/editor-for-vue@next --save
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>
<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>