9、element-plus之70个组件(含ref),表单之dateRange、回车创建标签,el-table之换行、勾选禁止、嵌套展开、搜集选中项、全选、自定义表头筛选查询、倒计时,各种提示区别,表单验证, 自定义组件之图标、季度半年、26小时、添加水印、多级菜单(3400行)
一、element-plus之70个组件 附1、:是Vue的语法糖,用于绑定动态的值 附2、element-ui标签的ref属性 (1)ref="字符串",this.$refs.xxx,可用于访问DOM元素或子组件实例 (2):ref="函数",与element-plus相同 A、当元素或组件挂载到DOM上时,该函数会被调用,并且会将元素或组件的实例作为参数传递给这个函数 B、当元素或组件从DOM中移除时,该函数会再次被调用,此时传递null作为参数 附3、element-plus标签的ref属性 (1)ref="变量",xxx.value,可用于访问DOM元素或子组件实例 (2):ref="函数", A、当元素或组件挂载到DOM上时,该函数会被调用,并且会将元素或组件的实例作为参数传递给这个函数 B、当元素或组件从DOM中移除时,该函数会再次被调用,此时传递null作为参数 /* 组件的四个配置 <div ref="btn" @click="clickButton()">我是一个按钮</div> (1)属性名: (2)事件名:@click,用户-点击-时-触发 (3)方法名:click,使-点击事件-触发,this.$refs.btn.click() (4)插槽名: */ /* 1、Basic 基础 (1)Button 按钮 (2)Border 边框 (3)Color 色彩 (4)Container 布局容器 (5)Icon 图标 (6)Layout 布局 (7)Link 链接 (8)Scrollbar 滚动条 (9)Space 间距 (10)Typography 排版 2、配置 (1)Config Provider 全局配置 3、Form 表单 (1)Autocomplete 自动补全输入框 (2)Cascader 级联选择器 (3)Checkbox 多选框 (4)ColorPicker 取色器(颜色选择器) (5)DatePicker 日期选择器 (6)DateTimePicker 日期时间选择器 (7)Form 表单 (8)Input 输入框 (9)Input Number 数字输入框 (10)Radio 单选框 (11)Rate 评分 (12)Select 选择器 (13)Select V2 虚拟列表选择器 (14)Slider 滑块 (15)Switch 开关 (16)TimePicker 时间选择器 (17)TimeSelect 时间选择 (18)Transfer 穿梭框 (19)Upload 上传 4、Data 数据展示 (1)Avatar 头像 (2)Badge 徽章 (3)Calendar 日历 (4)Card 卡片 (5)Carousel 走马灯 (6)Collapse 折叠面板 (7)Descriptions 描述列表 (8)Empty 空状态 (9)Image 图片 (10)Infinite Scroll 无限滚动 (11)Pagination 分页 (12)Progress 进度条 (13)Result 结果 (14)Skeleton 骨架屏 (15)Table 表格 (16)Virtualized Table 虚拟化表格 (17)Tag 标签 (18)Timeline 时间线 (19)Tree 树形控件 (20)TreeSelect 树形选择 (21)Tree V2 虚拟化树形控件 5、Navigation 导航 (1)Affix 固钉 (2)Backtop 回到顶部 (3)Breadcrumb 面包屑 (4)Dropdown 下拉菜单 (5)Menu 菜单 (6)Page Header 页头 (7)Steps 步骤条 (8)Tabs 标签页 6、Feedback 反馈 (1)Alert 提示 (2)Dialog 对话框 (3)Drawer 抽屉 (4)Loading 加载 (5)Message 消息提示 (6)MessageBox 消息弹框 (7)Notification 通知 (8)Popconfirm 气泡确认框 (9)Popover 弹出框(气泡卡片) (10)Tooltip 文字提示 7、Others 其他 (1)Divider 分割线 */ 1、Basic 基础组件 (1)Button 按钮 <el-button type="primary">Primary</el-button> (2)Border 边框 <template> <table class="demo-border"> <tbody> <tr> <td class="text">Thickness</td> <td class="line">Demo</td> </tr> </tbody> </table> </template> <style scoped> :root { --green:green; } .demo-border .line div { width: 100%; height: 0; border-top: 1px solid var(--green); } </style> (3)Color 色彩 (4)Container 布局容器 <template> <div class="common-layout"> <el-container> <el-aside width="200px">Aside</el-aside> <el-container> <el-header>Header</el-header> <el-main>Main</el-main> </el-container> </el-container> </div> </template> (5)Icon 图标 <el-icon> <Delete /> </el-icon> (6)Layout 布局 <el-row> <el-col :span="12"><div class="grid-content ep-bg-purple" /></el-col> <el-col :span="12"><div class="grid-content ep-bg-purple-light" /></el-col> </el-row> (7)Link 链接 <el-link href="https://element.eleme.io" target="_blank">default</el-link> (8)Scrollbar 滚动条 通过height属性设置滚动条高度,若不设置则根据父容器高度自适应;当元素宽度大于滚动条宽度时,会显示横向滚动条 <el-scrollbar height="400px"> <p v-for="item in 20" :key="item" class="scrollbar-demo-item">{{ item }}</p> </el-scrollbar> (9)Space 间距 <el-space wrap> <el-card v-for="i in 3" :key="i" class="box-card" style="width: 250px"> <template #header> <div class="card-header"> <span>Card name</span> <el-button class="button" text>Operation button</el-button> </div> </template> <div v-for="o in 4" :key="o" class="text item"> {{ 'List item ' + o }} </div> </el-card> </el-space> (10)Typography 排版 <script lang="ts" setup> import { isDark } from '~/composables/dark' </script> <template> <div v-if="!isDark" class="demo-term-box"> <img src="/images/typography/term-helvetica.png" alt="" /> <img src="/images/typography/term-arial.png" alt="" /> </div> <div v-else class="demo-term-box"> <img src="/images/typography/term-helvetica-dark.png" alt="" /> <img src="/images/typography/term-arial-dark.png" alt="" /> </div> </template> 2、配置组件 (1)i18n 配置 <template> <div> <el-button mb-2 @click="toggle">Switch Language</el-button><br /> <el-config-provider :locale="locale"> <el-table mb-1 :data="[]" /> <el-pagination :total="100" /> </el-config-provider> </div> </template> <script lang="ts" setup> import { computed, ref } from 'vue' import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import en from 'element-plus/dist/locale/en.mjs' const language = ref('zh-cn') const locale = computed(() => (language.value === 'zh-cn' ? zhCn : en)) const toggle = () => { language.value = language.value === 'zh-cn' ? 'en' : 'zh-cn' } </script> (2)对按钮进行配置 <template> <div> <div m="b-2"> <el-checkbox v-model="config.autoInsertSpace">autoInsertSpace</el-checkbox> </div> <el-config-provider :button="config"> <el-button>中文</el-button> </el-config-provider> </div> </template> <script lang="ts" setup> import { reactive } from 'vue' const config = reactive({ autoInsertSpace: true, }) </script> (3)对消息进行配置 <template> <div> <el-config-provider :message="config"> <el-button @click="open">OPEN</el-button> </el-config-provider> </div> </template> <script lang="ts" setup> import { reactive } from 'vue' import { ElMessage } from 'element-plus' const config = reactive({ max: 3, }) const open = () => { ElMessage('This is a message.') } </script> 3、Form 表单组件 (1)Autocomplete 自动补全输入框 <el-autocomplete v-model="state1" :fetch-suggestions="querySearch" clearable class="inline-input w-50" placeholder="Please Input" @select="handleSelect" /> (2)Cascader 级联选择器 注意,级联选择器、树形控件、树形选择的区别 const deptCodeProps = { multiple: true,//每个下拉项左侧都有1个复选框,该下拉项与其子项互相关联 label: 'name', value: 'id', } <el-cascader v-model="filterForm.deptCode" filterable placeholder="全部" style="width: 590px;" :options="centersOptions" :props="deptCodeProps" :show-all-levels="false" @change="deptCodeChange" collapse-tags collapse-tags-tooltip clearable /> (3)Checkbox 多选框 <div> <el-checkbox v-model="checked1" label="Option 1" size="large" /> <el-checkbox v-model="checked2" label="Option 2" size="large" /> </div> (4)ColorPicker 取色器(颜色选择器) <div class="demo-color-block"> <span class="demonstration">With default value</span> <el-color-picker v-model="color1" /> </div> (5)DatePicker 日期选择器 <el-date-picker v-model="filterForm.date" type="daterange" value-format="YYYY-MM-DD" :disabled-date="disabledDate" range-separator="—" :shortcuts="date_shortcuts" :clearable="false" start-placeholder="开始日期" end-placeholder="结束日期" /> </el-form-item> const disabledDate = (time) => { var isStart = time.getTime() < new Date( 2024, 0, 1)// 面板时间早于2024-01-01,禁用 var isEnd = time.getTime() >new Date(new Date() - 24 * 60 * 60 * 1000)// 面板时间晚于昨天此刻,禁用 return isStart || isStart } filterForm.date = dateRange(7, 1)//rangeDate A、shortcuts示例 function rangeDate(numOrStr, number) { var start; var end = new Date(); var thisDate = new Date(); var day = thisDate.getDay(); var one = 1000 * 60 * 60 * 24; // 一天的时间戳 day = day == 0 ? 6 : day - 1;//周日为0,所以需要特殊处理 number = number || 0; if(typeof numOrStr === 'number') { start = new Date(thisDate - (numOrStr + number) * one) ; //.getTime() end = new Date(thisDate - number * one); }else if(numOrStr === 'prevWeek') { thisDate = thisDate - one * 7;//7天前 start = new Date(thisDate - one * day);//上周一 end = new Date(thisDate + one * (6 - day));//上周日 }else if(numOrStr === 'weekStart') { thisDate = thisDate;//今天 start = new Date(thisDate - one * day);//本周一 }else if(numOrStr === 'prevMonth') { var year = thisDate.getFullYear(); var month = thisDate.getMonth(); if (month == 0) month = 12 start = new Date(year, month-1, 1)//上月1号 end = new Date(year, month, 0);//上月最后一天 }else if(numOrStr === 'monthStart') { start = new Date(thisDate.getFullYear(), thisDate.getMonth(), 1)//本月1号 } // 以下,有时可以省略 const addZero = (num) => { return num < 10 ? "0" + num : num; } // 以下,开始日期的年月日 var beginYear = start.getFullYear(); var beginMonth = start.getMonth() + 1; var beginDate = start.getDate(); // 以下,结束日期的年月日 var finishYear = end.getFullYear(); var finishMonth = end.getMonth() + 1; var finishDate = end.getDate(); // 以下,格式化开始日期、结束日期 start = beginYear + "-" + addZero(beginMonth) + "-" + addZero(beginDate) end = finishYear + "-" + addZero(finishMonth) + "-" + addZero(finishDate) // 以上,有时可以省略 return [start, end] } const rangeMonth = (start, end) => { //start月前开始,end天前结束 if(start < 0) return '第1个参数错误!' var time = new Date(); var year = time.getFullYear(); var month = time.getMonth(); var date = time.getDate(); var begin, finish; const addZero = (num) => { return num < 10 ? "0" + num : num; } if (end == 'prev') { begin = new Date(year, month - start, 1); finish = new Date(year, month, 0); }else if (end == 'yesterday') { var startMonthLastDate = new Date(year, month - start + 1, 0).getDate(); var yesterday = date - 1; if (startMonthLastDate < yesterday) { begin = new Date(year, month - start, startMonthLastDate); }else { begin = new Date(year, month - start, yesterday); } finish = new Date(year, month, yesterday); }else if (end == 'today') { var startMonthLastDate = new Date(year, month - start + 1, 0).getDate(); var today = date; if (startMonthLastDate < today) { begin = new Date(year, month - start, startMonthLastDate); }else { begin = new Date(year, month - start, today); } finish = new Date(year, month, today); }else { return '第2个参数错误!' } // 以下,开始日期的年月日 var beginYear = begin.getFullYear(); var beginMonth = begin.getMonth() + 1; var beginDate = begin.getDate(); // 以下,结束日期的年月日 var finishYear = finish.getFullYear(); var finishMonth = finish.getMonth() + 1; var finishDate = finish.getDate(); // 以下,格式化开始日期、结束日期 start = beginYear + "-" + addZero(beginMonth) + "-" + addZero(beginDate) end = finishYear + "-" + addZero(finishMonth) + "-" + addZero(finishDate) return [start, end] } const date_shortcuts = [ { text: '近7天(截止到昨天)', value: rangeDate(7, 1),//从昨天开始往过去推7天 }, { text: '近30天(截止到今天)', value: rangeDate(30),//从今天开始往过去推30天 }, { text: '上周', value: rangeDate('prevWeek'), }, { text: '本周初至今', value: rangeDate('weekStart'), }, { text: '上个月', value: rangeDate('prevMonth'), }, { text: '本月初至今', value: rangeDate('monthStart'), }, { text: '前3个整月', value: rangeMonth(3, 'prev'), }, { text: '前3个月(截止到昨天)', value: rangeMonth(3, 'yesterday'), }, { text: '前3个月(截止到今天)', value: rangeMonth(3, 'today'), } ] for(let i = 0; i < date_shortcuts.length; i++) { console.log(date_shortcuts[i].text, date_shortcuts[i].value) } B、rangeMonth(测试)//dateRange;rangeDate var num = 3 var isUseNowDate = true; const rangeMonth = (start, end) => { //start月前开始,end天前结束 if(start < 0) return '第1个参数错误!' var time = new Date(); var year = time.getFullYear(); var month = isUseNowDate? time.getMonth():4; var date = isUseNowDate? time.getDate():31; var begin, finish; var text = '假如今天是'+(month+1)+'月'+date+'日,那么'; const addZero = (num) => { return num < 10 ? "0" + num : num; } if (end == 'last') { begin = new Date(year, month - start, 1); finish = new Date(year, month, 0); console.log(text+'从'+start+'个月前的1日开始到上个月的最后1天结束,是') }else if (end == 'yesterday') { var startMonthLastDate = new Date(year, month - start + 1, 0).getDate(); var yesterday = date - 1; if (startMonthLastDate < yesterday) { begin = new Date(year, month - start, startMonthLastDate); }else { begin = new Date(year, month - start, yesterday); } finish = new Date(year, month, yesterday); console.log(text+'从'+start+'个月前的昨天开始到昨天结束,是') }else if (end == 'today') { var startMonthLastDate = new Date(year, month - start + 1, 0).getDate(); var today = date; if (startMonthLastDate < today) { begin = new Date(year, month - start, startMonthLastDate); }else { begin = new Date(year, month - start, today); } finish = new Date(year, month, today); console.log(text+'从'+start+'个月前的今天开始到今天结束,是') }else { return '第2个参数错误!' } // 以下,开始日期的年月日 var beginYear = begin.getFullYear(); var beginMonth = begin.getMonth() + 1; var beginDate = begin.getDate(); // 以下,结束日期的年月日 var finishYear = finish.getFullYear(); var finishMonth = finish.getMonth() + 1; var finishDate = finish.getDate(); // 以下,格式化开始日期、结束日期 start = beginYear + "-" + addZero(beginMonth) + "-" + addZero(beginDate) end = finishYear + "-" + addZero(finishMonth) + "-" + addZero(finishDate) return [start, end] } console.log( rangeMonth(num, 'last') ); console.log( rangeMonth(num, 'yesterday') ); console.log( rangeMonth(num, 'today') ); (6)DateTimePicker 日期时间选择器 <el-date-picker v-model="datetimeValue" type="datetimerange" range-separator="To" start-placeholder="Start date" end-placeholder="End date" /> import { ref } from 'vue' const datetimeValue = ref<[Date, Date]>([ new Date(2000, 10, 10, 10, 10), new Date(2000, 10, 11, 10, 10), ]) /* 1、format 日期格式 format="YYYY/MM/DD HH:mm:ss" //客户端24小时补0显示;时间字母,双写为补0,单写为不补0; format="YYYY/MM/DD hh:mm:ss a" //客户端12小时补0显示;H,大写为24小时,小写为12小时(a为am,A为AM) value-format="YYYY-MM-DD HH:mm:ss" //服务端24小时补0显示;取值"X"为秒,取值"x"为毫秒 value-format="YYYY-MM-DD hh:mm:ss a" //服务端12小时补0显示; 2、type 日期类型(year/month/date/datetime/ week/datetimerange/daterange) type="date" //日期 type="datetime" //日期时间 type="datetimerange" //日期时间范围 3、range-separator="To" //范围的分隔符 4、default-value 选择器打开时,默认显示的时间 5、default-time 选择日期后,默认显示的时间,未指定时,默认为00:00:00 6、size 输入框尺寸 large/default/small 7、placeholder 非范围选择时的占位内容 8、start-placeholder 范围选择时开始日期的占位内容 9、end-placeholder 范围选择时结束日期的占位内容 */ (7)Form 表单 <el-form :model="form" label-width="120px"> <el-form-item label="Activity name"> <el-input v-model="form.name" /> </el-form-item> </el-form> (8)Input 输入框 <el-input v-model="input" placeholder="Please input" /> (9)Input Number 数字输入框 <el-input-number v-model="num" :min="1" :max="10" @change="handleChange" /> (10)Radio 单选框 <el-radio-group v-model="radio1" class="ml-4"> <el-radio label="1" size="large">Option 1</el-radio> <el-radio label="2" size="large">Option 2</el-radio> </el-radio-group> (11)Rate 评分 <div class="demo-rate-block"> <span class="demonstration">Default</span> <el-rate v-model="value1" /> </div> (12)Select 选择器 <el-select v-model="value" class="m-2" placeholder="Select" size="large"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </el-select> (13)Select V2 虚拟列表选择器 A、示例1,一边输入字符,一边向后台请求下拉选项 <el-form-item label="选择栏目" prop="columnName" > <el-select-v2 v-model="ruleForm.columnName" filterable remote :remote-method="remoteMethod" clearable :options="allOptions" :loading="loading" placeholder="请输入栏目名称或栏目代码" loading-text="正在加载,请耐心等待!" @change="select2Change" /> </el-form-item> var obj = {} const remoteMethod = (query) => { obj.last = query;//防止出现-先请求后返回-的情况,此处同步改变标记 loading.value = true; selectLike({ id: query, }).then(res => { var data = res.data; for(var i=0 ;i< data.length; i++){ data[i].label = data[i].columnName ; data[i].value = data[i].columnName ; } if(obj.last === query){//防止出现-先请求后返回-的情况,此处异步比较标记 allOptions.value = [...data]; } loading.value = false; }) } B、示例2,敲击回车,创建标签 <template> <div> <el-dialog v-model="visible" title="添加标签" width="600px" :before-close="closeDialog"> <div class="add-label-el-select-v2"> <div style="width:100px">标签名称:</div> <el-select-v2 v-model="newLabel" filterable remote multiple placeholder="请输入" :options="allOptions" :remote-method="remoteMethod" :loading="loading" :reserve-keyword="false" allow-create loading-text="正在加载,请耐心等待!" @focus="handleFocus" > <!-- @keyup.enter="enterEvent" --> </el-select-v2> </div> <div class="add-label-el-button"> <el-button @click="cancel" >取消</el-button> <el-button type="primary" @click="confirm">确定</el-button> </div> </el-dialog> </div> </template> <script setup> import { ref } from 'vue'; import { getLabelInfo } from '@/api/sys.js' const emit = defineEmits(['close', 'confirm']) const props = defineProps({ show:{ type: Boolean, default: false, }, }) const allOptions = ref([]) const visible = ref(false) const loading = ref(false) const newLabel = ref([]); const key = ref(''); function handleFocus() { allOptions.value = [] } const enterEvent = function (event) { if (event.keyCode == 13) { if (key.value) { if (newLabel.value.indexOf(key.value) == -1) { newLabel.value.push(key.value) } } } } const remoteMethod = (query) => { key.value = query if (query) { loading.value = true let params = { name: query } getLabelInfo(params).then((res) => { allOptions.value = [] res.data.forEach((item) => { item.value = item.labelName item.label = item.labelName if (item.label != query) {//防止出现2个相同的下拉项,比如“大海” allOptions.value.push(item) } }) loading.value = false }).catch(() => { loading.value = false }) } } const closeDialog = function () { emit('close') } const cancel = function () { emit('close') } const confirm = function () { emit('confirm',newLabel.value) } onMounted( function() { visible.value = props.show }); onUpdated( function() { visible.value = props.show }); </script> <style> .add-label-el-select-v2 { display: flex; flex-direction: row; align-items: center; padding-bottom: 20px; } .add-label-el-button { display: flex; justify-content: flex-end; } </style> C、vue-virtual-scroll-list的说明 a、是一个用于Vue.js的虚拟滚动组件,它可以用来处理包含大量列表项的长列表 b、该组件通过仅渲染当前视口内的列表项来提高性能,从而减少渲染的计算和内存使用 D、vue-virtual-scroll-list的原理 a、将列表分割成多个固定高度的区块 b、只渲染当前视口内的区块 c、当用户滚动时,通过重用区块来优化性能 E、vue-virtual-scroll-list的伪代码 <template> <div class="virtual-list" @scroll="onScroll" :style="{ height: height + 'px', overflow: 'auto' }" > <div v-for="(item, index) in visibleItems" :key="start + index" :style="{ height: itemHeight + 'px' }" > <!-- 插入你的列表项内容 --> {{ items[start + index] }} </div> </div> </template> <script> export default { props: { items: { type: Array, required: true }, itemHeight: { type: Number, required: true } }, data() { return { start: 0, end: 0, height: 0 }; }, computed: { visibleItems() { return this.items.slice(this.start, this.end + 1); } }, methods: { onScroll() { const scrollTop = this.$el.scrollTop; this.start = Math.floor(scrollTop / this.itemHeight); this.end = this.start + Math.ceil(this.$el.clientHeight / this.itemHeight) + 1; } }, mounted() { this.height = this.itemHeight * this.items.length; } }; </script> <style> .virtual-list { overflow-y: scroll; position: relative; } </style> (14)Slider 滑块 <div class="slider-demo-block"> <span class="demonstration">Default value</span> <el-slider v-model="value1" /> </div> (15)Switch 开关 <el-switch v-model="value2" class="ml-2" style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" active-value="100" inactive-value="0" />/* 背景颜色、value绑定、禁用状态;尺寸、文字描述、自定义图标、加载状态、阻止切换、自定义动作图标、自定义操作图标 */ (16)TimePicker 时间选择器 <el-time-picker v-model="value2" arrow-control placeholder="Arbitrary time" /> (17)TimeSelect 时间选择 <el-time-select v-model="value" start="08:30" step="00:15" end="18:30" placeholder="Select time" /> (18)Transfer 穿梭框 <el-transfer v-model="value" :data="data" /> (19)Upload 上传 //利用插槽定义上传按钮和说明文字 <el-upload v-model:file-list="fileList" class="upload-demo" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" multiple :on-preview="handlePreview" :on-remove="handleRemove" :before-remove="beforeRemove" :limit="3" :on-exceed="handleExceed" :http-request="httpRequest" > <el-button type="primary">Click to upload</el-button> <template #tip> <div class="el-upload__tip"> jpg/png files with a size less than 500KB. </div> </template> </el-upload> /* 1、on-preview:点击文件列表中已上传的文件时的钩子 2、on-remove:文件列表移除文件时的钩子 3、on-success:文件上传成功时的钩子 4、on-error:文件上传失败时的钩子 5、on-progress:文件上传时的钩子 6、on-change:文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 7、on-exceed:当超出限制时,执行的钩子函数 8、before-upload:上传文件之前的钩子,参数为上传的文件,若返回false或者返回Promise且被reject,则停止上传 9、before-remove:删除文件之前的钩子,参数为上传的文件和文件列表,若返回false或者返回Promise且被reject,则停止删除 10、http-request:覆盖默认的Xhr行为,允许自行实现上传文件的请求,如切片。 */ data() { return { fileList: [], chunkSize: 1024 * 1024 * 6, // 6M 为一个切片,超过6M,则采用上传大文件的方法进行文件上传 percentage: {}, // 进度条 } }, httpRequest(params) { // 来源:https://blog.csdn.net/qq_33547169/article/details/127807333 let {file: {size}} = params; size > this.chunkSize ? this.uploadBigFile(params, this.chunkSize) : this.uploadFile(params) }, uploadBigFile(params, chunkSize) { let {file, filename, onSuccess} = params; let {size, type, name} = file; const chunkLength = Math.ceil(file.size / chunkSize) let chunks = Array.from({length: chunkLength}).map((v, i) => file.slice(i * chunkSize, i * chunkSize + chunkSize)) let loadeds = []; let chunkRequests = chunks.map((chunk, i) => { const formData = new FormData(); formData.append(filename, chunk); loadeds[i] = []; const config = { 'Content-type': 'multipart/form-data', onUploadProgress: progress => { loadeds[i].push(progress.loaded) this.calculationPercentage(file, i, loadeds, size); } } return this.$post(this.$servers.openServer, 'tb/minio/upload', formData, config); }) this.$axios.all(chunkRequests).then(res => { let fileNames = res.map(({data: {minioPath}}) => minioPath) const params = {fileName: name, contentType: type, fileSize: size, fileNames} this.$post(this.$servers.openServer, 'tb/minio/merge', params).then(onSuccess) }) }, uploadFile(params) { let {file, filename, onSuccess} = params; const formData = new FormData(); formData.append(filename, file); const config = { 'Content-type': 'multipart/form-data', onUploadProgress: progress => this.percentage = Math.floor(progress.loaded / progress.total * 100) } this.$post(this.$servers.openServer, 'tb/minio/upload', formData, config).then(onSuccess) }, 4、Data 数据展示 (1)Avatar 头像 <div v-for="size in sizeList" :key="size" class="block"> <el-avatar :size="size" :src="circleUrl" /> </div> (2)Badge 徽章 <el-badge :value="12" class="item"> <el-button>comments</el-button> </el-badge> (3)Calendar 日历 <el-calendar v-model="value" /> (4)Card 卡片 <el-card class="box-card"> <template #header> <div class="card-header"> <span>Card name</span> <el-button class="button" text>Operation button</el-button> </div> </template> <div v-for="o in 4" :key="o" class="text item">{{ 'List item ' + o }}</div> </el-card> (5)Carousel 走马灯 <el-carousel height="150px"> <el-carousel-item v-for="item in 4" :key="item"> <h3 class="small justify-center" text="2xl">{{ item }}</h3> </el-carousel-item> </el-carousel> (6)Collapse 折叠面板 <el-collapse v-model="activeNames" @change="handleChange"> <el-collapse-item title="Consistency" name="1"> <div> Consistent with real life: in line with the process and logic of real life, and comply with languages and habits that the users are used to; </div> <div> Consistent within interface: all elements should be consistent, such as: design style, icons and texts, position of elements, etc. </div> </el-collapse-item> </el-collapse> (7)Descriptions 描述列表 <el-descriptions title="User Info"> <el-descriptions-item label="Username">kooriookami</el-descriptions-item> </el-descriptions> (8)Empty 空状态 <el-empty description="description" /> <el-empty image="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png"/> <el-empty v-if="showEmpty" class="content-table" style="min-height:500px" :image="emptyLogo" :image-size="220" description="选择查询条件!"/> <div v-else class="content-table"></div> (9)Image 图片 <div v-for="fit in fits" :key="fit" class="block"> <span class="demonstration">{{ fit }}</span> <el-image style="width: 100px; height: 100px" :src="url" :fit="fit" /> </div> (10)Infinite Scroll 无限滚动(无限滚动加载) <ul v-infinite-scroll="load" class="list" :infinite-scroll-disabled="disabled" > <li v-for="i in count" :key="i" class="list-item">{{ i }}</li> </ul> import { computed, ref } from 'vue' const count = ref(10) const loading = ref(false) const noMore = computed(() => count.value >= 20) const disabled = computed(() => loading.value || noMore.value) //为false,加载 //(loading.value || noMore.value)与(!loading.value && !noMore.value)等效,都为false,执行 const load = () => { loading.value = true setTimeout(() => { count.value += 2 loading.value = false //为false }, 2000) } (11)Pagination 分页 <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[100, 200, 300, 400]" :small="small" :disabled="disabled" :background="background" layout="total, sizes, prev, pager, next, jumper" :total="400" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> const currentPage = ref(4); const pageSize = ref(100); const handleSizeChange = (val) => {} const handleCurrentChange = (val) => {} (12)Progress 进度条 <el-progress :percentage="100" status="success" /> (13)Result 结果 不同颜色圆形背景下的,对号、错号、感叹号图标 <el-result icon="success" title="Success Tip" sub-title="Please follow the instructions" > <template #extra> <el-button type="primary">Back</el-button> </template> </el-result> (14)Skeleton 骨架屏 <el-skeleton /> <br /> <el-skeleton style="--el-skeleton-circle-size: 100px"> <template #template> <el-skeleton-item variant="circle" /> </template> </el-skeleton> (15)Table 表格 A、留出padding换行 .el-table .cell{ padding: 4px 8px; white-space: normal; } B、禁止勾选 <el-table-column type="selection" width="55" align="center" :selectable="selectionDisabled" :selectable="(row) => { return row.status === 2&&row.summary===2}" /> var selectionDisabled = function(row, index){ return row.status === 2 && row.summary === 2; // 返回false,禁止勾选 } C、嵌套表格,无子级,无展开按钮,展开图标不显示 <template> <el-table :data="allTableData" :cell-class-name="addClassToCell"> </template> <script setup> const addClassToCell = ({ row, column }) => { if (!row.columnVOList && column.type === 'expand') { return 'no-expand'; } } </script> <style> .no-expand.el-table_1_column_1.el-table__expand-column.el-table__cell>div { display: none; } </style> D、在Element-plus中el-table的某项展开,该项的子el-table出现 //.toggleRowExpansion,row(切换该行的展开状态), expanded(如存在,则固定该行的展开状态) <template> <el-table :data="tableData" ref="tableRef" > <el-table-column type="expand"> <template #default="scope"> <div> 这是展开行的详细信息:{{ scope.row.detail }} </div> </template> </el-table-column> <el-table-column prop="name" label="名称"></el-table-column> <el-table-column prop="age" label="年龄"></el-table-column> </el-table> <el-button @click="toggleFirstRowExpansion">切换第一行展开状态</el-button> </template> <script setup> import { ref } from 'vue'; const tableRef = ref(null); const tableData = [ { name: '张三', age: 25, detail: '张三的详细信息' }, { name: '李四', age: 30, detail: '李四的详细信息' }, { name: '王五', age: 28, detail: '王五的详细信息' } ]; const toggleFirstRowExpansion = () => { if (tableRef.value) { const table = tableRef.value; const firstRow = tableData[0]; table.toggleRowExpansion(firstRow); } }; </script> E、在Element-plus中el-table里,搜集选中项 //.getSelectionRows,返回当前选中的行 //.clearSelection,用于多选表格,清空用户的选择 <template> <el-button @click="handleCollectSelection">收集选中项</el-button> <el-table :data="tableData" ref="tableRef" @selection-change="changeSelection" style="width: 100%" row-key="id" highlight-current-row :selection-id="row => row.id" > <el-table-column type="selection" width="55"></el-table-column> <el-table-column prop="date" label="日期" width="180"></el-table-column> <el-table-column prop="name" label="姓名" width="180"></el-table-column> <el-table-column prop="address" label="地址"></el-table-column> </el-table> </template> <script setup> import { ref } from 'vue'; const tableData = ref([ // ...数据 ]); const tableRef = ref(null); const selectedRows = ref([]); const changeSelection = (selection) => { selectedRows.value = selection; }; const handleCollectSelection = () => { if (tableRef.value) { const selectedIds = tableRef.value.getSelectionRows().map(row => row.id); console.log('选中项的ID:', selectedIds); } }; </script> F、在Element-plus中el-table的某项勾选,该项的子el-table全选 //@select,当用户手动勾选数据行的Checkbox时触发的事件,selection(所有行), row(当前行) //@select-all,当用户手动勾选全选Checkbox时触发的事件,selection(所有行) //@selection-change:当(单选或全选)选择项发生变化时会触发该事件,selection(所有行。组件内部会自动把翻页前后的所有勾选项都汇总到这里,并通过row-key决定勾选) //.toggleRowSelection,用于多选表格,row(切换该行的选中状态),selected(如存在,则固定该行的选中状态) //.toggleAllSelection,用于多选表格,切换全选和全不选,tableRef.value.toggleAllSelection(true); 附、所有页勾选的实施方案 (1)false,不处理 (2)true,存储未勾选项到数组里,把翻页后的数据逐条与数组比对,比中则不勾选,比不中则勾选;全比不中,则组件会自动勾选本页的全选 <template> <el-table :data="parentData" @select="handleSelect" @select-all="handleAllSelect" @selection-change="handleSelectionChange" row-key="manuMd5" v-loading="loading" > <el-table-column type="selection" width="55"></el-table-column> <!-- 其他列 --> <el-table-column label="操作"> <template #default="{ row }"> <el-table :data="row.children" :ref="`childTable${row.id}`" @selection-change="childSelection => handleChildSelection(row.id, childSelection)" > <el-table-column type="selection" width="55"></el-table-column> <!-- 子表其他列 --> </el-table> </template> </el-table-column> </el-table> </template> <script setup> import { ref } from 'vue'; const parentData = ref([ // ...数据 ]); const handleSelect = (selection, row) => {//选中的行, 当前行数据 const childTable = this.$refs[`childTable${row.id}`][0]; if (childTable) {//如果该项存在子表格 childTable.toggleAllSelection(selection.includes(row)); } }; const handleAllSelect = (selection) => {//全选状态下的所有行数据 this.parentData.forEach(row => { //遍历父表格数据 const childTable = this.$refs[`childTable${row.id}`][0]; if (childTable) {//如果该项存在子表格 childTable.toggleAllSelection(selection.includes(row)); } }); }; const handleChildSelection = (parentRowId, selection) => { const parentTable = this.$refs.parentTable; if (selection.length === this.parentData.find(row => row.id === parentRowId).children.length) { parentTable.toggleRowSelection(this.parentData.find(row => row.id === parentRowId), true); } else { parentTable.toggleRowSelection(this.parentData.find(row => row.id === parentRowId), false); } }; </script> G、element-plus的el-table自定义表头筛选查询 来源,https://blog.csdn.net/i_am_a_div/article/details/140489557 <template> <div class="page-view" @click="handleClickOutside"> <el-button @click="resetFilters">Reset Filters</el-button> <el-table ref="tableRef" row-key="date" :data="filteredData" style="width: 100%"> <el-table-column prop="date" label="Date" sortable width="180" column-key="date" /> <el-table-column prop="name" label="Name" sortable width="180"> <template #header> <div style="display: inline-block; align-items: center;"> <span>Name</span> <el-icon ref="buttonRef" @click.stop="toggleNameFilter(buttonRef, 'nameFilter')" style="cursor: pointer; margin-left: 10px;" :style="{ color: searchDate.nameFilter ? '#007d7b' : '#85888e' }"> <Filter /> </el-icon> </div> </template> <template #default="scope"> <span>{{ scope.row.name }}---</span> </template> </el-table-column> <el-table-column prop="address" label="Address" width="300" /> <el-table-column prop="tag" label="Tag"> <template #header> <div style="display: inline-block; align-items: center;"> <span>Tag</span> <el-icon ref="buttonRef2" @click.stop="toggleNameFilter(buttonRef2, 'tagFilter')" style="cursor: pointer; margin-left: 10px;" :style="{ color: searchDate.tagFilter ? '#007d7b' : '#85888e' }"> <Filter /> </el-icon> </div> </template> <template #default="scope"> <el-tag :type="scope.row.tag === 'Home' ? 'primary' : 'success'">{{ scope.row.tag }}</el-tag> </template> </el-table-column> </el-table> {{ visible }} <pre>{{ searchDate }}</pre> <el-popover :visible="visible" :virtual-ref="refName" placement="bottom" :width="'fit-content'"> <el-icon class="popover-close" @click="visible=false"><Close /></el-icon> <div> <el-input v-if="showKey === 'nameFilter'" v-model="searchDate.nameFilter" placeholder="输入框" @input="applyNameFilter" clearable style="margin-top: 10px; width: 150px;" /> <el-select v-if="showKey === 'tagFilter'" v-model="searchDate.tagFilter" placeholder="Select Tag" @change="applyTagFilter" clearable style="margin-top: 10px; width: 150px;"> <el-option label="Home" value="Home" /> <el-option label="Office" value="Office" /> </el-select> <div class="mt" style="text-align: right;"> <el-button type="info" link @click="cancelFilter">重置</el-button> <el-button type="primary" link @click="searchFilter">筛选</el-button> </div> </div> </el-popover> </div> </template> <script setup> import { ref, computed, unref } from 'vue' // import { ElIconSearch, ElIconSArrowDown } from '@element-plus/icons-vue' import { Filter } from '@element-plus/icons-vue' import { ClickOutside as vClickOutside } from 'element-plus' const buttonRef = ref() const buttonRef2 = ref() const popoverRef = ref() const onClickOutside = () => { if (unref(popoverRef) && unref(popoverRef).popperRef) { unref(popoverRef).popperRef?.delayHide?.() } } const showKey = ref(undefined) // 当前展示哪个筛选窗 const visible = ref(false) // 手动控制筛选窗显隐 const refName = ref(null) // 动态绑定在哪个表头图标下,------非常重要 const tableRef = ref() // 表格ref const searchDate = ref({ nameFilter: undefined, tagFilter: undefined, }) // 查询参数 // 全局重置 const resetFilters = () => { searchDate.value.nameFilter = undefined searchDate.value.tagFilter = undefined applyNameFilter() applyTagFilter() getData() } // 触发筛选 const toggleNameFilter = (ref, key) => { if (showKey.value !== key) { visible.value = false getData() } refName.value = ref // 动态绑定在哪个表头图标下,------非常重要 showKey.value = key visible.value = !visible.value } // 点击其他元素 const handleClickOutside = () => { // visible.value = false; }; // 重置 const cancelFilter = () => { searchDate.value[showKey.value] = undefined visible.value = false; getData() } // 筛选 const searchFilter = () => { visible.value = false; getData() } // 单独过滤 const applyNameFilter = () => { // Filtering logic can be customized if needed } const applyTagFilter = () => { // Filtering logic can be customized if needed } // 原数据 const tableData = [ { date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', tag: 'Home', }, { date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', tag: 'Office', }, { date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', tag: 'Home', }, { date: '2016-05-01', name: 'Jack', address: 'No. 189, Grove St, Los Angeles', tag: 'Office', }, ] // 列表显示的数据 // const filteredData = computed(() => { // return tableData.filter((item) => { // const nameMatch = searchDate.value.nameFilter ? item.name.toLowerCase().includes(searchDate.value.nameFilter.toLowerCase()) : true // const tagMatch = searchDate.value.tagFilter ? item.tag === searchDate.value.tagFilter : true // return nameMatch && tagMatch // }) // }) const filteredData = ref(JSON.parse(JSON.stringify(tableData))) // 获取数据 const getData = () => { filteredData.value = tableData.filter((item) => { const nameMatch = searchDate.value.nameFilter ? item.name.toLowerCase().includes(searchDate.value.nameFilter.toLowerCase()) : true const tagMatch = searchDate.value.tagFilter ? item.tag === searchDate.value.tagFilter : true return nameMatch && tagMatch }) } </script> <style lang="scss" scoped> :deep(.el-popover) { width: fit-content !important; } :deep(.el-popover.el-popper) { width: fit-content !important; } </style> H、element-plus的el-table调用内置的升降排序 <el-table :data="allTableData" v-loading="loading" ref="tvTable" @sort-change="sortChange" > <el-table-column label="周期内首播总时长(分钟)" prop="columnTotalLengthMinutes" sortable="custom" :sort-orders="['descending', 'ascending']" > <template #default="props">{{props.row.columnTotalLengthMinutes}}</template> </el-table-column> <el-table-column label="栏目开播时间" prop="" align="center" > <template #default="props">{{ thisFormat(props.row.columnPlayBeginPlayDate) }}</template> </el-table-column> </el-table> function sortChange(data) { console.log(data); // order: "ascending" // prop: "columnTotalLengthMinutes" if (data.order) { filterForm.value.orderName = data.prop filterForm.value.orderMethod = data.order === "descending" ? 'desc' : 'asc' } else { filterForm.value.orderName = '' filterForm.value.orderMethod = '' } page.value.pageNum = 1 listData() } function tabelClearSort() { filterForm.value.orderName = '' filterForm.value.orderMethod = '' tvTable.value && tvTable.value.clearSort() } I、倒计时表格 <template> <div class="el-col-right-title"> <div class="el-col-right-title-item el-col-right-title-current">我的订单</div> </div> <div class="el-col-right-order-select"> <div class="el-col-right-order-select-item" :class="{'el-col-right-order-select-item-current':orderStatus == item.state }" v-for="item in orderTabs" :key="item.state" @click="clickOrderState(item)" >{{ item.name }}</div> </div> <el-table v-for="(tablePay, indexRow) in rowPay" :data="tablePay" :header-cell-style="headerCellStyle" :cell-class-name="tableCellClassName" :span-method="spanMethod" class="profile-el-table" :key="indexRow" > <el-table-column v-for="(item, indexCol) in colPay[indexRow]" :key="indexCol" :prop="item.prop" :width="item.width" :label="item.label" > <template #header v-if="indexCol === 4"><!-- 表头,第5栏。若下面为空,则为空 --> <span v-if="item.orderStatus===1"> <span v-if="new Date().getTime() < item.expireTime"> <span>待支付,剩余时间</span> <span class="forpay-countdown">{{item.label}}</span> </span> <span v-else> <span>订单已失效</span> </span> </span> <span v-else-if="item.orderStatus===2" style="color: #0058E6;">{{item.label}}</span> <span v-else> <span>{{item.label}}</span> </span> </template> <template #default="scope" v-if="indexCol === 1"><!-- 表体,第2栏。若下面为空,则用默认值 --> <div v-if="scope.row.isColspan" style="text-align: right;"><!-- 表体,第2行。若下面为空,则为空 --> <span v-if="scope.row.orderStatus===1"> <span v-if="new Date().getTime() < scope.row.expireTime"> <span> <span>应付</span> <span style="color:#DD0000">¥{{ scope.row.aaa }}</span> </span> <span style="padding-left: 40px;"> <el-button @click="cancelOrder(scope.row)" style="font-size: 12px;color:#000000;font-weight: 600;">取消订单</el-button> <el-button @click="immediatePay(scope.row)" style="background-color: #0058E6;color:#ffffff;font-size: 12px;">立即支付</el-button> </span> </span> </span> <span v-else-if="scope.row.orderStatus===2"> <span>共1件产品,总计</span> <span style="color:#DD0000">¥{{ scope.row.aaa }}</span> </span> </div> </template> </el-table-column> </el-table> <div class="noData" v-show="rowPay.length===0">暂无数据</div> <div class="profile-el-pagination" v-show="total>0"> <span class="profile-el-pagination-span">共{{total}}条</span> <el-pagination layout=" ->, prev, pager, next" :total="total" @current-change="clickOrderpagination" /> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { getOrderData, distanceDateTime } from '@/api/profile'; import { orderCancel } from '@/api/pay'; import * as filters from '@/filter'; import useUserStore from '@/store/modules/user'; const userStore = useUserStore(); const userSid = userStore.user && userStore.user.zxktUser && userStore.user.zxktUser.sid; var total = ref(0) var orderStatus = ref(0) var timerArray = reactive([]) var colPay = reactive([]); var rowPay = reactive([]); var orderTabs = reactive([ { name: "全部订单", state: 0, }, { name: "待付款", state: 1, }, { name: "支付成功", state: 2, }, ]); var spanMethod = function({ row, column, rowIndex, columnIndex }){ if (rowIndex === 1 && columnIndex === 1) { return { rowspan: 1, colspan: 4 }; } }; var headerCellStyle = reactive({color:'#b2b8c4',background:'#eaeef5'}); var tableCellClassName = function({ row, column, rowIndex, columnIndex}){ if (rowIndex === 1 && columnIndex === 0) { return 'grayColor' }else{ return 'blackColor' } }; //以下切换订单Tab var clickOrderState = function(item){ orderStatus.value = item.state var page = item.page||1 var allData = { page: page, size: 10, userId: userSid, }; if(orderStatus.value == 1 ||orderStatus.value == 2){ allData.orderStatus = orderStatus.value } var totalIn = total getOrderData(allData).then(function(result){ totalIn.value = result.data.total; colPay.length = 0; rowPay.length = 0; var list = result.data.list; for(var i = 0; i<list.length; i++){ var item = list[i]; var singleCol = [//每个表头有5列 { prop: 'date', label: '下单时间:' + distanceDateTime(item.orderTime).clientDateTime, width: '280px' }, { prop: 'price', label: '原价(元)', }, { prop: 'payAmount', label: item.orderStatus===2 ? '实付(元)' : '应付(元)', }, { prop: 'favor', label: '优惠金额', }, { prop: 'forPay', label: ['','','支付成功','已退款','退款失败','支付失败','订单已关闭'][item.orderStatus], width: '204px', expireTime: item.expireTime, orderStatus: item.orderStatus, }, ]; var singleRow = [//每个表体有2行 { date: item.collectionTitle, price: filters.fmoney(item.price), payAmount: filters.fmoney(item.payAmount), favor: filters.fmoney(item.price - item.payAmount), forPay: item.forPay, }, { date: '订单号:' + item.orderNo, isColspan: true, totalNum: item.totalNum, aaa: filters.fmoney(item.payAmount),//aaa不能换成payAmount orderNo: item.orderNo, sid: item.sid, payMethod: item.payMethod, expireTime: item.expireTime, orderStatus: item.orderStatus, } ]; colPay.push(singleCol) rowPay.push(singleRow) } if(timerArray.length>0){ for(var i = 0; i < timerArray.length; i++){ clearInterval(timerArray[i]); } } var objArray = [] timerArray.length = 0; for(var i = 0; i < colPay.length; i++){ var obj = colPay[i][colPay[i].length-1];//获取每个表头的最后一项,里面包含-倒计时区域 var total = 0; if(obj.orderStatus != 1 ) return if(obj.expireTime < new Date().getTime() ) return countDown(obj)//立即执行一次倒计时,以免-倒计时区域-出现1秒钟的空白 objArray.push(obj)//存储所有的-倒计时区域 //当有定时器清除时,clickOrderState会再次执行,与定时器相关的数据会重新生成 var interval = setInterval(function(){//for循环结束时,objArray存储了所有的-倒计时区域,又过1秒钟后,多个定时器同时执行 var index = total%objArray.length;//获取本定时器对应的-倒计时区域的序号,即定时器已运行次数与定时器总个数的模, countDown(objArray[index])//执行倒计时,传入倒计时区域 total++ },1000) obj.interval = interval timerArray.push(interval) } }) } //以下是倒计时 var countDown = function (obj) { /* console.log( '倒计时正在运行,请判断是否必要!!!' ); */ function addZero(number) { return number.toString()[1] ? number : "0" + number; }; var nowMilliseconds = new Date().getTime(); var futureMilliseconds = obj.expireTime; if(nowMilliseconds >= futureMilliseconds){ obj.label = '定时器出错了'; clearInterval(obj.interval); clickOrderState({state: orderStatus.value}) return } var seconds = Math.floor((futureMilliseconds - nowMilliseconds)/1000) var str = ""; var day = Math.floor(seconds / 86400); var hour = Math.floor((seconds % 86400) / 3600); var minute = Math.floor(((seconds % 86400) % 3600) / 60); var second = Math.floor(((seconds % 86400) % 3600) % 60); if ( day > 0) { str += day + "天"; } if ( day > 0 || hour > 0) { str += addZero(hour) + "小时"; } if ( day > 0 || hour > 0 || minute > 0) { str += addZero(minute) + "分"; } str += addZero(second) + "秒"; obj.label = str; } var clickOrderpagination = function(num){ clickOrderState({ state: orderStatus.value, page: num, }) } var cancelOrder = function(row){ var data ={ orderNo: row.orderNo, userId: userSid, }; orderCancel(data).then(function(){ clickOrderState({state: orderStatus.value}) }) } import { useRouter } from "vue-router" const router = useRouter() var immediatePay = function(row){ var payMethod = { WECHAT: 1, ALIPAY: 2, }; router.push({ path: '/pay', query: { collectionId: row.collectionId, channel: payMethod[row.payMethod], expiretime: row.expireTime, } }) } const emit = defineEmits(['refreshPage']) onMounted(function() { clickOrderState({state:0}) emit('refreshPage',router.currentRoute.value.name) }); onBeforeUnmount(function() { if(timerArray.length>0){ for(var i = 0; i < timerArray.length; i++){ clearInterval(timerArray[i]); } } }); </script> <style lang="scss"> .blackColor{ color:#000000; } .grayColor{ color:#707889; } </style> (16)Virtualized Table 虚拟化表格 <template> <el-table-v2 :columns="columns" :data="data" :width="700" :height="400" fixed /> </template> <script lang="ts" setup> const generateColumns = (length = 10, prefix = 'column-', props?: any) => Array.from({ length }).map((_, columnIndex) => ({ ...props, key: `${prefix}${columnIndex}`, dataKey: `${prefix}${columnIndex}`, title: `Column ${columnIndex}`, width: 150, })) const generateData = ( columns: ReturnType<typeof generateColumns>, length = 200, prefix = 'row-' ) => Array.from({ length }).map((_, rowIndex) => { return columns.reduce( (rowData, column, columnIndex) => { rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}` return rowData }, { id: `${prefix}${rowIndex}`, parentId: null, } ) }) const columns = generateColumns(10) const data = generateData(columns, 1000) </script> (17)Tag 标签 <el-tag class="ml-2" type="warning">Tag 4</el-tag> (18)Timeline 时间线 <template> <el-timeline> <el-timeline-item v-for="(activity, index) in activities" :key="index" :timestamp="activity.timestamp" > {{ activity.content }} </el-timeline-item> </el-timeline> </template> <script lang="ts" setup> const activities = [ { content: 'Event start', timestamp: '2018-04-15', }, { content: 'Approved', timestamp: '2018-04-13', }, { content: 'Success', timestamp: '2018-04-11', }, ] </script> (19)Tree 树形控件 注意,级联选择器、树形控件、树形选择的区别 <el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick" /> (20)TreeSelect 树形选择 注意,级联选择器、树形控件、树形选择的区别 <el-tree-select v-model="value" :data="data" :render-after-expand="false" show-checkbox /> (21)Tree V2 虚拟化树形控件 <el-tree-v2 :data="data" :props="props" :height="208" /> 5、Navigation 导航 (1)Affix 固钉 <el-affix :offset="120"> <el-button type="primary">Offset top 120px</el-button> </el-affix> (2)Backtop 回到顶部 <el-backtop :right="100" :bottom="100" /> (3)Breadcrumb 面包屑 <template> <el-breadcrumb separator="/"> <el-breadcrumb-item :to="{ path: '/' }">homepage</el-breadcrumb-item> <el-breadcrumb-item><a href="/">promotion management</a></el-breadcrumb-item> <el-breadcrumb-item>promotion list</el-breadcrumb-item> <el-breadcrumb-item>promotion detail</el-breadcrumb-item> </el-breadcrumb> </template> (4)Dropdown 下拉菜单 <el-dropdown> <span class="el-dropdown-link"> Dropdown List <el-icon class="el-icon--right"> <arrow-down /> </el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>Action 1</el-dropdown-item> <el-dropdown-item>Action 2</el-dropdown-item> <el-dropdown-item>Action 3</el-dropdown-item> <el-dropdown-item disabled>Action 4</el-dropdown-item> <el-dropdown-item divided>Action 5</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> (5)Menu 菜单 <el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect" > <el-menu-item index="1">Processing Center</el-menu-item> <el-sub-menu index="2"> <template #title>Workspace</template> <el-menu-item index="2-1">item one</el-menu-item> <el-menu-item index="2-2">item two</el-menu-item> <el-menu-item index="2-3">item three</el-menu-item> <el-sub-menu index="2-4"> <template #title>item four</template> <el-menu-item index="2-4-1">item one</el-menu-item> <el-menu-item index="2-4-2">item two</el-menu-item> <el-menu-item index="2-4-3">item three</el-menu-item> </el-sub-menu> </el-sub-menu> <el-menu-item-group title="Group One"> <el-menu-item index="1-1">item one</el-menu-item> <el-menu-item index="1-2">item two</el-menu-item> </el-menu-item-group> <el-menu-item index="3" disabled>Info</el-menu-item> <el-menu-item index="4">Orders</el-menu-item> </el-menu> (6)Page Header 页头 <el-page-header @back="onBack"> <template #breadcrumb> <el-breadcrumb separator="/"> <el-breadcrumb-item :to="{ path: './page-header.html' }"> homepage </el-breadcrumb-item> <el-breadcrumb-item ><a href="./page-header.html">route 1</a></el-breadcrumb-item > <el-breadcrumb-item>route 2</el-breadcrumb-item> </el-breadcrumb> </template> <template #content> <div class="flex items-center"> <el-avatar class="mr-3" :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /> <span class="text-large font-600 mr-3"> Title </span> <span class="text-sm mr-2" style="color: var(--el-text-color-regular)" > Sub title </span> <el-tag>Default</el-tag> </div> </template> <template #extra> <div class="flex items-center"> <el-button>Print</el-button> <el-button type="primary" class="ml-2">Edit</el-button> </div> </template> <el-descriptions :column="3" size="small" class="mt-4"> <el-descriptions-item label="Username">kooriookami</el-descriptions-item> <el-descriptions-item label="Telephone">18100000000</el-descriptions-item> <el-descriptions-item label="Place">Suzhou</el-descriptions-item> <el-descriptions-item label="Remarks"> <el-tag size="small">School</el-tag> </el-descriptions-item> <el-descriptions-item label="Address">No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu Province </el-descriptions-item> </el-descriptions> <p class="mt-4 text-sm"> Element Plus team uses <b>weekly</b> release strategy under normal circumstance, but critical bug fixes would require hotfix so the actual release number <b>could be</b> more than 1 per week. </p> </el-page-header> (7)Steps 步骤条 <el-steps :active="active" finish-status="success"> <el-step title="Step 1" /> <el-step title="Step 2" /> <el-step title="Step 3" /> </el-steps> (8)Tabs 标签页 <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick"> <el-tab-pane label="User" name="first">User</el-tab-pane> <el-tab-pane label="Config" name="second">Config</el-tab-pane> <el-tab-pane label="Role" name="third">Role</el-tab-pane> <el-tab-pane label="Task" name="fourth">Task</el-tab-pane> </el-tabs> A、需求与实现 需求:点击el-tab-pane,向后台发送请求,请求结果没返回前,禁止点击el-tab-pane, 实现:将el-tab-pane的disabled属性与loading绑定 说明:该方案同样适用于由div和el-button模拟的选项卡中 6、Feedback 反馈组件 (1)Alert 提示 Alert不是浮层元素,不会自动消失或关闭 <el-alert title="success alert" type="success" /> (2)Dialog 对话框 (2-1)示例 <el-dialog v-model="dialogVisible" title="Tips" width="30%" :before-close="handleClose" > <span>This is a message</span> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">Cancel</el-button> <el-button type="primary" @click="dialogVisible = false"> Confirm </el-button> </span> </template> </el-dialog> (2-2)弹窗上下固定,中间滚动 <el-dialog title="详情" :visible.sync="dialogPreviewVisible" width="1150px" > <div style="height:640px;overflow: auto;"> <AlbumPreview :sid="item.f_id"></AlbumPreview> </div> <el-button>确定</el-button> </el-dialog> (2-3)弹窗作为子组件(传值传参传数据) A、需求 a、父组件控制子组件的隐现 b、子组件只有一个弹窗 c、弹窗只有一个表单 B、实现 a、父组件属性传参 b、子组件获取参数,显示弹窗和表单 c、在点击按钮后,或点击叉号时,发射事件(携带数据), d、父组件向后台发送数据,然后)成功后,属性传参,关闭弹窗 注、在子组件里,给接收的属性直接赋值,违背了单向数据流的原则 注、在子组件里,给接收的属性解构并初始化为本组件的变量,然后可以给该变量重新赋值 (3)Drawer 抽屉 <el-drawer v-model="drawer" title="I am the title" :direction="direction" :before-close="handleClose" > <span>Hi, there!</span> </el-drawer> (4)Loading 加载 <el-table v-loading="loading" :data="tableData" style="width: 100%"> <el-table-column prop="date" label="Date" width="180" /> <el-table-column prop="name" label="Name" width="180" /> <el-table-column prop="address" label="Address" /> </el-table> (5)Message 消息提示 Message用于主动操作后的反馈提示 Notification用于系统级通知的被动提醒 <template> <el-button :plain="true" @click="open">Show message</el-button> </template> <script lang="ts" setup> import { h } from 'vue' import { ElMessage } from 'element-plus' const open = () => { ElMessage('this is a message.') } const open = () => { ElMessage({ message: 'Warning, this is a warning message.', type: 'warning', }) } </script> (6)MessageBox 消息弹出框 简略版的Dialog(自悟) <template> <el-button text @click="open">Click to open the Message Box</el-button> </template> <script lang="ts" setup> import { ElMessage, ElMessageBox } from 'element-plus' import type { Action } from 'element-plus' const open = () => { ElMessageBox.alert('This is a message', 'Title', { // if you want to disable its autofocus // autofocus: false, confirmButtonText: 'OK', callback: (action: Action) => { ElMessage({ type: 'info', message: `action: ${action}`, }) }, }) } </script> (7)Notification 通知 Message用于主动操作后的反馈提示 Notification用于系统级通知的被动提醒 <template> <el-button plain @click="open"> Success </el-button> </template> <script lang="ts" setup> import { ElNotification } from 'element-plus' const open = () => { ElNotification({ title: 'Success', message: 'This is a success message', type: 'success',//可有、可无、可选 }) } </script> (8)Popconfirm 气泡确认框 在被点击元素旁边弹出一个简单的气泡确认框 <template> <el-popconfirm title="Are you sure to delete this?"> <template #reference> <el-button>Delete</el-button> </template> </el-popconfirm> </template> (9)Popover 弹出框(气泡卡片) 在被点击元素旁边弹出一个简单的气泡卡片 <el-popover placement="top-start" title="Title" :width="200" trigger="hover" content="this is content, this is content, this is content" > <template #reference> <el-button class="m-2">Hover to activate</el-button> </template> </el-popover> (10)Tooltip 文字提示 A、在被悬停元素旁边弹出一个简单的气泡卡片 <el-tooltip class="box-item" effect="dark" content="Top Left prompts info" placement="top-start" > <el-button>top-start</el-button> </el-tooltip> B、下拉项,选中太多,悬停满行时,右侧边框不出现 .el-popper.is-light { //写在局部不生效,只能写在全局。YANSHOU-10690 margin-right: 10px; } C、溢出才显示,来源,bcbf-web //以下,组件定义 <template> <el-tooltip placement="top" :disabled="!isShowTooltip" :content="content" effect="dark"> <div class="ellipsis-text" @mouseenter="visibilityChange($event)"> <slot></slot> </div> </el-tooltip> </template> <script setup> const isShowTooltip = ref(false); const content = ref('') // 判断是否显示 溢出的文本 el-tooltip const visibilityChange = (event) => { const ev = event.target; const evWeight = ev.scrollWidth; const contentWeight = ev.clientWidth; console.log(ev) // console.log(ev, evWeight, contentWeight, 1); if (evWeight > contentWeight) { // 实际宽度 > 可视宽度 文字溢出 isShowTooltip.value = true; content.value = ev.innerHTML } else { // 否则为不溢出 isShowTooltip.value = false; } }; </script> <style lang="scss" scoped> .ellipsis-text { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } </style> //以下,组件使用 import CctvEllipsis from '../base/cctv-ellipsis.vue' <CctvEllipsis v-if="scope.row.licenseData.olicense" class="single-line"> <template>{{ item.licenseName }}</template> </CctvEllipsis> 7、Others 其他 (1)Divider 分割线 <el-divider content-position="left">Rabindranath Tagore</el-divider> <el-divider> <el-icon> <star-filled /> </el-icon> </el-divider> <el-divider content-position="right">Rabindranath Tagore</el-divider> 二、element-plus各种提示区别 (1)Alert,正文 <div style="max-width: 600px"> <el-alert title="Success alert" type="success" /> <el-alert title="Info alert" type="info" /> <el-alert title="Warning alert" type="warning" /> <el-alert title="Error alert" type="error" /> </div> (2)大浮层(在页面上) A、MessageBox,弹窗,中断用户操作,模拟alert、confirm和prompt <pre> 附、原生弹窗说明 alert: 1、弹窗有确定按钮,阻塞线程, 2、alert后面的代码,点击确认后才执行 confirm: 1、弹窗有两个按钮(确定和取消),阻塞线程, 2、confirm后面的代码,点击按钮后才执行, 3、点击确定返回true,点击取消返回false prompt: 1、弹窗有一个输入框、两个按钮(确定和取消),阻塞线程 2、prompt后面的代码,点击按钮后才执行, 3、点击确定返回输入内容,为空返回空字符串,点击取消返回null </pre> const open = () => { ElMessageBox.confirm( 'proxy will permanently delete the file. Continue?', 'Warning', { confirmButtonText: 'OK', cancelButtonText: 'Cancel', type: 'warning', } ) .then(() => { ElMessage({ type: 'success', message: 'Delete completed', }) }) .catch(() => { ElMessage({ type: 'info', message: 'Delete canceled', }) }) } B、Message,(操作)提示,3秒后自动消失 const open2 = () => { ElMessage({ message: 'Congrats, this is a success message.', type: 'success', }) } C、Notification,(系统)通知,3秒后自动消失 const open2 = () => { ElNotification({ title: 'Prompt', message: 'This is a message that does not automatically close', duration: 0, }) } (3)小浮层(在元素旁),它们的插槽是正常展示的内容, A、Tooltip,文字提示。隐现方式,hover <el-tooltip content="Bottom center" placement="bottom" effect="light">{/* effect,风格/主题,dark(默认)和light */} <el-button>Light</el-button> </el-tooltip> <el-tooltip placement="top"> <div slot="content">多行信息<br/>第二行信息</div> <el-button>Top center</el-button> </el-tooltip> B、Popconfirm,确认框。隐现方式,点击按钮 <template> <el-popconfirm title="这是一段内容确认?" confirm-button-text="确认" cancel-button-text="取消" icon="el-icon-info"//这是图标,非常重要! icon-color="red" @confirm="handleConfirm" @cancel="handleCancel" > <el-button slot="reference">点击我</el-button>{/* 可能不起作用 */} <template #reference>/* 起作用 */ <el-button type="primary">系统误报</el-button> </template > </el-popconfirm> </template> <script> export default { methods: { handleConfirm() { console.log('确认操作'); }, handleCancel() { console.log('取消操作'); } } } </script> C、Popover,弹出框。隐现方式,trigger(触发,hover、click、focus)或v-model a、以下用法一 <el-popover ref="popoverRef" placement="right" title="标题" width="200" trigger="hover、click、focus" content="这是一段内容"> </el-popover> <el-button v-popover:popoverRef>click</el-button>{/* 触发按钮写在外面 */} <script setup lang="ts"> const popoverRef = ref() </script> b、以下用法二 <el-popover placement="top" width="160" v-model="visible"> <p>此处是一句话</p> <el-table :data="gridData"> <el-table-column width="150" property="date" label="日期"></el-table-column> <el-table-column width="100" property="name" label="姓名"></el-table-column> <el-table-column width="300" property="address" label="地址"></el-table-column> </el-table> <div style="text-align: right; margin: 0"> <el-button size="mini" type="text" @click="visible = false">取消</el-button> <el-button type="primary" size="mini" @click="visible = false">确定</el-button> </div> <el-button slot="reference">删除</el-button> </el-popover> <script lang="ts" setup> const visible = ref(false) </script> 三、element-plus之Form表单验证规则 1、可验证的属性 (1)required:是否必填。{ required: true, message: '该字段不能为空', trigger: 'blur' } (2)max:最大值。{ max: 10, message: '长度不能超过10个字符', trigger: 'blur' } (3)min:最小值。{ min: 6, message: '长度至少为6个字符', trigger: 'blur' } (4)type:数据类型,{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' } 附、取值范围,'number'、'email'、'array'、'date'、'url'、'enum'、'boolean' (5)pattern:正则(必须加required:true)。{ pattern: /^[A-Za-z]+$/, message: '只能输入字母', trigger: 'blur' } (6)len:指定字段长度(优先级高于max,min)。{ len: 5, message: '长度必须为5个字符', trigger: 'blur' } (7)whitespace:是否允许空白字符。{ whitespace: true, message: '不能只输入空格', trigger: 'blur' } 2、属性配置 (1)message:提示内容 (2)trigger:触发条件 (change||blur) (3)validator: 自定义验证逻辑 3、触发验证 (1)单项触发:trigger:change||blur (2)整体触发:submitForm 4、验证范围 (1)单项验证(两种实现) <el-form-item label="邮箱" prop="email" :rules="[ { required: true, message: '请输入邮箱地址', trigger: 'blur' }, { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] } ]"> <el-input v-model="dynamicValidateForm.email"></el-input> </el-form-item> formRef.value.validateField('username', (valid) => { //vue3使用验证 if (valid) { console.log('用户名校验成功'); } else { console.log('用户名校验失败'); } }); (2)整体验证 <el-form :model="ruleForm" :rules="rules" ref="ruleForm" ref="formRef" label-width="100px"> <el-form-item label="活动名称" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item> <el-form-item label="活动区域" prop="region"> <el-select v-model="ruleForm.region" placeholder="请选择活动区域"> <el-option label="区域一" value="shanghai"></el-option> <el-option label="区域二" value="beijing"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button> <el-button @click="resetForm('ruleForm')">重置</el-button> </el-form-item> </el-form> var validateMin = (rule, value, callback) => { if (value <= 0) { callback(new Error('数量必须大于0')); } }; var rules = { name: [{ required: true, message: '请输入奖项名称', trigger: 'blur' }], count: [ { required: true, message: '请输入奖项数量', trigger: 'blur' }, { type: 'number', message: '数量必须为数字', trigger: 'blur' }, { min: 0, validator: validateMin, trigger: 'blur' } ], }; this.$refs.ruleForm.validate((valid) => {//vue2使用验证 if (valid) { alert('submit!'); } else { console.log('error submit!!'); return false; } }); const formRef = ref(null) formRef.value.validate((valid, invalidFields) => {//vue3使用验证 if (valid) { ElMessage.success( props.text + '成功' ); emit('submit', formModel) } else { console.log('Invalid fields:', invalidFields); ElMessage.error('表单验-证失败'); return false; } }); const clearOrReset = function () { formRef.value.clearValidate() //clearValidate,移除验证 formRef.value.clearValidate('date'); //移除验证,且重置为初始值 formRef.value.resetField() formRef.value.resetField('date') }; 四、自定义组件 1、自定义组件-图标svg-icon,来源,ai-web (1)配置 A、package.json,引入依赖 //npm i vite-plugin-svg-icons -D { "devDependencies": { "vite-plugin-svg-icons": "^2.0.1", } } B、vite.config.js,使用插件 import createVitePlugins from './vite/plugins' export default defineConfig( ({command,mode})=>{ plugins: [ createVitePlugins(), ], } ) C、createVitePlugins,存入插件 import vue from '@vitejs/plugin-vue' import createSvgIcon from './svg-icon' export default function createVitePlugins(viteEnv, isBuild = false) { const vitePlugins = [vue()]//插件的存储容器 vitePlugins.push(createSvgIcon(isBuild)) return vitePlugins } D、createSvgIcon,生成插件 import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import path from 'path' export default function createSvgIcon(isBuild) { return createSvgIconsPlugin({ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')], symbolId: 'icon-[dir]-[name]',//与iconName关联,注意iconName的前缀# svgoOptions: isBuild }) } 附、B,C,D步骤可以简化为本步骤 来源,https://blog.csdn.net/weixin_53731501/article/details/125478380 //vite.config.js中配置,使用vite-plugin-svg-icons插件显示本地svg图标 import path from 'path' import {createSvgIconsPlugin} from 'vite-plugin-svg-icons' export default defineConfig((command) => { return { plugins: [ createSvgIconsPlugin({ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],// 指定要缓存的文件夹 symbolId: '[name]'// 指定symbolId格式 }) ], } }) (2)定义 <template> <svg :class="svgClass"> <use :xlink:href="iconName" :fill="color" /> </svg> </template> <script> export default defineComponent({ props: { nameIcon: { type: String, required: true }, className: { type: String, default: '' }, color: { type: String, default: '' }, }, setup(props) { return { iconName: computed(() => `#icon-${props.nameIcon}`), svgClass: computed(() => { if (props.className) { return `svg-icon ${props.className}` } return 'svg-icon' }) } } }) </script> (3)注入 import { createApp } from 'vue' import App from './App.vue' import SvgIcon from '@/components/SvgIcon' const app = createApp(App) app.component('svg-icon', SvgIcon)//全局引入使用 app.mount('#app') (4)使用, <svg-icon name-icon="clock" />,clock指向clock.svg 2、自定义组件-季度半年,来源:bcbf-web 附、注入图标,main.js --注释版 import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) import router from './router' import '@/permission' app.use(router) import store from './store' import useUserStore from '@/store/modules/user' import { findArrByObj } from '@/utils/index' const userStore = useUserStore(store) //defineStore的返回值是useUserStore,本行代码写在$hasPermit内部较为适宜,自悟 app.config.globalProperties.$hasPermit = function (name) {//判断有无权限 let menus = JSON.parse(JSON.stringify(userStore.allMenus)) let find = findArrByObj(menus, name, 'children', 'name') return !!find } app.use(store) //以下注入图标(插件) import * as ElementPlusIconsVue from '@element-plus/icons-vue' for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } //以下注入图标(自定义) import 'virtual:svg-icons-register' //导入一个虚拟模块,自动注册项目中的SVG图标,使项目使用SVG图标时,无需手动导入 import SvgIcon from '@/components/SvgIcon' app.component('svg-icon', SvgIcon) //以下注入全局方法(自定义),格式化数字为美国数字 app.config.globalProperties.$formatNumber = function (value) { if (!value) return ''; let nf = new Intl.NumberFormat('en-US') return nf.format(value); } //以下注入其它 import "element-plus/theme-chalk/el-message-box.css" import "element-plus/theme-chalk/el-loading.css"; import "element-plus/theme-chalk/el-message.css"; import "element-plus/theme-chalk/el-notification.css"; //以下似乎没必要 import '@/mock' app.mount('#app') 附、使用全局方法(自定义),格式化数字为美国数字//manyDate.vue <div>{{ $formatNumber(1234567.89) }}</div> <script setup> import { getCurrentInstance } from 'vue'; var formatNumber = getCurrentInstance().appContext.config.globalProperties.$formatNumber function clickArrow() { var num = formatNumber(1234567.89); }; </script> (1)引入依赖,package.json { "dependencies": { "@element-plus/icons-vue": "^2.1.0", } } (2)定义组件,src\view\report-normal\manyDate.vue <script setup> import { DArrowLeft, DArrowRight, Calendar, } from '@element-plus/icons-vue' const emit = defineEmits(['dateOk']) var { typeObj } = defineProps({ typeObj: { }, //按四季显示,还是按半年显示 isDisabled:{ type:Boolean, default:false } }); var nowWhich = ref('');//当前所在的季节或半年 var clickWhich = ref(typeObj.type);//默认的或被点击的季节或半年 var nowYear = typeObj.year;//当前所在的年份 var usedYear = ref(nowYear);//被使用的年份 var twoHalfYearRef = ref(''); var fourSeasonsRef = ref(''); var dateRangeStr = ref(''); var dateRangeTempStr = ref(''); var isShowTwoHalfYear = ref(false); var isShowFourSeasons = ref(false); function getNowWhich() {//获取当前所在的季节或半年 var type = typeObj.type; var four = ['one','two','three','four','one']; var two = ['up','down','up']; if(four.indexOf(type) > -1){ nowWhich.value = four[four.indexOf(type) + 1] } if(two.indexOf(type) > -1){ nowWhich.value = four[four.indexOf(type) + 1] } } function clickInput() { usedYear.value = nowYear; if( ['up','down'].indexOf(clickWhich.value) !== -1 ){ isShowTwoHalfYear.value = true; isShowFourSeasons.value = false; setTimeout(function(){ twoHalfYearRef.value.focus(); }, 150); } if( ['one','two','three','four'].indexOf(clickWhich.value) !== -1 ){ isShowTwoHalfYear.value = false; isShowFourSeasons.value = true; setTimeout(function(){ fourSeasonsRef.value.focus(); }, 150); } } function getBlur() { isShowTwoHalfYear.value = false; isShowFourSeasons.value = false; } function clickArrow(type) { if(type === 'left') usedYear.value--; if(type === 'right') usedYear.value++; } function addSpace(num) { //添加空格 var str = '\xa0'; for(var i = 0; i < num; i++) str += '\xa0' return str; } function clickHalfYearOrSeason(type, isUseLifecycle, isOnMounted) { clickWhich.value = type; var obj = { one: { start: usedYear.value + '-01', end: usedYear.value + '-03' }, two: { start: usedYear.value + '-04', end: usedYear.value + '-06' }, three: { start: usedYear.value + '-07', end: usedYear.value + '-09' }, four: { start: usedYear.value + '-10', end: usedYear.value + '-12' }, up: { start: usedYear.value + '-01', end: usedYear.value + '-06' }, down: { start: usedYear.value + '-07', end: usedYear.value + '-12' }, }; var start = obj[type].start; var end = obj[type].end; dateRangeTempStr.value = addSpace(6) + start + addSpace(8) + '-' + addSpace(8) + end; if(!isUseLifecycle){ isShowTwoHalfYear.value = false; isShowFourSeasons.value = false; dateRangeStr.value = dateRangeTempStr.value; emit('dateOk',[start, end]) }else if(isOnMounted){ dateRangeStr.value = dateRangeTempStr.value; emit('dateOk',[start, end]) } } onMounted( function() { getNowWhich(); clickHalfYearOrSeason(clickWhich.value, true, true) }); onUpdated( function() { clickHalfYearOrSeason(clickWhich.value, true) }); </script> <template> <div style="width: 100%;"> <el-input v-model="dateRangeStr" :disabled="isDisabled" :readonly="true" @click="clickInput()"> <!-- @blur="outerBlur" --> <template #prefix> <el-icon><Calendar /></el-icon> </template> </el-input> <div class="many-date" tabindex="-1" v-show="isShowFourSeasons" ref="fourSeasonsRef" @blur="getBlur"> <div class="many-date-up"> <el-icon class="many-date-up-content" @click="clickArrow('left')" ><DArrowLeft /></el-icon> <div class="many-date-up-content" >{{ usedYear }}</div> <el-icon class="many-date-up-content" @click="clickArrow('right')"><DArrowRight /></el-icon> </div> <el-divider class="many-date-divider"/> <div class="many-date-down"> <div class="many-date-down-content" @click="clickHalfYearOrSeason('one')" :class="[ nowWhich=='one'?'many-date-down-content-bgNow':null, clickWhich=='one'?'many-date-down-content-bgClick':null, ]" >第一季度</div> <div class="many-date-down-content" @click="clickHalfYearOrSeason('two')" :class="[ nowWhich=='two'?'many-date-down-content-bgNow':null, clickWhich=='two'?'many-date-down-content-bgClick':null, ]" >第二季度</div> </div> <div class="many-date-down"> <div class="many-date-down-content" @click="clickHalfYearOrSeason('three')" :class="[ nowWhich=='three'?'many-date-down-content-bgNow':null, clickWhich=='three'?'many-date-down-content-bgClick':null, ]" >第三季度</div> <div class="many-date-down-content" @click="clickHalfYearOrSeason('four')" :class="[ nowWhich=='four'?'many-date-down-content-bgNow':null, clickWhich=='four'?'many-date-down-content-bgClick':null, ]" >第四季度</div> </div> </div> <div class="many-date" tabindex="-1" v-show="isShowTwoHalfYear" ref="twoHalfYearRef" @blur="getBlur"> <div class="many-date-up"> <el-icon class="many-date-up-content" @click="clickArrow('left')"><DArrowLeft /></el-icon> <div class="many-date-up-content" >{{ usedYear }}</div> <el-icon class="many-date-up-content" @click="clickArrow('right')"><DArrowRight /></el-icon> </div> <el-divider class="many-date-divider"/> <div class="many-date-down"> <div class="many-date-down-content" @click="clickHalfYearOrSeason('up')" :class="[ nowWhich=='up'?'many-date-down-content-bgNow':null, clickWhich=='up'?'many-date-down-content-bgClick':null, ]" >上半年</div> <div class="many-date-down-content" @click="clickHalfYearOrSeason('down')" :class="[ nowWhich=='down'?'many-date-down-content-bgNow':null, clickWhich=='down'?'many-date-down-content-bgClick':null, ]" >下半年</div> </div> </div> </div> </template> <style lang="scss"> .many-date{ width: 60%; margin-left: 20%; border-radius: 4px; margin-top: 10px; position: absolute; z-index: 100; background: var(--el-bg-color-overlay); box-shadow: var(--el-box-shadow-light); &-up { display: flex; justify-content: space-between; align-items: center; padding: 10px 10% ; &-content { cursor: pointer; user-select: none; color: var(--el-text-color-regular); } &-content:hover { color: var(--el-color-primary); } } &-divider { margin: 0 10%; width: 80% } &-down { display: flex; justify-content: space-around; align-items: center; padding: 10px 10% ; &-content { padding: 4px 20px ; cursor: pointer; user-select: none; color: var(--el-text-color-regular); &-bgClick { background: #2162db; border-radius: 18px; color: #ffffff !important; font-weight: bolder; } &-bgNow { color: #2162db; font-weight: bolder; } } &-content:hover { color: var(--el-color-primary); } } } </style> (4)使用组件 <script setup> import manyDate from './manyDate.vue' const ruleForm = ref({ cycle: '', }) const rules = ref({ cycle: [ { required: true, message: '请选择统计周期', trigger: 'blur', }, ], }) //以下为新增或改动内容 var formRef = ref(null); const typeObj = ref({ year: '', type: '', }) function validateSelect() { formRef.value.validateField('period');//校验 } function focusSelect() { formRef.value.clearValidate('period');//清除验证 } function addZero(number) { return number.toString()[1] ? number : "0" + number; } function periodChange() { //需求:可根据选择的周期自动填充上一年、上半年、上一季度、上个月的日期 var date = new Date; var month = date.getMonth(); var fullYear = date.getFullYear(); var period = createForm.value.period; if (period === '月') { if (month == 0) {//如果现在处在今年一月,那么默认为去年十二月 month = 12; fullYear--; } myDate.value = [fullYear + '-' + addZero(1), fullYear + '-' + addZero(month)];//month可以用于表示上个月 } else if (period === '季度') { var four = { 3: 'four', 6: 'one', 9: 'two', 12: 'three', }; for (var key in four) { if (month < key) { if (key == 3) fullYear--;//如果现在处在今年第一季度,那么默认为去年第四季度 typeObj.value.year = fullYear; typeObj.value.type = four[key]; myDate.value = true;//为了绕过为空校验 @blur="validateSelect" break; } } } else if (period === '半年') { var two = { 6: 'down', 12: 'up', } for (var key in two) { if (month < key) { if (key == 6) fullYear--;//如果现在处在今年上半年,那么默认为去年下半年 typeObj.value.year = fullYear; typeObj.value.type = two[key]; myDate.value = true;//为了绕过为空校验 @blur="validateSelect" break; } } } else if (period === '全年') { fullYear--; myDate.value = fullYear.toString(); } } function handleDateOk(data){ myDate.value = data } onMounted( function() { periodChange() }); </script> <template> <el-form ref="formRef" style="max-width: 580px" :model="createForm" :rules="rules" label-width="auto" class="content-table-createForm"> <el-form-item label="报表名称" prop="name"> <el-input v-model="createForm.name" maxlength="50" show-word-limit placeholder="请输入报表名称" /> </el-form-item> <el-form-item label="统计周期" prop="period"> <!-- 如果在触发@change时,不给myDate赋值;那么触发@blur时,验证myDate不通过;many-date渲染后,myDate就有值了 --> <el-select v-model="createForm.period" placeholder="请选择统计周期" @focus="focusSelect" @change="periodChange" @blur="validateSelect" :disabled="!isAdd" > <el-option label="月" value="月" /> <el-option label="季度" value="季度" /> <el-option label="半年" value="半年" /> <el-option label="全年" value="全年" /> </el-select> </el-form-item> <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '月'"> <el-date-picker v-model="myDate" type="monthrange" :editable="false" start-placeholder="开始日期" end-placeholder="结束日期" :disabled="!createForm.period" style="width: 100%;" format="YYYY-MM" value-format="YYYY-MM" @change="handleChange" /> </el-form-item> <!-- 以下,使用自定义-组件 --> <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '季度'"> <many-date :isDisabled="!isAdd" :typeObj="typeObj" @dateOk="handleDateOk" /> </el-form-item> <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '半年'"> <many-date :isDisabled="!isAdd" :typeObj="typeObj" @dateOk="handleDateOk" /> </el-form-item> <!-- 以上,使用自定义-组件 --> <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '全年'"> <el-date-picker v-model="myDate" type="year" :editable="false" start-placeholder="开始日期" end-placeholder="结束日期" :disabled="!createForm.period" style="width: 100%;" format="YYYY" value-format="YYYY" /> </el-form-item> <el-form-item label="考核规则版本" prop="rule" style="position: relative;"> <el-select v-model="assessmentRule" value-key="sid" placeholder="请选择考核规则"> <el-option :label="item.name" :value="item" v-for="item in rulesData" :key="item.ruleSid" /> </el-select> <el-button type="primary" text @click="jumpVersionDetail" class="jump-version-btn">查看详情</el-button> </el-form-item> <el-form-item label="报表字段" prop="displayField"> <el-button type="primary" :icon="Edit" @click="showColumnDialog" /> </el-form-item> </el-form> </template> 3、自定义组件-26小时-半成品,可演示,来源:bcbf-web (1)定义组件 <template> <div class="timeInputBox" :id="'t1-'+id" :ref="'ref-'+id"> <div style="display: flex;"> <el-input ref="timeInput" :id="'t2-'+id" @focus="focus" v-model="input" :placeholder="placeholder" class="centered-input"/> <span style="padding: 0 50px;">至</span> <el-input ref="timeInput" :id="'t2-'+id" @focus="focus" v-model="input" :placeholder="placeholder" class="centered-input"/> </div> <div :class="{ menuBox: true, activemenuBox: isShowOption }" :id="'t3-'+id"> <div class="triangle" :id="'t4-'+id"></div> <div class="menuMain" :id="'t5-'+id" style="display: flex"> <div class="two"> <div class="container" :id="'t6-'+id" ref="startHour"> <div class="timeItem" :id="'t7-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-1'" /> <div @click="clickTime('startHour', index)" :ref="'timeItem'+index" v-for="(num, index) in timeList.hour" :key="index+'01'" :class="{ activeTime: index == selectHourIndex }" :id="'t8-'+id">{{ textbuil(num) }} </div> <div class="timeItem" :id="'t9-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-2'" /> </div> <div class="container" :id="'t10-'+id" ref="startMinute"> <div class="timeItem" :id="'t11-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-3'" /> <div :id="'t12-'+id" @click="clickTime('startMinute', index)" v-for="(num, index) in timeList.minute" :key="index+'02'" :class="{ activeTime: index == selectMinuteIndex }">{{ num }} </div> <div class="timeItem" :id="'t13-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-4'" /> </div> <div class="container" :id="'t30-'+id" ref="startSecond"> <div class="timeItem" :id="'t31-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-3'" /> <div :id="'t32-'+id" @click="clickTime('startSecond', index)" v-for="(num, index) in timeList.second" :key="index+'02'" :class="{ activeTime: index == selectSecondIndex }">{{ num }} </div> <div class="timeItem" :id="'t33-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-4'" /> </div> <div class="botBut" :id="'t4-'+id"> </div> <div class="line" :id="'t17-'+id" :style="{ top: `calc(50% - ${clientHeight - 5}px)` }"></div> <div class="line" :id="'t18-'+id" :style="{ top: `calc(50% + ${5}px)` }"></div> </div> <div style="width:10%">至</div> <div class="two"> <div class="container" :id="'t6-'+id" ref="overHour"> <div class="timeItem" :id="'t7-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-1'" /> <div @click="clickTime('overHour', index)" :ref="'timeItem'+index" v-for="(num, index) in timeList.hour" :key="index+'01'" :class="{ activeTime: index == selectHourIndex }" :id="'t8-'+id">{{ textbuil(num) }} </div> <div class="timeItem" :id="'t9-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-2'" /> </div> <div class="container" :id="'t10-'+id" ref="overMinute"> <div class="timeItem" :id="'t11-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-3'" /> <div :id="'t12-'+id" @click="clickTime('overMinute', index)" v-for="(num, index) in timeList.minute" :key="index+'02'" :class="{ activeTime: index == selectMinuteIndex }">{{ num }} </div> <div class="timeItem" :id="'t13-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-4'" /> </div> <div class="container" :id="'t30-'+id" ref="overSecond"> <div class="timeItem" :id="'t31-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-3'" /> <div :id="'t32-'+id" @click="clickTime('overSecond', index)" v-for="(num, index) in timeList.second" :key="index+'02'" :class="{ activeTime: index == selectSecondIndex }">{{ num }} </div> <div class="timeItem" :id="'t33-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]" :key="item+'-4'" /> </div> <div class="botBut" :id="'t4-'+id"> <el-button class="elbutton" :id="'t15-'+id" type="text" @click="confirmTime()">确定</el-button> <el-button class="elbutton" :id="'t16-'+id" style="color: #333;" type="text" @click="cancelTime()">取消</el-button> </div> <div class="line" :id="'t17-'+id" :style="{ top: `calc(50% - ${clientHeight - 5}px)` }"></div> <div class="line" :id="'t18-'+id" :style="{ top: `calc(50% + ${5}px)` }"></div> </div> </div> </div> </div> </template> <script> export default { name: "timePicker", components: {}, props: { id: { //多个时必传且不能相同 type: String, default: '', }, time: { //格式,10:30 type: String, default: '00:00:00', }, placeholder: { type: String, default: '请选择时间', }, timeList: { //hour,24时=>次日00时,25时=>次日01时,以此类推 type: Object, default: () => { return { hour: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"], minute: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "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"], second: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "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"], } } } }, data() { return { startTime: '', overTime: '', input: '', isMounted: true, isShowOption: false, clientHeight: 36, selectHourIndex: -1, selectMinuteIndex: -1, selectSecondIndex: -1, setTimeing: false, }; }, mounted() { // 点击页面其他地方关闭时间选择组件 document.addEventListener('click', (e) => { const ids = e.target.id?.split("-")[1] if (this.id != ids) { this.isShowOption = false } }) // 监听时/分/秒滚动 this.$refs.startHour.addEventListener('scroll', () => { const scrollTop = this.$refs.startHour.scrollTop const index =this.selectHourIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字 this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] }) this.$refs.startMinute.addEventListener('scroll', () => { const scrollTop = this.$refs.startMinute.scrollTop; const index =this.selectMinuteIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字 this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] }) this.$refs.startSecond.addEventListener('scroll', () => { const scrollTop = this.$refs.startSecond.scrollTop; const index =this.selectSecondIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字 this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] }) // 监听时/分/秒滚动 this.$refs.overHour.addEventListener('scroll', () => { const scrollTop = this.$refs.overHour.scrollTop const index =this.selectHourIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字 this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] }) this.$refs.overMinute.addEventListener('scroll', () => { const scrollTop = this.$refs.overMinute.scrollTop; const index =this.selectMinuteIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字 this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] }) this.$refs.overSecond.addEventListener('scroll', () => { const scrollTop = this.$refs.overSecond.scrollTop; const index =this.selectSecondIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字 this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] }) this.setInput() }, methods: { focus() { this.isShowOption = true this.$emit('focus') }, clickTime(type, index) { this.$refs[type].scrollTop = index * this.clientHeight; }, confirmTime() { if (this.setTimeing) return this.setTimeing = true setTimeout(() => { this.setTimeing = false }, 100); // 以上阻止连续点击 this.startTime = this.timeList.hour[this.selectHourIndex]+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] this.startTime = this.timeList.hour[this.selectHourIndex]+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex] this.$emit('sendTime', this.startTime); this.isShowOption = false }, cancelTime() { this.$emit('sendTime', this.startTime); this.isShowOption = false }, textbuil(data) { /* if (data > 23) { data = data * 1 return '次日0'+(data - 24) } */ return data }, setInput() { const [hourDistance, minuteDistance, secondDistance] = this.time.split(':') this.startTime = this.input = this.textbuil(hourDistance)+':'+minuteDistance+':'+secondDistance }, }, watch: { isShowOption: { handler: function (newVal) { if (newVal) { var num = this.isMounted? 8:0; this.isMounted = false; this.clientHeight = this.$refs.timeItem0[0].clientHeight; const [hourDistance = 0, minuteDistance = 0, secondDistance = 0] = this.startTime.split(':'); this.$refs.startHour.scrollTop = hourDistance * this.clientHeight + num; this.$refs.startMinute.scrollTop = minuteDistance * this.clientHeight + num; this.$refs.startSecond.scrollTop = secondDistance * this.clientHeight + num; ///////////////////////////////////////////// this.$refs.overHour.scrollTop = hourDistance * this.clientHeight + num; this.$refs.overMinute.scrollTop = minuteDistance * this.clientHeight + num; this.$refs.overSecond.scrollTop = secondDistance * this.clientHeight + num; } } }, }, }; </script> <style lang="scss"> .timeInputBox { position: relative; .menuBox { position: absolute; width: 500px; height: 230px; z-index: 9; transition: .25s; overflow: hidden; transform: scaleY(0); transform-origin: top; box-shadow: 10px 9px 20px -10px #e1dfdf; .triangle { width: 0; height: 0; border-top: 6px solid transparent; border-bottom: 6px solid #fff; border-left: 6px solid transparent; border-right: 6px solid transparent; margin-left: 10px; margin-bottom: -1px; z-index: 10; position: absolute; top: 0; left: 10px; } .menuMain { background-color: #fff; height: calc(100% - 10px); width: 100%; border-radius: 4px; border: 1px solid #eaeaea; z-index: 9; margin-top: 10px; box-shadow: 2px 1px 20px -11px #333; display: flex; .two { width: 40%; .container { float: left; height: calc(100% - 36px); width: 33%; overflow: auto; text-align: center; font-size: 12px; color: #606266; cursor: pointer; :hover { background-color: #4d7fff16; } .timeItem:hover { background-color: transparent; } .activeTime { font-weight: bold; color: #000; &:hover { background-color: transparent; } } } .botBut { width: 50%; height: 36px; border-top: 1px solid #d7d7d7; position: absolute; bottom: 2px; background-color: #ffffff; .elbutton { font-size: 12px; float: right; margin: auto 10px; font-weight: 500; } } .line { position: absolute; height: 1px; width: calc((100% - 40px)); left: 20px; background-color: #e1e1e1; // border-top: 1px solid #e1e1e1; // border-bottom: 1px solid #e1e1e1; } } } } .activemenuBox { transform: scaleY(1); } } .container::-webkit-scrollbar { width: 7px; // height: 5px; } .container::-webkit-scrollbar-thumb { // background: linear-gradient(to bottom right, #4d7fff 0%, #1a56ff 100%); background-color: #dfdfdf; border-radius: 5px; } .container::-webkit-scrollbar-track { background-color: #fafafa; // border: 1px solid #ccc; } .container::-webkit-scrollbar-button { background-color: #fff; border-radius: 5px; } .container::-webkit-scrollbar-button:hover { background-color: #c1c1c1; } .centered-input .el-input__inner { text-align: center !important; } </style> (2)使用组件 import TimePicker from "./timePicker"; <el-form-item label="播出时间--1" > <TimePicker :id="'32'" :time="'20:34:58'" /> </el-form-item> 4、自定义组件-添加水印,来源:bcbf-web 附、添加水印组件的实现逻辑 A、向组件传递图片地址、水印初始文字、监听事件(把base64的url转化成二进制文件,发给后台) B、在组件中,监听图片地址,加载图片完成后,将图片和初始文字分别绘制到canvas上 C、在组件中,监听文字配置,如旋转角度变化时,将文字绘制到canvas上 D、在组件中,发射事件,将canvas的base64的url发给父组件 附、为什么vue3的有些逻辑写在watch里,而不写在updated里 A、逻辑写在updated里,watch数据没有改变时,逻辑也会执行 (1)定义组件 <script setup> const props = defineProps({ imgUrl: { type: String, default: '' }, content:{ type: String, default: '' } }); const emit = defineEmits(['watermarkUpdate']); let maxHeight = ref(650) let maxWidth = ref(540) const config = reactive({ content: '', fontSize: 22, color: 'rgba(157, 141, 141, 0.86)', rotate: -22, gap: [10, 10], }); const graph = ref(null); const canRefVas = ref(null); const tempImg = ref(null); //这是一个img标签,里面包含src属性,指向图片地址 const loading = ref(false) const handleSave = () => { loading.value = true // toDataURL // const imageData = canRefVas.value.toDataURL('image/png'); // 将canvas对象中的图像数据转换为base64编码的dataURL // 这种dataURL可以直接在HTML中使用,或者通过JavaScript发送给服务器进行保存或传输 emit('watermarkUpdate', imageData, config.content); loading.value = false }; const drawImg = () => { // 重新设置canvas的宽高,会清空画布 let w = tempImg.value.width; let h = tempImg.value.height; canRefVas.value.width = w; canRefVas.value.height = h; let hs = maxHeight.value/h; let ws = maxWidth.value/w; if(hs>ws){ maxHeight.value = h*ws maxWidth.value = 540 } else { maxWidth.value = w*hs maxHeight.value = 650 } console.log('----',tempImg.value.width, tempImg.value.height, maxHeight.value, maxWidth.value) canRefVas.value.getContext('2d').drawImage(tempImg.value, 0, 0, w, h); // drawImage(image, x, y, width, height)方法用于在canvas上下文中绘制图像 // image:要绘制的图像源。这可以是一个 // HTMLImageElement(例如通过new Image()创建并加载后的图像对象) // HTMLCanvasElement(另一个canvas元素) // HTMLVideoElement(视频元素) // x和y:图像在画布上的起始坐标,坐标原点(0,0)位于canvas的左上角 // width和height:用于指定绘制图像的宽度和高度,可选 // 如果不提供,图像将以其原始大小进行绘制;如果提供,可以对图像进行缩放,假如, // 图像的宽高为(3840 2160),正好铺满标签样式(class、style)的宽高,<canvas style="width:400px;height:400px"></canvas> // 设置为(1920,1080),图像的宽高将缩小为原来的一半绘制在canvas标签里,标签里将会有3/4的空间是空白 // 设置为(7680,4320),图像的宽高将放大为原来的二倍绘制在canvas标签里,图像将会有3/4的面积隐藏在标签外 // 图像的宽高绘制在标签样式的宽高上,图像的宽高导致,标签属性的宽高失效,<canvas width=400 height=400></canvas> // 图像的宽高、标签属性的宽高、标签样式的宽高 }; const drawText = () => { /* if(!config.content){//http://10.70.38.84/browse/YANSHOU-10133 return } */ drawImg(); let w = tempImg.value.width; config.fontSize = parseInt(w/30); let textCtx = canRefVas.value.getContext('2d'); textCtx.fillStyle = config.color; textCtx.font = config.fontSize + 'px Arial' ; //以下获取文字的宽高 let text = textCtx.measureText('啊') let textWidth = text.width; let textHeight = text.actualBoundingBoxAscent + text.actualBoundingBoxDescent; //基线:是一条虚拟的线,文本中的字符都是基于这条线进行排列的(26个大小写字母在四线三格中的书写规范) //actualBoundingBoxAscent:从文本的基线到文本的最顶部的垂直距离,它衡量的是文本在基线上方所占据的空间高度 //actualBoundingBoxDescent:从文本的基线到文本的最底部的垂直距离。它衡量的是文本在基线下方所占据的空间高度 const list = config.content? config.content.split(/[(\r\n)\r\n]+/):[]; //一次水印可能有多行 let multiple = 0; list.forEach((item) => { multiple = Math.max(multiple, item.length); }); console.log(list) let maxWidth = multiple*textWidth + config.gap[0]*10; //一次水印所需的最大宽度 let maxHeight = multiple*textHeight + config.gap[1]*10; //一次水印所需的最大高度 maxWidth = Math.max(maxWidth, 100); maxHeight = Math.max(maxHeight, 100); let iii = 0; for (let x=0; x<canRefVas.value.width; x+=maxWidth) { //每行多次水印的总宽度不超过画布的宽度(从左向右绘制) for (let y=0,z=0; y<canRefVas.value.height; y+=maxHeight,z++) { //每列多次水印的总高度不超过画布的高度(从上向下绘制) textCtx.save(); //保存当前环境 console.log('--'+(iii++)+'--', x, [0,maxWidth/2][z%2], y ); textCtx.translate(x+[0,maxWidth/2][z%2], y); //translate(x,y)每次水印的起始坐标(x,y) textCtx.rotate(config.rotate*Math.PI/180); //每次水印,文字旋转的角度 list.forEach((item, index) => {//每次水印 textCtx.fillText(item, 0, (config.fontSize+80)*index);//textCtx.fillText(text,x,y)每行文字的起始坐标(x,y),与strokeText相似 }); textCtx.restore(); //返回已保存过的环境 } } }; watch(() => props.imgUrl, () => { if (props.imgUrl.length === 0) { return; } tempImg.value = new Image; tempImg.value.onload = function () { drawText(); return canRefVas.value; }; config.content = props.content; tempImg.value.src = props.imgUrl; }, { immediate: true }); watch(config, () => { drawText() }) </script> <template> <div class="watermark__wrap"> <div ref="graph" class="watermark__image"> <canvas ref="canRefVas" :style="{'max-height': maxHeight+'px','max-width':maxWidth+'px'}" ></canvas> </div> <el-form class="watermark__form" :model="config" label-position="top" label-width="50px"> <el-form-item label="文字"> <el-input type="textarea" :rows="6" style="width: 600px" maxlength="100" placeholder="请输入水印信息" show-word-limit v-model="config.content" /> </el-form-item> <el-form-item label="颜色"> <el-color-picker v-model="config.color" show-alpha /> </el-form-item> <el-form-item label="字号"> <el-slider v-model="config.fontSize" :max="500" /> </el-form-item> <el-form-item label="旋转"> <el-slider v-model="config.rotate" :min="-180" :max="180" /> </el-form-item> <el-form-item label="间隙"> <el-space> <el-input-number v-model="config.gap[0]" controls-position="right" /> <el-input-number v-model="config.gap[1]" controls-position="right" /> </el-space> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSave" :loading="loading">确定</el-button> </el-form-item> </el-form> </div> </template> <style lang="scss"> .watermark { &__wrap { display: flex; flex-direction: row; } &__image { display: flex; align-items: center; justify-content: center; width: 558px; margin-right: 10px; height: 650px; } } </style> (2)使用组件 <el-dialog v-model="showEditWatermark" width="1200"> <Watermark :imgUrl="editWatermarkImageUrl" v-if="showEditWatermark" :content="watermark" @watermarkUpdate="handleWatermarkUpdate"/> </el-dialog> const handleWatermarkUpdate = (imageURL, content) => {//添加水印 showEditWatermark.value = false; applicationInfo.value.licenses[editWatermarkIndex.value].licenseImageUrl = imageURL; applicationCheckForm.value.licenses[editWatermarkIndex.value].watermark = content let file = { raw: dataURLtoFile( imageURL, applicationInfo.value.licenses[editWatermarkIndex.value].licenseName + '.png' ) } uploadFileTool( file, 'license' ).then(res => { applicationCheckForm.value.licenses[editWatermarkIndex.value].fileId = res.data.fileInfoId }) } // 将base64转换为文件 function dataURLtoFile(dataUrl, filename) { var arr = dataUrl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mime }); } 5、自定义组件-多级菜单,来源:dyxh-collect-manage (1)定义组件//SidebarItem.vue <template> <div v-if="!item.hidden"> <template v-if="hasOneShowingChild(item.children, item)"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }" > <item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" /> </el-menu-item> </app-link> </template> <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body > <template #title> <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> </template> <sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> </el-sub-menu> </div> </template> <script> export default { name: "SidebarItem", }; </script> (2)使用组件 <template> <div class="has-logo"> <logo :collapse="isCollapse" v-if="false"/> <el-scrollbar wrap-class="scrollbar-wrapper"> <el-menu :default-active="activeMenu" :collapse="isCollapse" :unique-opened="true" :collapse-transition="false" mode="vertical" > <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> </el-menu> </el-scrollbar> </div> </template>