一、vue之分页组件(含勾选、过滤)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>勾选和分页组件之vue2.6.10版</title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<style>
table{
border-collapse: collapse;
border: 1px solid #cbcbcb;
width: 1000px;
}
table td,table th {
padding: 5px;
border: 1px solid #cbcbcb;
}
table thead {
background-color: #e0e0e0;
color: #000;
text-align: left;
}
.filter{
width:998px;
border:1px solid gray;
padding:10px 0px;
}
.line{
display:flex
}
.group{
width:330px;
}
.label{
display: inline-block;
width:120px;
height: 24px;
line-height: 24px;
text-align: right;
}
.input{
display: inline-block;
width:180px;
height: 24px;
line-height: 24px;
border-radius: 3px;
}
.select{
display: inline-block;
width:188px;
height: 26px;
line-height: 26x;
border-radius: 3px;
}
</style>
</head>
<body>
<div id="app">
<div style="padding-bottom:5px;color:red">
<button style="color:red" @click="checkDatasOne.getResultOfCheckAndFilter(divideDatasOne.isShowFilter,divideDatasOne.filterOptions)">获取勾选和过滤结果</button>
<span>{{checkDatasOne.toServerDatas}}</span>
</div>
<div style="padding-bottom:5px">
<img :src="checkDatasOne.stateAllPages&&checkDatasOne.allExcludedIds.length===0?checkImg.yes:checkImg.no" @click="checkDatasOne.clickAllPages(divideDatasOne.tableDatas)"/>
<span>{{checkDatasOne.textAllPages}}</span>
</div>
<div style="padding-bottom:5px">
<button @click="divideDatasOne.toggleShowFilter()">{{divideDatasOne.isShowFilter?'关闭过滤':'使用过滤'}}</button>
<button @click="divideDatasOne.emptyFilterOptions({value5:10})">清空过滤</button>
<button @click="divideDatasOne.request(1,divideDatasOne.eachPageItemsNum)">刷新</button>
</div>
<div style="margin-bottom:5px" class="filter" v-show="divideDatasOne.isShowFilter">
<div class="line">
<div class="group">
<label class="label">标签</label>
<input class="input" type="text" v-model="divideDatasOne.filterOptions.value1" />
</div>
<div class="group">
<label class="label">这就是长标签</label>
<input class="input" type="text" v-model="divideDatasOne.filterOptions.value2" />
</div>
<div class="group">
<label class="label">标签</label>
<input class="input" type="text" v-model="divideDatasOne.filterOptions.value3" />
</div>
</div>
<div class="line" style="padding-top: 10px;">
<div class="group">
<label class="label">这就是长标签</label>
<input class="input" type="text" v-model="divideDatasOne.filterOptions.value4" />
</div>
<div class="group">
<label class="label">下拉框</label>
<select class="select" v-model="divideDatasOne.filterOptions.value5">
<option v-for="item in selectOptions" :value="item.back">{{item.front}}</option>
</select>
</div>
<div class="group">
<label class="label"></label>
<button style="width:188px;height:28px" @click="divideDatasOne.request(1,divideDatasOne.eachPageItemsNum)">过滤</button>
</div>
</div>
</div>
<div style="width: 1000px">
<table>
<thead>
<tr>
<th><img :src="checkDatasOne.stateThisPage?checkImg.yes:checkImg.no"
@click="checkDatasOne.clickThisPage(divideDatasOne.tableDatas,divideDatasOne.allItemsNum)"/></th>
<th>序号</th>
<th>数据1</th>
<th>数据2</th>
<th>数据3</th>
<th>数据4</th>
<th>数据5</th>
<th>数据6</th>
</tr>
</thead>
<tbody>
<tr v-for="(data,index) in divideDatasOne.tableDatas">
<td><img :src="data.state?checkImg.yes:checkImg.no" @click="checkDatasOne.clickSingleItem(data,divideDatasOne.tableDatas,divideDatasOne.allItemsNum)"/></td>
<td>{{(divideDatasOne.nowPageNum-1)*divideDatasOne.eachPageItemsNum + (index+1)}} </td>
<td>{{ data.key1 }}</td>
<td>{{ data.key2 }}</td>
<td>{{ data.key3 }}</td>
<td>{{ data.key4 }}</td>
<td>{{ data.key5 }}</td>
<td>{{ data.key6 }}</td>
</tr>
</tbody>
</table>
</div>
<divide-page :divide-datas="divideDatasOne" :check-datas="checkDatasOne" :fixed-datas="fixedDatas"></divide-page>
</div>
</body>
<script>
new Vue({
el: '#app',
data(){
return {
divideDatasOne:{
nowPageNum:0,
allPagesNum:0,
allItemsNum:0,
eachPageItemsNum:0,
tableDatas:[],
filterOptions:{value5:10},
isShowFilter:false,
otherDatas:{}
},
checkDatasOne:{
idKey: 'id',//每条数据的唯一标志
stateThisPage: false,//当前页所有项是否全选
allIncludedIds: [],//所有被选中数据的ID构成的数组
allExcludedIds: [],//所有没被选中数据的ID构成的数组
textAllPages: '全选未启用,没有选择任何项!',//复选框被点击后的提示文字。
stateAllPages: false,//复选框被点击后的提示文字。
toServerDatas: null,
},
}
},
methods: {
},
created(){
this.fixedDatas = {};
this.selectOptions = [
{ back: 10, front: '来' },
{ back: 20, front: '来自于' },
{ back: 30, front: '来自于国内' },
{ back: 40, front: '来自于国内攻击' },
{ back: 50, front: '来自于国内攻击-2' }
];
this.checkImg = {
yes: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAADqADAAQAAAABAAAADgAAAAC98Dn6AAAA+UlEQVQoFZWSMU4DMRBF/584G7QSRcIxuAZKEykNEiUVHVTQRaKh4AIcgAvQpkukVDlBOAYNSGSlXXuwpViyYYFdS9aMZ/6bsezh5HZ3T2KhqkfosEhWqnjkyd1u3xWKdQMsfaEAB0Zilf8swfdU0w0klmpGpz1BvpbHcklbPf8Okts0CfJtWBTz/Yc++Jc8S3PZVQfKGwiuvMD6XYsMzm1dT/1jXKdQ8E0asHRrAzOzbC6UGINWHPQp1UQ/6wjF2LpmJSKfhti4Bi8+lhWP4I+gAqV1uqSi8j9WRuF3m3eMWVUJBeKxzUoYn7bEX7HDyPmB7QEHbRjyL+/+VnuXDUFOAAAAAElFTkSuQmCC',
no: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAADqADAAQAAAABAAAADgAAAAC98Dn6AAAAbklEQVQoFWM8c+ZMLQMDQxUQcwAxMeAHUFEbC5CoYmNj02ZmZn5FjK6/f/+K/fr16ypIIwdIk7a29hdiNF69ehWkjIOJGMXY1IxqxBYqULEhFDiglPMDlIygKQKPryBSILUgPSCNbaC0B6RJSuQAbowizhJuOsAAAAAASUVORK5CYII=',
}
},
components: {
dividePage: {
props: {
divideDatas: {
type: Object,
default: {}
},
checkDatas: {
type: Object,
default: {}
},
fixedDatas: {
type: Object,
default: {}
}
},
template: `
<div v-show="divideDatas.allPagesNum>=1" style="display:flex;width:1000px;margin-top:20px;">
<div>
<button
v-show="divideDatas.allPagesNum>10"
@click="clickDividePage('front') "
:disabled="divideDatas.nowPageNum===1"
>上一页</button>
<button
:disabled="number==='...'"
v-for="number in divideArray"
@click="clickDividePage(number)"
:style="{marginRight:'5px',color:number===divideDatas.nowPageNum?'red':'gray'}"
>{{ number }}</button>
<button
v-show="divideDatas.allPagesNum>10"
@click="clickDividePage('back')"
:disabled="divideDatas.nowPageNum===divideDatas.allPagesNum"
>下一页</button>
</div>
<div style="display:flex; flex:1; justify-content:flex-end;">
<div style="margin-right:20px;">
<span>转到第</span>
<input type="text" v-model="customString" @keydown="clickDividePage('leap',$event)" style="width:30px;">
<span>页</span>
<button @click="clickDividePage('leap',{which:13})">Go</button>
</div>
<div>
<span>每页显示</span>
<select v-model="divideDatas.eachPageItemsNum" @change="selectChange(divideDatas.eachPageItemsNum)">
<option v-for="item in numOptions" :value="item.back">{{item.front}}</option>
</select>
<span>条,</span>
</div>
<div>
<span>{{frontMoreText}}</span>
<span>{{totalText}}</span>
<span>{{divideDatas.allItemsNum}}</span>
<span>{{totalUnit}}</span>
<span>{{backMoreText}}</span>
</div>
</div>
</div
`,
data() {
return {
customString:''
}
},
created(){
var that = this;
//1、请求配置
this.url = this.fixedDatas.url || '';
this.method = this.fixedDatas.method || 'post';
this.isShowParams = this.fixedDatas.isShowParams || false;//显式还是隐式传参。有时需要在请求发出前手动改变。
//2、响应配置(前端通过这个配置,获取后台的数据)
this.nowPageNum = this.fixedDatas.nowPageNum || 'nowPageNum';//来自服务器的当前页码
this.allPagesNum = this.fixedDatas.allPagesNum || 'allPagesNum';//来自服务器的所有页页数
this.allItemsNum = this.fixedDatas.allItemsNum || 'allItemsNum';//来自服务器的所有页数据数
this.eachPageItemsNum = this.fixedDatas.eachPageItemsNum || 'eachPageItemsNum';//来自服务器的每页最多数据数
this.tableDatas = this.fixedDatas.tableDatas || 'tableDatas';//来自服务器的表格数据
//3、以下配置使用哪种转圈方式(前端根据需要决定,不受后台影响)
this.partCircle = this.fixedDatas.partCircle;//局部是否转圈。this.fixedDatas.partCircle=$scope.partCircle={isShow =false}。
this.isUsePartCircle = this.fixedDatas.isUsePartCircle;//局部是否转圈,由当前页的一个变量控制
this.isUseWholeCircle = this.fixedDatas.isUseWholeCircle;//全局是否转圈,由本项目的一个服务控制
//4、初始化以下数据,供页面使用(前端根据需要决定,不受后台影响)
this.frontMoreText = this.fixedDatas.frontMoreText || "";//('文字 ')或者("文字 "+result.numOne+" 文字 ")
this.totalText = this.fixedDatas.totalText || "";//'共'
this.totalUnit = this.fixedDatas.totalUnit || '条';//总数据的单位
this.backMoreText = this.fixedDatas.backMoreText || "";//(' 文字')或者("文字 "+result.numThree+" 文字")
this.numOptions = [
{ back: 10, front: 10 },
{ back: 20, front: 20 },
{ back: 30, front: 30 },
{ back: 40, front: 40 },
{ back: 50, front: 50 }
];
this.request = this.divideDatas.request = function (nowPageNum,eachPageItemsNum) {
var isOnce = true;
//1、向后台发送请求,
//2、返回正确结果result
var data=[];
var allItemsNum = 193;
var nowPageNum = nowPageNum||1;
var eachPageItemsNum = eachPageItemsNum||10;
var allPagesNum = Math.ceil(allItemsNum/eachPageItemsNum);
for(var i=1;i<=allItemsNum;i++){
var obj={
id:'id'+i,
key1:'数据'+(i+0),
key2:'数据'+(i+1),
key3:'数据'+(i+2),
key4:'数据'+(i+3),
key5:'数据'+(i+4),
key6:'数据'+(i+5),
key7:'数据'+(i+6),
};
data.push(obj)
}
var tableDatas = data.slice((nowPageNum-1)*eachPageItemsNum,nowPageNum*eachPageItemsNum);
//3、使用正确结果result
that.customString = nowPageNum;
that.divideDatas.nowPageNum = nowPageNum;
that.divideDatas.allPagesNum = allPagesNum;
that.divideDatas.allItemsNum = allItemsNum;
that.divideDatas.eachPageItemsNum = eachPageItemsNum;
that.divideDatas.tableDatas = tableDatas;//正常逻辑
if(that.divideDatas.once && isOnce){//只使用一次,常用于初始化一些数据,比如过滤条件
that.divideDatas.once();
isOnce = false;
}
if(that.divideDatas.trueCb){//异常逻辑
that.divideDatas.trueCb()
}
if(that.checkDatas && that.checkDatas.signCheckbox){//处理勾选
that.checkDatas.signCheckbox(that.divideDatas.tableDatas)
}
that.createDividePage();//创建分页
//4、处理错误结果
if(that.divideDatas.errorCb){
that.divideDatas.errorCb()
}
};
if (!this.divideDatas.isAbandonInit) {
this.request(1,this.divideDatas.eachPageItemsNum);
};
this.divideDatas.toggleShowFilter = function () {
this.isShowFilter = !this.isShowFilter;
if (!this.isShowFilter) {
this.request(1,that.divideDatas.eachPageItemsNum);
}
};
this.divideDatas.emptyFilterOptions = function (extraObject) {
//清空选项时,所有值恢复成默认
for(var key in this.filterOptions){
this.filterOptions[key] = undefined;
};
if (extraObject) {
//小部分选项的默认值不是undefined
for(var key in extraObject){
this.filterOptions[key] = extraObject[key];
};
};
this.request(1,that.divideDatas.eachPageItemsNum);
};
this.checkDatas.init=function(){//点击“刷新”、“过滤”、“清除过滤”时执行
this.idKey = idKey ? idKey : 'id';
this.allIncludedIds = [];
this.allExcludedIds = [];
this.textAllPages = '全选未启用,没有选择任何项!';
this.stateAllPages = false;
this.stateThisPage = false;
};
this.checkDatas.clickAllPages = function (itemArray) {//所有页所有条目全选复选框被点击时执行的函数
if(this.stateAllPages){
if(this.allExcludedIds.length>0){
this.stateAllPages = true;
this.stateThisPage = true;
this.textAllPages= '全选已启用,没有排除任何项!';
itemArray.forEach(function (item) {
item.state = true;
});
}else if(this.allExcludedIds.length==0){
this.stateAllPages = false;
this.stateThisPage = false;
this.textAllPages= '全选未启用,没有选择任何项!';
itemArray.forEach(function (item) {
item.state = false;
});
}
}else{
this.stateAllPages = true;
this.stateThisPage = true;
this.textAllPages= '全选已启用,没有排除任何项!';
itemArray.forEach(function (item) {
item.state = true;
});
}
this.allExcludedIds = [];
this.allIncludedIds = [];
};
this.checkDatas.clickThisPage = function (itemsArray,allItemsNum) {//当前页所有条目全选复选框被点击时执行的函数
var that = this;
this.stateThisPage = !this.stateThisPage
itemsArray.forEach(function (item) {
item.state = that.stateThisPage;
if (item.state) {
that.delID(item[that.idKey], that.allExcludedIds);
that.addID(item[that.idKey], that.allIncludedIds);
} else {
that.delID(item[that.idKey], that.allIncludedIds);
that.addID(item[that.idKey], that.allExcludedIds);
}
});
if(this.stateAllPages){
if(this.stateThisPage && this.allExcludedIds.length === 0){
this.textAllPages = '全选已启用,没有排除任何项!';
}else{
this.textAllPages = '全选已启用,已排除'+ this.allExcludedIds.length + '项!排除项的ID为:' + this.allExcludedIds;
}
}else{
if(!this.stateThisPage && this.allIncludedIds.length === 0){
this.textAllPages='全选未启用,没有选择任何项!';
}else{
this.textAllPages = '全选未启用,已选择' + this.allIncludedIds.length + '项!选择项的ID为:' + this.allIncludedIds;
}
}
};
this.checkDatas.clickSingleItem = function (item, itemsArray, allItemsNum) {//当前页单个条目复选框被点击时执行的函数
var that = this;
item.state = !item.state;
if (item.state) {
this.stateThisPage = true;
this.addID(item[this.idKey], this.allIncludedIds);
this.delID(item[this.idKey], this.allExcludedIds);
itemsArray.forEach( function (item) {
if (!item.state) {
that.stateThisPage = false;
}
});
} else {
this.stateThisPage = false;
this.addID(item[this.idKey], this.allExcludedIds);
this.delID(item[this.idKey], this.allIncludedIds);
}
if(this.stateAllPages){
if(this.stateThisPage && this.allExcludedIds.length === 0){
this.textAllPages = '全选已启用,没有排除任何项!';
}else{
this.textAllPages = '全选已启用,已排除'+ this.allExcludedIds.length + '项!排除项的ID为:' + this.allExcludedIds;
}
}else{
if(!this.stateThisPage && this.allIncludedIds.length === 0){
this.textAllPages='全选未启用,没有选择任何项!';
}else{
this.textAllPages = '全选未启用,已选择' + this.allIncludedIds.length + '项!选择项的ID为:' + this.allIncludedIds;
}
}
};
this.checkDatas.signCheckbox = function (itemsArray) {//标注当前页被选中的条目,在翻页成功后执行。
var that = this;
if(this.stateAllPages){
this.stateThisPage = true;
itemsArray.forEach(function (item) {
var thisID = item[that.idKey];
var index = that.allExcludedIds.indexOf(thisID);
if (index > -1) {
item.state = false;
that.stateThisPage = false;
} else {
item.state = true;
}
});
}else{
this.stateThisPage = true;
itemsArray.forEach( function (item) {
var thisID = item[that.idKey];
var index = that.allIncludedIds.indexOf(thisID);
if (index === -1) {
item.state = false;
that.stateThisPage = false;
}
});
}
};
this.checkDatas.addID = function (id, idArray) {
var index = idArray.indexOf(id);
if (index === -1) {
idArray.push(id);//如果当前页的单项既有勾选又有非勾选,这时勾选当前页全选,需要这个判断,以免重复添加
}
};
this.checkDatas.delID = function (id, idArray) {
var index = idArray.indexOf(id);
if (index > -1) {
idArray.splice(index, 1)
}
};
this.checkDatas.getResultOfCheckAndFilter = function (isShowFilter,filterOptions) {//获取发送给后台的所有参数。
var toServerDatas;
var allIncludedIds = that.deepClone(this.allIncludedIds);
var allExcludedIds = that.deepClone(this.allExcludedIds);
if (!this.stateAllPages) {
if (allIncludedIds.length === 0) {
//return 弹窗告知:没有勾选项
}
toServerDatas = {
isSelectAll: false,
allIncludedIds: allIncludedIds,
}
}else {
toServerDatas = { //exclude
isSelectAll: true,
allExcludedIds: allExcludedIds,
};
}
if (isShowFilter) {
for(var key in filterOptions){
toServerDatas[key]=filterOptions[key]
}
}
this.toServerDatas=toServerDatas;//这行代码在实际项目中不需要
return toServerDatas;
}
},
methods: {
deepClone : function (arrayOrObject) {
function isArray(value) { return {}.toString.call(value) === "[object Array]"; }
function isObject(value) { return {}.toString.call(value) === "[object Object]"; }
var target = null;
if (isArray(arrayOrObject)) target = [];
if (isObject(arrayOrObject)) target = {};
for (var key in arrayOrObject) {
var value = arrayOrObject[key];
if (isArray(value) || isObject(value)) {
target[key] = deepClone(value);
} else {
target[key] = value;
}
}
return target;
},
selectChange:function(eachPageItemsNum){
this.divideDatas.eachPageItemsNum = eachPageItemsNum;
this.request(1,eachPageItemsNum);
},
createDividePage : function () {
var divideArray = [];
var allPagesNum = this.divideDatas.allPagesNum;
var nowPageNum = this.divideDatas.nowPageNum;
if (allPagesNum >= 1 && allPagesNum <= 10) {
for (var i = 1; i <= allPagesNum; i++) {
divideArray.push(i);
}
} else if (allPagesNum >= 11) {
if (nowPageNum > 6) {
divideArray.push(1);
divideArray.push(2);
divideArray.push(3);
divideArray.push('...');
divideArray.push(nowPageNum - 1);
divideArray.push(nowPageNum);
} else {
for (i = 1; i <= nowPageNum; i++) {
divideArray.push(i);
}
}
//以上当前页的左边,以下当前页的右边
if (allPagesNum - nowPageNum >= 6) {
divideArray.push(nowPageNum + 1);
divideArray.push(nowPageNum + 2);
divideArray.push('...');
divideArray.push(allPagesNum - 2);
divideArray.push(allPagesNum - 1);
divideArray.push(allPagesNum);
} else {
for (var i = nowPageNum + 1; i <= allPagesNum; i++) {
divideArray.push(i);
}
}
}
this.divideArray = divideArray;
},
clickDividePage : function (stringOfNum, event) {
var allPagesNum = this.divideDatas.allPagesNum;
var nowPageNum = this.divideDatas.nowPageNum;
if (stringOfNum === 'front' && nowPageNum != 1) {
nowPageNum--;
} else if (stringOfNum === 'back' && nowPageNum != allPagesNum) {
nowPageNum++;
} else if (stringOfNum === 'leap') {
if (event.which != 13) return;//不拦截情形:(1)聚焦输入框、按“Enter”键时;(2)点击“GO”时
var customNum = Math.ceil(parseFloat(this.customString));
if (customNum < 1 || customNum == 'NaN') {
nowPageNum = 1;//不给提示
} else if(customNum > allPagesNum) {
nowPageNum = allPagesNum;//不给提示
} else {
nowPageNum = customNum;
}
} else {
nowPageNum = Math.ceil(parseFloat(stringOfNum));
}
this.request(nowPageNum,this.divideDatas.eachPageItemsNum);
},
}
}
},
})
</script>
</html>
二、Vue之五级联动组件-省、市、县、乡、村
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Vue之五级联动组件-省、市、县、乡、村</title>
</head>
<style type="text/css">
.edit{
height:40px;
line-height: 40px;
}
.paddingBottom{
padding-bottom: 40px;
}
.question-select {
height: 60px;
}
.question-select select {
border-radius: 5px;
box-shadow: 0 0 5px #666;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
border: none;
outline: none;
height: 40px;
padding: 0 20px;
color: #333;
font-size: 22px;
}
.question-select select.short {
width: 120px;
}
.question-select select.long {
width: 240px;
}
.birth-year{
width:90px;
margin-right: 20px;
height: 100px;
overflow-y: scroll;
}
.birth-month{
width:70px;
margin-right: 20px;
}
.birth-date{
width:70px;
}
</style>
<body>
<div id="component">
<five-grade :all-datas="allDatas"></five-grade>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- Vue.js v2.6.10 -->
<script type="text/javascript">
Vue.component('five-grade', {
template: `
<div>
<div class="question-select">
<select v-model="singleProvince" class="short" @change="selectName(singleProvince)">
<option v-for="key in allProvinces" :value="key" v-text="key"></option>
</select>
<select v-model="singleCity" v-show="singleProvince" class="short" @change="selectName(singleProvince,singleCity)">
<option v-for="key in allCitys" :value="key" v-text="key"></option>
</select>
<select v-model="singleCounty" v-show="singleCity" class="short" @change="selectName(singleProvince,singleCity,singleCounty)">
<option v-for="key in allCountys" :value="key" v-text="key"></option>
</select>
<select v-model="singleTown" v-show="singleCounty" class="long" @change="selectName(singleProvince,singleCity,singleCounty,singleTown)">
<option v-for="key in allTowns" :value="key" v-text="key"></option>
</select>
<select v-model="singleVillage" v-show="singleTown" class="long" @change="selectName(singleProvince,singleCity,singleCounty,singleTown,singleVillage)">
<option v-for="key in allVillages" :value="key" v-text="key"></option>
</select>
</div>
<div class="paddingBottom">{{address}}</div>
</div>
`,
props: {
allDatas: {
type: Object
}
},
data: function(){
return {
allProvinces: [],
singleProvince: '',
allCitys: [],
singleCity: '',
allCountys: [],
singleCounty: '',
allTowns: [],
singleTown: '',
allVillages: [],
singleVillage: '',
address: '',
}
},
beforeMount: function () {
this.selectName();
},
methods: {
selectName: function(singleProvince,singleCity,singleCounty,singleTown,singleVillage){
var siteArray = [singleProvince,singleCity,singleCounty,singleTown,singleVillage];
var modelArray = ['singleProvince','singleCity','singleCounty','singleTown','singleVillage'];
var optionsArray = ['allProvinces','allCitys','allCountys','allTowns','allVillages'];
var allDatasNext;
var address = "你选择的地址是:";
for(var i=0;i<siteArray.length;i++){//遍历数组所有项
if(!siteArray[i]) this[modelArray[i]] = '';
}
for(var i=0;i<siteArray.length;i++){//遍历数组至undefined第1次出现时停止
allDatasNext = i == 0 ? this.allDatas : allDatasNext[siteArray[i-1]];
if(!siteArray[i]) {
var array = [];
for (var key in allDatasNext) {
array.push(key)
}
this[optionsArray[i]] = array;
break;
}
}
if(this.singleProvince) address += this.singleProvince;
if(this.singleCity) address += '-' + this.singleCity;
if(this.singleCounty) address += '-' + this.singleCounty;
if(this.singleTown) address += '-' + this.singleTown;
if(this.singleVillage) address += '-' + this.singleVillage;
this.address = address;
}
},
});
new Vue({
el: '#component',
data(){
return {
allDatas : makeallDatas()
}
},
methods:{
}
})
function makeallDatas() {
var allDatas = {
"北京市": {
"区": {
"通州区": {
"中仓街道办事处": {
"滨河社区居委会": "110112001029",
"运河湾社区居委会": "110112001030"
},
"漷县镇": {
"后元化村委会": "110112106260",
"前元化村委会": "110112106261"
}
},
"昌平区": {
"天通苑南街道办事处":{
"东辰社区居委会":"110114009001",
"佳运园社区居委会":"110114009002",
"天通苑第二社区居委会":"110114009003",
"天通西苑第一社区居委会":"110114009004",
"天通东苑第一社区居委会":"110114009005",
"天通东苑第二社区居委会":"110114009006",
"天通苑第一社区居委会":"110114009007",
"嘉诚花园社区居委会":"110114009008",
},
"霍营街道办事处": {
"华龙苑南里社区居委会": "110114010001",
"华龙苑北里社区居委会": "110114010002",
"蓝天园社区居委会": "110114010003",
"天鑫家园社区居委会": "110114010004",
"霍营小区社区居委会": "110114010005",
"上坡佳园社区居委会": "110114010006",
"华龙苑中里社区居委会": "110114010007",
}
}
},
"县": {
"密云县": {
"密云镇": {
"小唐庄社区居委会": "110228100001",
"李各庄社区居委会": "110228100002",
"大唐庄社区居委会": "110228100003",
"季庄村委会": "110228100205",
},
"溪翁庄镇": {
"东智北村委会": "110228101209",
"石墙沟村委会": "110228101210",
"黑山寺村委会": "110228101211",
"立新庄村委会": "110228101212",
},
},
"延庆县": {
"旧县镇": {
"常里营村委会": "110229104215",
"盆窑村委会": "110229104216",
"团山村委会": "110229104217",
"大柏老村委会": "110229104218",
},
"珍珠泉乡": {
"双金草村委会": "110229214209",
"小川村委会": "110229214210",
"小铺村委会": "110229214211",
"仓米道村委会": "110229214212",
}
}
}
},
"河南省": {
"郑州市": {
"金水区": {
"凤凰台街道办事处": {
"凤凰台社区居民委员会": "410105013004",
"王庄社区居民委员会": "410105013005",
"张庄社区居民委员会": "410105013006",
"凤凰城社区居民委员会": "410105013007",
},
"金光路街道办事处": {
"徐庄村委会": "410105564201",
"贾陈村委会": "410105564202",
"柳园口村委会": "410105564203",
"马楼村委会": "410105564204",
}
},
"登封市": {
"嵩阳街道办事处": {
"玉溪路居委会": "410185001014",
"苹果园居委会": "410185001015",
"颖河路居委会": "410185001016",
"守敬路居委会": "410185001017",
},
"少林街道办事处": {
"塔沟居委会": "410185002001",
"少林居委会": "410185002002",
"耿庄村委会": "410185002203",
"王庄村委会": "410185002204",
},
"送表矿区": {
"东送表村委会": "410185400201",
"梁庄村委会": "410185400202",
"安庄村委会": "410185400203",
"刘楼村委会": "410185400204",
}
}
},
"信阳市": {
"浉河区": {
"老城街道办事处": {
"义阳居委会": "411502001001",
"三里店居委会": "411502001003",
"东方红居委会": "411502001004",
"浉河居委会": "411502001007",
},
"民权街道办事处": {
"民权居委会": "411502002001",
"新生居委会": "411502002002",
"成功居委会": "411502002003",
"白果树居委会": "411502002004",
},
"车站街道办事处": {
"工区东居委会": "411502003001",
"工区路居委会": "411502003002",
"六里棚居委会": "411502003003",
"新公房居委会": "411502003004",
}
},
"固始县": {
"段集镇": {
"街道居委会": "411525109001",
"段集村委会": "411525109201",
"棠树岗村委会": "411525109202",
"青峰村委会": "411525109203",
"下楼村委会": "411525109204",
"桂岭村委会": "411525109205",
"窑沟村委会": "411525109206",
"五尖山村委会": "411525109207",
"柳林村委会": "411525109208",
"齐山村委会": "411525109209",
"钓鱼台村委会": "411525109210",
"蒋营村委会": "411525109211",
"庙山村委会": "411525109212",
"汪旱庄村委会": "411525109213",
"乐道村委会": "411525109214",
"姚老家村委会": "411525109215",
"赵营村委会": "411525109216",
"童庙村委会": "411525109217",
"高庙村委会": "411525109218"
},
"武庙集镇": {
"街道居委会": "411525113001",
"汪小庄村委会": "411525113201",
"黄土岭村委会": "411525113202",
"平阳村委会": "411525113203",
"长江河村委会": "411525113204",
"锁口村委会": "411525113205",
"皮冲村委会": "411525113206",
"刘中楼村委会": "411525113207",
"迎水寺村委会": "411525113208",
"余楼村委会": "411525113209",
"钱老楼村委会": "411525113210",
"新店村委会": "411525113211",
"徐小店村委会": "411525113212",
"邓岭村委会": "411525113213",
"太平村委会": "411525113214",
"汪楼村委会": "411525113215",
"李瓦房村委会": "411525113216"
},
"祖师庙镇": {
"祖师庙居委会": "411525117001",
"仰天洼社区居委会": "411525117002",
"小店村委会": "411525117201",
"王行村委会": "411525117202",
"大冲村委会": "411525117203",
"松林村委会": "411525117204",
"刘楼村委会": "411525117205",
"羁马村委会": "411525117206",
"黄楼村委会": "411525117207",
"彭畈村委会": "411525117208",
"童圩村委会": "411525117209",
"仓房村委会": "411525117210",
"毛店村委会": "411525117211",
"万岗村委会": "411525117212",
"七冲村委会": "411525117213",
"三区村委会": "411525117214",
"杨楼村委会": "411525117215"
}
},
"息县": {
"杨店乡": {
"杨店村委会": "411528204200",
"安寨村委会": "411528204201",
"何庄村委会": "411528204202",
"李大庄村委会": "411528204203",
},
"张陶乡": {
"张陶村委会": "411528205200",
"曹林村委会": "411528205201",
"陈圈行村委会": "411528205202",
"大陈庄村委会": "411528205203",
},
"白土店乡": {
"白土店村委会": "411528206200",
"白衣阁村委会": "411528206201",
"大江庄村委会": "411528206202",
"桂庄村委会": "411528206203",
},
"岗李店乡": {
"岗李店村委会": "411528207200",
"大彭庄村委会": "411528207201",
"方老庄村委会": "411528207202",
"贾后寨村委会": "411528207203",
}
}
},
},
};
return allDatas;
}
</script>
</html>
三、2种三层弹窗组件之simple-dialog
1、不可拖拽
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue多层弹窗</title>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<style>
.simpleDialog {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.simpleDialog .mask {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: black;
opacity: 0.5;
}
.simpleDialog .content {
position: fixed;
background: white;
opacity: 1;
display: flex;
flex-direction: column;
}
.simpleDialog .content .title {
display: flex;
background: blue;
color: white;
padding: 10px;
cursor: pointer;
}
.simpleDialog .content .conform {
display: flex;
justify-content: center;
padding: 10px;
background: blue;
}
</style>
</head>
<body>
<div id="el">
<button @click="clickButton()" style="margin-top: 30px;">
点击-出现-弹窗
</button>
<simple-dialog :required-data="requiredDataOut">
插槽一
<simple-dialog :required-data="requiredDataMid">
插槽二
<simple-dialog :required-data="requiredDataIn">
插槽三
</simple-dialog>
</simple-dialog>
</simple-dialog>
</div>
<script>
Vue.component('simple-dialog', {
template: `
<div>
<div class="simpleDialog" v-show="requiredData.isShow">
<div class="mask" v-show="requiredData.isShow"></div>
<div class="content" v-show="requiredData.isShow">
<div class="title">
<span>系统消息</span>
</div>
<div :style="{width:requiredData.width||'800px',height:requiredData.height||'400px'}">
<slot></slot>
</div>
<div class="conform">
<button v-on:click="close()">关闭</button>
<button v-on:click="open()">打开</button>
</div>
<div>
</div>
</div>
`,
props: {
requiredData: {
type: Object
}
},
data: function() {
return {}
},
methods: {
close: function () {
this.requiredData.isShow = false;
if(this.requiredData.closeFn) this.requiredData.closeFn();
},
open: function () {
if(this.requiredData.openFn) this.requiredData.openFn();
}
}
});
new Vue({
el: '#el',
data(){
var that = this;
return {
requiredDataOut : {
isShow : false,
width : '900px',
height : '600px',
openFn : function () {
that.requiredDataMid.isShow = true;
}
},
requiredDataMid : {
isShow : false,
width : '600px',
height : '400px',
openFn : function () {
that.requiredDataIn.isShow = true;
},
},
requiredDataIn : {
isShow : false,
width : '300px',
height : '200px',
},
//一层弹窗可以如下使用
//requiredDataOut = {
// isShow : false,
//};
//clickButton() {
// requiredDataOut.isShow = true;
//};
}
},
methods:{
clickButton() {
this.requiredDataOut.isShow = true;
}
}
})
</script>
</body>
</html>
2、可拖拽(含插槽)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue多层弹窗</title>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<script>
function drag(wholeTitleId, wholeContentId) {
var wholeTitleId = wholeTitleId||'titleId';
var wholeContentId = wholeContentId||'contentId';
var oDiv = document.getElementById(wholeContentId);
if(!oDiv) return;
oDiv.onmousedown = down;
function processThis(fn, nowThis) {
return function (event) {
fn.call(nowThis, event);
};
}
function down(event) {
event = event || window.event;
if (event.target.id != wholeTitleId) return;
this.initOffsetLeft = this.offsetLeft;
this.initOffsetTop = this.offsetTop;
this.initClientX = event.clientX;
this.initClientY = event.clientY;
this.maxOffsetWidth =
(document.documentElement.clientWidth || document.body.clientWidth) -
this.offsetWidth;
this.maxOffsetHeight =
(document.documentElement.clientHeight ||
document.body.clientHeight) - this.offsetHeight;
if (this.setCapture) {
this.setCapture();
this.onmousemove = processThis(move, this);
this.onmouseup = processThis(up, this);
} else {
document.onmousemove = processThis(move, this);
document.onmouseup = processThis(up, this);
}
}
function move(event) {
var nowLeft = this.initOffsetLeft + (event.clientX - this.initClientX);
var nowTop = this.initOffsetTop + (event.clientY - this.initClientY);
this.style.left = nowLeft + 'px';
this.style.top = nowTop + 'px';
}
function up() {
if (this.releaseCapture) {
this.releaseCapture();
this.onmousemove = null;
this.onmouseup = null;
} else {
document.onmousemove = null;
document.onmouseup = null;
}
}
};
</script>
<style>
.simpleDialog {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.simpleDialog .mask {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: black;
opacity: 0.5;
}
.simpleDialog .content {
position: fixed;
background: white;
opacity: 1;
display: flex;
flex-direction: column;
}
.simpleDialog .content .title {
display: flex;
background: blue;
color: white;
padding: 10px;
cursor: pointer;
}
.simpleDialog .content .conform {
display: flex;
justify-content: center;
padding: 10px;
background: blue;
}
</style>
</head>
<body>
<div id="el">
<button @click="clickButton()" style="margin-top: 30px;">
点击-出现-弹窗
</button>
<simple-dialog :required-data="requiredDataOut">
插槽一
<simple-dialog :required-data="requiredDataMid">
插槽二
<simple-dialog :required-data="requiredDataIn">
插槽三
</simple-dialog>
</simple-dialog>
</simple-dialog>
</div>
<script>
Vue.component('simple-dialog', {
template: `
<div>
<div class="simpleDialog" v-show="requiredData.isShow">
<div class="mask" v-show="requiredData.isShow"></div>
<div class="content" v-show="requiredData.isShow" :id="requiredData.contentId||'contentId'">
<div class="title" :id="requiredData.titleId||'titleId'">
<span>系统消息</span>
</div>
<div :style="{width:requiredData.width||'800px',height:requiredData.height||'400px'}">
<slot></slot>
</div>
<div class="conform">
<button v-on:click="close()">关闭</button>
<button v-on:click="open()">打开</button>
</div>
<div>
</div>
</div>
`,
props: {
requiredData: {
type: Object
}
},
data: function() {
return {}
},
methods: {
close: function () {
this.requiredData.isShow = false;
if(this.requiredData.closeFn) this.requiredData.closeFn();
var content = this.requiredData.contentId;
document.getElementById(content).style.cssText = "position:fixed;display:flex;";
},
open: function () {
if(this.requiredData.openFn) this.requiredData.openFn();
}
},
});
new Vue({
el: '#el',
data(){
var that = this;
return {
requiredDataOut : {
isShow : false,
width : '900px',
height : '600px',
titleId : "titleId1",
contentId : "contentId1",
openFn : function () {
that.requiredDataMid.isShow = true;
drag(that.requiredDataMid.titleId,that.requiredDataMid.contentId);
}
},
requiredDataMid : {
isShow : false,
width : '600px',
height : '400px',
titleId : "titleId2",
contentId : "contentId2",
openFn : function () {
that.requiredDataIn.isShow = true;
drag(that.requiredDataIn.titleId,that.requiredDataIn.contentId);
},
},
requiredDataIn : {
isShow : false,
width : '300px',
height : '200px',
titleId : "titleId3",
contentId : "contentId3",
},
//一层弹窗可以如下使用
//requiredDataOut = {
// isShow : false,
//};
//clickButton() {
// requiredDataOut.isShow = true;
//};
}
},
methods:{
clickButton() {
this.requiredDataOut.isShow = true;
drag(this.requiredDataOut.titleId,this.requiredDataOut.contentId);
}
}
})
</script>
</body>
</html>
四、2种三层弹窗组件之el-dialog
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>vue2.6.10组件之el-dialog多层弹窗</title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script src="https://cdn.bootcss.com/element-ui/2.10.1/index.js"></script>
<link href="https://cdn.bootcss.com/element-ui/2.10.1/theme-chalk/index.css" rel="stylesheet">
<style>
#app{
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<div id="app">
<el-button type="text" @click="outerVisible = true">点击打开外层弹窗</el-button>
<el-dialog
width="70%"
title="外层"
:visible.sync="outerVisible"
>
以下是外层弹窗的插槽<br/>
<div style="height:200px;border:1px solid #ccc;padding:20px;">
width="70%",决定弹-窗的宽<br/>
style="height:200px",通过给弹-窗插槽的部分标签设置高,决定弹-窗的高,如此框<br/>
:visible.sync="middleVisible",传递引用,此处改变,别处也改变<br/>
:visible="middleVisible",传递普通值,此处改变,别处不改变<br/>
<el-dialog
width="50%"
title="中层"
:visible.sync="middleVisible"
append-to-body
>
以下是中层弹-窗的插槽<br/>
<el-dialog
width="30%"
title="内层"
:visible.sync="innerVisible"
append-to-body
>
以下是内层弹-窗的插槽<br/>
<div slot="footer" class="dialog-footer">
<el-button @click="innerVisible = false">关闭内层</el-button>
<el-button type="primary" @click="innerVisible = false">关闭内层</el-button>
</div>
以上是内层弹-窗的插槽<br/>
</el-dialog>
<div slot="footer" class="dialog-footer">
<el-button @click="middleVisible = false">关闭中层</el-button>
<el-button type="primary" @click="innerVisible = true">打开内层</el-button>
</div>
以上是中层弹-窗的插槽<br/>
</el-dialog>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="outerVisible = false">关闭外层</el-button>
<el-button type="primary" @click="middleVisible = true">打开中层</el-button>
</div>
以上是外层弹-窗的插槽(代码中,此句位于按钮下面)<br/>
</el-dialog>
</div>
</body>
<script>
new Vue({
el: '#app',
data() {
return {
outerVisible: false,
middleVisible: false,
innerVisible: false
};
},
methods: {
},
components: {
},
})
</script>
</html>
五、vue问题与解决
1、问题1,用插件vod-js-sdk-v6上传视频时
来源,https://cloud.tencent.com/developer/ask/260695
(1)现象,报错“Error: ugc upload | signature verify forbidden”
(2)原因,
A、前端向后台索要签名,后台用小权限“腾讯账户”向腾讯索要签名,
B、腾讯给后台小权限签名,后台给前端小权限签名,前端将小权限签名发给腾讯,腾讯给前端报错
(3)解决,让后台换用大权限“腾讯账户”
2、问题2
(1)现象,不是第一页的最后一页为空白
(2)原因,最后一页不是第一页,删除最后一页的唯一项,然后请求当前页数据
(3)解决,
delModelTrainings({trainingId: row.trainingId}).then(() => {
ElMessage.success('删除成功');
var index = (page.value.pageNum - 1)*page.value.pageSize + 1;
//当前页第1项的序号>每页条数(当前页不是第1页) && 当前页第1项的序号=数据总数(当前页是最后1页且只有1项)
if(index>page.value.pageSize && index===page.value.count){
page.value.pageNum--;
}
listData();
})
3、问题3,
(1)现象,:src 文件路径错误问题的解决方法
(2)解决,
<template>
<img :src="item.url" alt="logo" />
</template>
import background from '@/assets/image/background.png'
var collect = reactive([
{
url: background, //url: '/src/assets/images/logo.png' 注释这样的写法会出错
teacher: "讲师:李老师",
}
]);
4、问题4
(1)报错信息:[VUE ERROR] Invalid default value for prop "slides":
Props with type Object/Array must use a factory function to return the default value
(2)错误原因:对于数组和对象类型的prop,Vue官方从最初的版本就推荐使用工厂函数返回默认值,而不是直接赋值空数组[]或空对象{}
(3)错误示例
props: {
sizeRadio1: {
type: Array,
default: []
}
sizeRadio2: {
type: Object,
default: {}
}
},
(4)正确示例
props: {
sizeRadio1: {
type: Array,
default: function() {
return []
}
}
sizeRadio2: {
type: Object,
default: function() {
return {}
}
}
},
5、问题5,汉化(汉字、中文、配置)
(1)方案一,在App.vue中
<script >
import { defineComponent } from 'vue'
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
export default defineComponent({
components: {
ElConfigProvider,
},
setup() {
return {
locale: zhCn,
}
},
})
</script>
<template>
<div style="height:100%">
<el-config-provider :locale="locale">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</el-config-provider>
</div>
</template>
(2)方案二,在main.js中
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
6、问题6,深度选择器(也叫穿透选择器)!!!
(1)>>>(直接穿透),在Vue3中已废弃,
A、适用场景:Vue2的scoped样式中,不支持大多数CSS预处理器(如Sass、Less、Stylus)
B、示例:
<style scoped>
/* 穿透到子组件中类名为.child 的元素 */
.parent >>> .child {
color: red;
}
</style>
(2)/deep/(通用穿透),在Vue3中已废弃,与>>>类似,但兼容性更好
A、适用场景:Vue2的scoped样式,支持大多数CSS预处理器(如Sass、Less、Stylus)
B、示例:
<style scoped lang="scss">
/* 穿透到深层的 .deep-element 元素 */
.container /deep/ .deep-element {
font-size: 16px;
}
</style>
(3)::v-deep(Vue专用穿透),Vue官方推荐的深度选择器,兼容性最佳
A、适用场景:Vue2.7+和Vue3,支持所有预处理器
B、示例:
<style scoped>
/* Vue2.7+语法 */
::v-deep .child-component button {
background: blue;
}
/* Vue3推荐语法 */
:deep(.child-component) button {
background: blue;
}
</style>
六、收缩展开
1、多行收缩为2行,点击则“用动画”展开
(1)隐藏Outer,不能触发点击事件
(2)获取初始数据;内层显示full区域,异步计算full区域的高度并存储、显示fold区域、隐藏full区域
(3)显示Outer
(4)缺少1、3步骤,页面会闪烁
(5)点击箭头,内层“用动画”显示溢出区域,隐藏初始区域
<template>
<div :style="{visibility: isOuterShow ? 'visible':'hidden'}">
<div v-for="(item,index) in allNote" :style="{'background':item.readStatus?'#eaeef5':'#f5f6f9'}" class="mynote" @click="clickNoteItem(item)" :key="index">
<div class="mynote-item">
<div>{{item.title}}</div>
<div>
<span style="padding-right: 30px;">{{millsecondsToDate(item.receiveTime)}}</span>
<el-icon :style="{visibility: item.isShowArrow ? 'visible':'hidden'}" class="cursor">
<ArrowRight style="color: gray" v-show="item.isFoldShow" @click.stop="clickItemArrow(item)" />
<ArrowDown style="color: gray" v-show="!item.isFoldShow" @click.stop="clickItemArrow(item)" />
</el-icon>
</div>
</div>
<div v-show="isFullShow" class="isFullShow">{{item.content}}</div>
<div v-show="item.isFoldShow" class="fold_">{{item.content}}</div>
<!-- 下面,from-height,to-height(过程伴随overflow:hidden,应当与style-height一致) -->
<div v-show="!item.isFoldShow"
:class="!item.isFoldShow? 'divDown' + item.index:''"
:style="{'height':item.height + 'px','overflow':'hidden'}"
>{{item.content}}</div>
</div>
</div>
</template>
<script setup>
var isOuterShow = ref(false);//防止闪烁
var isFullShow = ref(false);//计算行高
var clickItemArrow = function(item){
item.isFoldShow = !item.isFoldShow;
};
var getData = function(){
getAllNotes(data).then(function(result){
isFullShow.value = true;
allNote.length = 0;
total.value = result.data.total;
var list = result.data.list;
for(var i = 0; i<list.length; i++){
list[i].isShowArrow = false;
list[i].isFoldShow = false;
allNote.push(list[i])
}
setTimeout(function(){
var all = document.getElementsByClassName('isFullShow')
for(var i = 0; i<all.length; i++){
if(all[i].clientHeight > 50) {
allNote[i].isShowArrow = true;
allNote[i].isFoldShow = true;
allNote[i].index = i;
allNote[i].height = all[i].clientHeight;
document.styleSheets[0].insertRule(
"@keyframes moveDown" + i +
"{" +
"from { height: 40px; }" +
"to { height: " + all[i].clientHeight + "px; }" +
"}"
)
document.styleSheets[0].insertRule(
".divDown" + i +
"{" +
"animation: moveDown" + i + " 2s ease 0s 1 normal;" + /* 非常重要:2s前面有空格 */
"}"
)
}
}
isFullShow.value = false;
isOuterShow.value = true;
});
})
};
onMounted(function() {
getData();
})
</script>
<style lang="scss">
.cursor{
cursor: pointer;
}
.fold_{
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
</style>
七、各种滚动
1、滚动滚轮,元素底部从视口底部出现
(1)可演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>元素底部出现,触发事件</title>
<style>
.div{
height: 500px;
}
#elementToTrack {
height: 400px;
background: gray;
}
</style>
</head>
<body>
<div class="div"></div>
<div class="div"></div>
<div class="div"></div>
<div id="elementToTrack"></div>
<div class="div"></div>
</body>
<script>
var element = document.getElementById('elementToTrack');
window.onscroll = function() {
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;//获取视口高度
var viewportBottom = window.scrollY + viewportHeight;//页面顶部到视口底部的距离
var elementBottom = element.offsetTop + element.offsetHeight;//页面顶部到元素底部的距离
if (elementBottom < viewportBottom) {//如果元素底部高于视口底部(元素完全进入视口)!!!
console.log(viewportBottom, elementBottom);
}
};
</script>
</html>
(2)真实项目
需求:
A、表格底部消失在页面下面,左右滚动条出现在页面底部;
B、表格底部出现,滚动条的位置恢复为默认的表格底部
//src/view/base/search-topical-table.vue;57--59;202--230;233
watch(() => props, (newVal) => {
setTimeout(() => {
checkScroll()
},100)
}, { immediate: true, deep: true })
var tableBody = null;
var container = null;
var isOver = ref(false)
onMounted(() => {
container = document.querySelector('.main-container');
tableBody = document.querySelector('.el-table__body');
container.addEventListener('scroll', checkScroll);
checkScroll();
})
onUnmounted(() => {
container.removeEventListener('scroll', checkScroll)
})
function checkScroll() {
//获取目标元素的底部位置
var tableBottom = tableBody.getBoundingClientRect().bottom;
var documentBottom = document.body.clientHeight;
//表格底部位于页面底部上方
if ( tableBottom < documentBottom ) {
isOver.value = true;
} else {
isOver.value = false;
}
}
<div class="search-topical-table" :class="{'search-topical-table-fixed':!isOver}">
<el-table></el-table>
<el-pagination/>
</div>
<style lang="scss">
.search-topical-table-fixed{
.el-table__body-wrapper .el-scrollbar__bar.is-horizontal{
position: fixed;
bottom: 10px;
left:165px;
}
}
</style>
(3)真实项目
需求:滚动条的位置由默认的表格底部改为页面底部
<template>
<div class="search-topical-table">
<el-table></el-table>
<el-pagination/>
</div>
</template>
<style lang="scss">
.search-topical-table{
.el-table__body-wrapper .el-scrollbar__bar.is-horizontal{
position: fixed;
bottom: 10px;
height: 10px;
}
}
</style>
2、触发按钮,“页面”交替回到顶部底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>页面滚动条回到顶部示例</title>
<style>
button {
color: red;
font-size: 50px;
border-radius: 10px;
position: fixed;
bottom: 0;
right: 0;
}
.black {
background: #000;
color: #fff;
}
.div {
height: 2000px;
background-color: gray;
}
</style>
</head>
<body>
<button id="btn">顶部--底部</button>
<div class="black">顶部</div>
<div class="div"></div>
<div class="black">底部</div>
</body>
<script>
var y = 0 ;
var btn = document.getElementById('btn');
btn.addEventListener('click', function() {
y = y == 0 ? 2000 : 0;
window.scrollTo({
left: 0,
top: y,
behavior: 'smooth'
});
});
</script>
</html>
3、触发按钮,“元素”交替回到顶部底部
(1)演示示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>元素滚动条回到顶部示例</title>
<style>
button {
color: red;
font-size: 50px;
border-radius: 10px;
position: fixed;
top: 50px;
right: 50px;
}
#myDiv {
height: 500px;
overflow: auto;
}
.black {
background: #000;
color: #fff;
}
#myDiv .myDiv {
height: 1000px;
background: gray;
}
</style>
</head>
<body>
<button onclick="clickBtn()">顶部--底部</button>
<div id="myDiv">
<div class="black">顶部</div>
<div class="myDiv"></div>
<div class="black">底部</div>
</div>
</body>
<script>
var y = 0 ;
function clickBtn() {
y = y == 0? 1000 : 0;
var div = document.getElementById('myDiv');
div.scrollTo(0, y);
}
</script>
</html>
(2)项目示例
.app-main {
/*50 = navbar */
width: 100%;
padding-bottom: 85px;
margin-bottom: 20px;
box-sizing: border-box;
position: relative;
//以下是,新增内容
overflow: auto;
}
method(params).then(res => {
allTableData.value = res.data.rows
page.value.pageNum = res.data.pageNum
page.value.pageSize = res.data.pageSize
page.value.count = res.data.recordCount
loading.value = false
//以下是,新增内容
var height = window.innerHeight - 100
var ele = document.getElementsByClassName('app-main')[0];
ele.style.height = height + 'px'
ele.scrollTo({ top: 0, behavior: 'smooth' })
})
4、初始化时,“元素”滚动条回到底部
(1)html
<div class="training-result-code">
<div class="training-result-title">
训练日志
</div>
<div class="training-result-code_content1" ref="divShow">
<div ref="divSonShow">
{{ modelLogs }}
</div>
</div>
<div class="training-result-code_content2" ref="divHide" >
{{ modelLogs }}
</div>
</div>
/* 以下弹窗中使用 */
<el-dialog title="查看日志" v-model="dialogVisible" width="70%">
<div class="record-detail-code">
<div class="record-detail-code_content1" ref="divShow">
<div ref="divSonShow">
{{ modelLogs }}
<div style="padding-bottom: 100px"></div>
</div>
</div>
<div class="training-result-code_content2" ref="divHide" >
{{ modelLogs }}
</div>
</div>
<template #footer>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</template>
</el-dialog>
(2)css
&-code{
background-color: #fff;
border-radius: 10px;
height: 500px;
padding: 30px;
&_content1{
background-color: #000;
overflow: scroll;
color: #fff;
white-space: pre-wrap;
height: 420px;
}
&_content2{
background-color: #000;
overflow: scroll;
color: #fff;
white-space: pre-wrap;
visibility: hidden;
}
}
/* 以下弹窗中使用 */
.record-detail-code{
background-color: #fff;
border-radius: 10px;
height: 500px;
padding: 10px;
overflow: hidden;
&_content1{
background-color: #000;
overflow: scroll;
color: #fff;
white-space: pre-wrap;
height: 490px;
}
&_content2{
background-color: #000;
overflow: hidden;
color: #fff;
white-space: pre-wrap;
visibility: hidden;
}
}
(3)js
const modelLogs = ref('');
const divShow = ref(null);
const divSonShow = ref(null);
const divHide = ref(null);
const timerId = ref(null);
const getLogs = function () {
getModelTrainingLogs({trainingId: route.query.trainingId}).then(res => {
modelLogs.value = res.data.runningLogs||'没有训练日志';
setTimeout(() => {
divSonShow.value.style.height = divHide.value.clientHeight + 100 + 'px';
divShow.value.scrollTop = divHide.value.clientHeight ;
});
})
}
getLogs()
var textAry = ['训练失败','训练完成'];
if(textAry.indexOf(route.query.text) === -1){
timerId.value = setInterval(() => {
getLogs()
}, 500);
}
/* 以下弹窗中使用 */
const modelLogs = ref('');
const divShow = ref(null);
const divSonShow = ref(null);
const divHide = ref(null);
const showRecord = function(row){
dialogVisible.value = true;
getModelTrainingLogs({trainingId: row.trainingId}).then(res => {
modelLogs.value = res.data.runningLogs||'没有训练日志';
setTimeout(() => {
divSonShow.value.style.height = divHide.value.clientHeight + 'px';
divShow.value.scrollTop = divHide.value.clientHeight*5 ;
});
})
}
八、拖拽
1、真实案例(与el-checkbox-group联合使用,拖拽之勾选表格列表)
<script setup>
import draggable from 'vuedraggable'
const checkList = ref([])
const columns = ref([
{
"label": "序号",
"prop": "processVersionSid",
"canChange": false,
"tip": "数据示例:2346"
},
{
"label": "栏目代码",
"prop": "columnCode",
"canChange": false,
"tip": "数据示例:J875023"
},
{
"label": "详细情况说明",
"prop": "reasonDetail",
"canChange": true,
"tip": "数据示例:数据示例示例示例"
},
{
"label": "系统备案栏目首播时间",
"prop": "planPremiereTime",
"canChange": true,
"tip": "数据示例:每周四 7:00 - 8:00"
},
{
"label": "中心核对栏目首播信息",
"prop": "checkPremiereTime",
"canChange": true,
"tip": "数据示例:每周四 7:00 - 8:00"
},
{
"label": "栏目重播信息",
"prop": "replayTime",
"canChange": true,
"tip": "数据示例:每周四 7:00 - 8:00"
},
{
"label": "完成率",
"prop": "completeRate",
"canChange": true,
"tip": "数据示例:98%"
},
{
"label": "备注",
"prop": "remark",
"canChange": false,
"tip": ""
}
])
</script>
<template>
<div class="column-select-warp">
<el-checkbox-group v-model="checkList">
<draggable
v-model="columns"
:force-fallback="true"
chosen-class="chosen"
draggable=".drag"
animation="300"
item-key="prop"
><!--
draggable=".drag",拖拽项的class;
:class="{'drag': element.canChange}",该项是否添加class -->
<template #item="{ element, index }">
<div class="column-select-item" :class="{'drag': element.canChange}">
<svg-icon iconClass="move" class="move-icon"></svg-icon>
<el-checkbox :disabled="!element.canChange" :value="element.prop" size="large" >
<span>{{ element.label }}</span>
<span class="column-select-item__tip">{{ element.tip }}</span>
</el-checkbox>
</div>
</template>
</draggable>
</el-checkbox-group>
</div>
</template>
2、可运行案例(与table联合使用,可拖动表头栏和表体行)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>vue.draggable、表格、vue3联合示例</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
<script src="https://www.itxst.com/package/vue3/vue.global.js"></script>
<script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>
<script src="https://www.itxst.com/package/vuedraggablenext/vuedraggable.umd.min.js"></script>
</head>
<body style="padding:10px;">
<div id="app">
<itxst-component></itxst-component>
</div>
</body>
<script type="x-template" id="itxst">
<div>教程,https://www.itxst.com/vue-draggable-next/tutorial.html</div>
<div>案例,https://debug.itxst.com/js/zfzeny7f</div>
<table class="tb">
<thead>
<draggable
v-model="headers"
animation="200"
tag="tr"
:item-key="(key) => key"
>
<template #item="{ element: header, index: index}">
<th class="move">
{{ header }}
</th>
</template>
</draggable>
</thead>
<draggable
:list="list"
handle=".move"
animation="300"
@start="onStart"
@end="onEnd"
tag="tbody"
item-key="name"
>
<template #item="{ element, index }">
<tr>
<td
class="move"
v-for="(header, index) in headers"
:key="header"
>
{{ element[header] }}
</td>
</tr>
</template>
</draggable>
</table>
</script>
<script>
const app = {
//注册draggable组件
components: {
'itxst-component': {
template: "#itxst",
components: {
"draggable": window.vuedraggable,
},
data() {
return {
//列的名称
headers: ["id", "name", "intro"],
//需要拖拽的数据,拖拽后数据的顺序也会变化
list: [
{ name: "www.itxst.com", id: 0, intro: "慢吞吞的蜗牛" },
{ name: "www.baidu.com", id: 1, intro: "中文搜索引擎" },
{ name: "www.google.com", id: 3, intro: "安卓操作系统" },
],
}
},
methods: {
//开始拖拽事件
onStart() {},
//结束拖拽事件
onEnd() {}
}
}
},
}
Vue.createApp(app).mount('#app')
</script>
<style>
.title {
padding: 3px;
font-size: 13px;
}
.itxst {
width: 600px;
}
.move {
cursor: move;
}
table.tb {
color: #333;
border: solid 1px #999;
font-size: 13px;
border-collapse: collapse;
min-width: 500px;
user-select: none;
}
table.tb th {
background: rgb(168 173 217);
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #999;
text-align: left;
}
table.tb th:nth-of-type(1) {
text-align: center;
}
table.tb td {
background: #d6c8c8;
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #999;
}
table.tb td:nth-of-type(1) {
text-align: center;
}
</style>
</html>
3、可运行案例(与vue3联合使用,无序列表)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>vue.draggable、无序列表、vue3联合示例</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
<script src="https://www.itxst.com/package/vue3/vue.global.js"></script>
<script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>
<script src="https://www.itxst.com/package/vuedraggablenext/vuedraggable.umd.min.js"></script>
</head>
<body>
<div id="app">
<div class="itxst">
<div class="group">
<draggable
:list="modules.group1" group="group1"
handle=".move" filter=".forbid"
@start="onStart" @end="onEnd" :move="onMove"
ghost-class="ghost" chosen-class="chosenClass" animation="300"
:force-fallback="true" :touch-start-threshold="50"
:fallback-class="true" :fallback-on-body="true" :fallback-tolerance="50"
>
<template #item="{ element, index }">
<div :class="element.disabledMove ? 'forbid item' : 'item'">
<label class="move">{{ element.name }}</label>
<p v-show="element.disabledPark" style="color:red">此处不允许拖拽和停靠</p>
<p v-show="!element.disabledPark">内容......</p>
</div>
</template>
</draggable>
</div>
<div class="group">
<draggable
:list="modules.group2" group="group1"
handle=".move" filter=".forbid"
@start="onStart" @end="onEnd" :move="onMove"
ghost-class="ghost" chosen-class="chosenClass" animation="300"
:force-fallback="true" :touch-start-threshold="50"
:fallback-class="true" :fallback-on-body="true" :fallback-tolerance="50"
>
<template #item="{ element, index }">
<div :class="element.disabledMove ? 'forbid item' : 'item'">
<label class="move">{{ element.name }}</label>
<p>内容....</p>
</div>
</template>
</draggable>
</div>
<div class="group">
<draggable
:list="modules.group3" group="group1"
handle=".move" filter=".forbid"
@start="onStart" @end="onEnd" :move="onMove"
ghost-class="ghost" chosen-class="chosenClass" animation="300"
:force-fallback="true" :touch-start-threshold="50"
:fallback-class="true" :fallback-on-body="true" :fallback-tolerance="50"
>
<template #item="{ element, index }">
<div :class="element.disabledMove ? 'forbid item' : 'item'">
<label class="move">{{ element.name }}</label>
<p>内容....</p>
</div>
</template>
</draggable>
</div>
</div>
<div>
<div>教程,https://www.itxst.com/vue-draggable-next/tutorial.html</div>
<div>案例,https://debug.itxst.com/js/byamn2ja</div>
<div>属性说明:</div>
<div style="display: flex;">
<pre>
animation:拖动时过渡动画持续时间
chosen-class:选中的样式
disabled:是否禁用拖拽组件
delay:鼠标按下多少秒之后可以拖拽元素
drag-class:拖动元素的样式
draggable:通过样式设置拖拽,:draggable=".item",样式类为item的元素才能被拖动
fallback-class:克隆选中元素的样式到跟随鼠标的样式
fallback-on-body:克隆的元素添加到文档的body中
fallback-tolerance:按下鼠标移动多少个像素才能拖动元素
filter:通过样式设置不拖拽,:filter=".unmover",设置了unmover样式的元素不允许拖动
force-fallback:忽略HTML5的拖拽行为
ghost-class:宿主样式
group:相同的组名可以相互拖拽
handle:通过样式设置拖拽,:handle=".mover",只有当鼠标在样式类为mover类的元素上才能触发拖动
</pre>
<pre>
list:数据源
scroll:有滚动区域是否允许拖拽
scroll-fensitivity:距离滚动区域多远时,滚动滚动条
scroll-fn:滚动回调函数
scroll-speed:滚动速度
sort:是否开启排序
touch-start-threshold:鼠标按下移动多少px才能拖动元素交换间隔的大小,可以查看菜单对应的属性说明
@start:开始拖拽事件
:move:拖拽中事件,可以用来控制是否允许停靠
@end:结束拖拽事件
vue-draggable组件的disabled、filter、handle、draggable这几个属性,优先级从高到低的排序是什么
disabled > handle > draggable(也可配置在插槽中) > filter
</pre>
</div>
</div>
</div>
<script>
const app = {
data() {
return {
modules: {
group1: [
{ name: "第1组-1", id: 1, disabledMove: true, disabledPark: true },
{ name: "第1组-2", id: 2, disabledMove: false, disabledPark: false },
{ name: "第1组-3", id: 3, disabledMove: false, disabledPark: false },
],
group2: [
{ name: "第2组-1", id: 5, disabledMove: false, disabledPark: false },
{ name: "第2组-2", id: 6, disabledMove: false, disabledPark: false },
{ name: "第2组-3", id: 7, disabledMove: false, disabledPark: false },
],
group3: [
{ name: "第3组-1", id: 8, disabledMove: false, disabledPark: false },
{ name: "第3组-2", id: 9, disabledMove: false, disabledPark: false },
],
},
}
},
//注册draggable组件
components: {
'draggable': window.vuedraggable
},
methods: {
//拖拽开始的事件
onStart() {
console.log("开始拖拽");
},
//拖拽结束的事件
onEnd() {
console.log("结束拖拽");
},
onMove(e) {
//不允许停靠
if (e.relatedContext.element.disabledPark == true) return false;
return true;
}
}
}
Vue.createApp(app).mount('#app')
</script>
<style>
body {
padding: 0px;
margin: 0px;
background-color: #f1f1f1;
}
.itxst {
background-color: #f1f1f1;
display: flex;
justify-content: space-between;
align-content: space-around;
padding: 20px;
}
.group {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-content: center;
width: 32%;
}
.item {
border: solid 1px #ddd;
padding: 0px;
text-align: left;
background-color: #fff;
margin-bottom: 10px;
display: flex;
flex-direction: column;
height: 100px;
user-select: none;
}
.item>label {
border-bottom: solid 1px #ddd;
padding: 6px 10px;
color: #333;
}
.item>label:hover {
cursor: move;
}
.item>p {
padding: 0 10px;
color: #666;
}
.ghost {
border: solid 1px rgb(19, 41, 239) !important;
}
.chosenClass {
opacity: 1;
border: solid 1px red;
}
.fallbackClass {
background-color: aquamarine;
}
</style>
</body>
</html>
九、vue从虚拟DOM到真实DOM的过程!!!
附、说明:
A、虚拟DOM是用JS对象模拟真实DOM结构的内存数据,它会占用浏览器的内存资源,且其Diff算法会消耗CPU资源
B、但相比频繁操作真实DOM(触发大量重排重绘),虚拟DOM能显著降低浏览器的整体性能开销
1、定义虚拟DOM节点构造函数
class VNode {
constructor(tag, data = {}, children = [], text = null) {
this.tag = tag; //标签名
this.data = data; //属性数据(class、style、事件等)
this.children = children; //子节点
this.text = text; //文本内容
this.key = data.key; //用于Diff的key
}
}
2、创建虚拟DOM树(设计图)
const vdomTree = new VNode(
'div',
{ id: 'app', class: 'container' },
[
new VNode('h1', { class: 'title' }, null, '虚拟DOM转真实DOM示例'),
new VNode(
'ul',
{ style: { 'list-style': 'none' } }, //Vue规范:style必须是对象
[
new VNode('li', { key: 1, class: 'item' }, null, '第一项'),
new VNode('li', { key: 2, class: 'item' }, null, '第二项'),
new VNode(
'li',
{ key: 3 },
[new VNode('span', { style: { color: 'blue' } }, null, '带子元素的项')]
)
]
),
new VNode(
'button',
{
class: 'btn',
on: { click: () => alert('按钮被点击') } //事件处理
},
null,
'点击我'
)
]
);
console.log(vdomTree);的执行结果如下
{
tag: 'div',
data: {
id: 'app',
class: 'container',
key: undefined //未指定key时为undefined
},
children: [
//第一个子节点:h1标题
{
tag: 'h1',
data: {
class: 'title',
key: undefined
},
children: null,
text: '虚拟DOM转真实DOM示例',
key: undefined
},
//第二个子节点:ul列表
{
tag: 'ul',
data: {
style: { 'list-style': 'none' },
key: undefined
},
children: [
//ul的第一个子节点:li
{
tag: 'li',
data: {
key: 1,
class: 'item'
},
children: null,
text: '第一项',
key: 1
},
//ul的第二个子节点:li
{
tag: 'li',
data: {
key: 2,
class: 'item'
},
children: null,
text: '第二项',
key: 2
},
//ul的第三个子节点:包含span的li
{
tag: 'li',
data: {
key: 3
},
children: [
//li的子节点:span
{
tag: 'span',
data: {
style: { color: 'blue' },
key: undefined
},
children: null,
text: '带子元素的项',
key: undefined
}
],
text: null,
key: 3
}
],
text: null,
key: undefined
},
//第三个子节点:button按钮
{
tag: 'button',
data: {
class: 'btn',
on: {
click: () => alert('按钮被点击') //绑定的点击事件
},
key: undefined
},
children: null,
text: '点击我',
key: undefined
}
],
text: null,
key: undefined
}
3、实现虚拟DOM转真实DOM的函数(按图施工)
function createElement(vnode) {
//处理文本节点
if (vnode.text !== null) {
return document.createTextNode(vnode.text);
}
//创建元素节点
const el = document.createElement(vnode.tag);
//处理属性
if (vnode.data) {
Object.keys(vnode.data).forEach(key => {
if (key === 'class') {
el.className = vnode.data.class;
}
else if (key === 'style') {
Object.assign(el.style, vnode.data.style); //style必须是对象
}
else if (key === 'on' && vnode.data.on) {
//绑定事件
Object.keys(vnode.data.on).forEach(event => {
el.addEventListener(event, vnode.data.on[event]);
});
}
else if (key !== 'key') { //key不渲染到真实DOM
el.setAttribute(key, vnode.data[key]);
}
});
}
//递归处理子节点
if (vnode.children && vnode.children.length) {
vnode.children.forEach(child => {
el.appendChild(createElement(child));
});
}
return el;
}
4、转换为真实DOM并插入到页面
//转换为真实DOM
const realDOM = createElement(vdomTree);
//插入到页面
document.body.appendChild(realDOM);
console.log('虚拟DOM转换完成,已插入页面');
5、最终渲染结果
<div id="app" class="container">
<h1 class="title">虚拟DOM转真实DOM示例</h1>
<ul style="list-style: none;">
<li class="item">第一项</li>
<li class="item">第二项</li>
<li>
<span style="color: blue;">带子元素的项</span>
</li>
</ul>
<button class="btn">点击我</button>
</div>
十、自定义组件
注、在Vue2、Vue3的单文件组件(SFC)模板中,支持驼峰命名和短横线分隔命名,
A、import CctvEllipsis from '../base/cctv-ellipsis.vue',
B、<CctvEllipsis />,
C、<cctv-ellipsis />
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
(1)定义组件,many-date
<script setup>
import {
DArrowLeft,
DArrowRight,
Calendar,
} from '@element-plus/icons-vue'
const props = defineProps({
showPeriod: ''
});
const emit = defineEmits(['dateOk'])
const period = ref(''); //统计周期
var date = new Date;
var initMonth = date.getMonth();
var initYear = date.getFullYear();
var clickYear = ref(initYear);//被使用的年份
var initItem = ref('');//当前所在的季节或半年
var clickItem = ref('');//默认的或被点击的季节或半年
var twoRef = ref('');
var fourRef = ref('');
var dateRange = ref('');
var isShowTwo = ref(false);
var isShowFour = ref(false);
// 以下处理统计周期改变
watch(() => props.showPeriod, (newValue, oldValue) => {
if (newValue !== oldValue) {
period.value = newValue;
changePeriod(period.value)
getInitItem()
if (period.value === '月'|| period.value === '全年') {
changeInput()
}
}
}, { immediate: true, deep: true })
function changePeriod(period) {
if(period === '月'){
if (initMonth == 0) {//如果现在处在今年一月,那么默认为去年十二月
initMonth = 12;
initYear--;
}
dateRange.value = [
initYear + '-' + String(1).padStart(2, '0'),
initYear + '-' + String(initMonth).padStart(2, '0')
];//initMonth可以用于表示上个月
}else if(period === '全年') {
initYear--;
dateRange.value = initYear.toString();
}else if(period === '季度') {
var four = {
3: 'four',
6: 'one',
9: 'two',
12: 'three',
};
for (var key in four) {
if (initMonth < key) {
if (key == 3) initYear--;//如果现在处在今年第一季度,那么默认为去年第四季度
clickFourAndTwo(four[key]);
break;
}
}
}else if(period === '半年') {
var two = {
6: 'down',
12: 'up',
}
for (var key in two) {
if (initMonth < key) {
if (key == 6) initYear--;//如果现在处在今年上半年,那么默认为去年下半年
clickFourAndTwo(two[key]);
break;
}
}
}
}
function getInitItem() {//获取当前所在的季节或半年
var type = clickItem.value;
var four = ['one','two','three','four','one'];
var two = ['up','down','up'];
if(four.indexOf(type) > -1){
initItem.value = four[four.indexOf(type) + 1]
}
if(two.indexOf(type) > -1){
initItem.value = two[two.indexOf(type) + 1]
}
}
function changeInput() {
emit('dateOk', dateRange.value)
}
// 以下处理季度和半年
function focusInput() {
clickYear.value = initYear;
if( period.value == '半年' ){
isShowTwo.value = true;
isShowFour.value = false;
setTimeout(function(){
twoRef.value.focus();
}, 150);
}
if( period.value == '季度' ){
isShowTwo.value = false;
isShowFour.value = true;
setTimeout(function(){
fourRef.value.focus();
}, 150);
}
}
function clickArrow(type) {
if(type === 'left') clickYear.value--;
if(type === 'right') clickYear.value++;
}
function getBlur() {
isShowTwo.value = false;
isShowFour.value = false;
}
function clickFourAndTwo(type) {
clickItem.value = type;
var obj = {
one: {
start: clickYear.value + '-01',
end: clickYear.value + '-03'
},
two: {
start: clickYear.value + '-04',
end: clickYear.value + '-06'
},
three: {
start: clickYear.value + '-07',
end: clickYear.value + '-09'
},
four: {
start: clickYear.value + '-10',
end: clickYear.value + '-12'
},
up: {
start: clickYear.value + '-01',
end: clickYear.value + '-06'
},
down: {
start: clickYear.value + '-07',
end: clickYear.value + '-12'
},
};
var start = obj[type].start;
var end = obj[type].end;
var temp = addSpace(6) + start + addSpace(8) + '-' + addSpace(8) + end;
isShowTwo.value = false;
isShowFour.value = false;
dateRange.value = temp;
emit('dateOk', [start, end])
}
function addSpace(num) { //添加空格
var str = '\xa0';
for(var i = 0; i < num; i++) str += '\xa0'
return str;
}
</script>
<template>
<div style="width: 100%;">
<!-- 以下调用已有组件,实现'月'和'全年'的功能 -->
<el-date-picker
v-if="period === '月'"
v-model="dateRange"
type="monthrange"
:editable="false"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%;"
format="YYYY-MM"
value-format="YYYY-MM"
@change="changeInput()"
/>
<el-date-picker
v-if="period === '全年'"
v-model="dateRange"
type="year"
:editable="false"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%;"
format="YYYY"
value-format="YYYY"
@change="changeInput()"
/>
<!-- 以下加工已有组件,实现'季度'和'半年'的功能 -->
<el-input
v-model="dateRange"
:disabled="isDisabled"
:readonly="true"
@focus="focusInput()"
v-if="period === '季度'|| period === '半年'"
>
<template #prefix>
<el-icon><Calendar /></el-icon>
</template>
</el-input>
<div class="many-date" tabindex="-1" v-if="isShowFour" ref="fourRef" @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" >{{ clickYear }}</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="clickFourAndTwo('one')"
:class="[
(initYear==clickYear-1 && initItem=='one')?'many-date-down-content-bgNow':null,
(initYear==clickYear && clickItem=='one')?'many-date-down-content-bgClick':null,
]"
>第一季度</div>
<div class="many-date-down-content" @click="clickFourAndTwo('two')"
:class="[
(initYear==clickYear && initItem=='two')?'many-date-down-content-bgNow':null,
(initYear==clickYear && clickItem=='two')?'many-date-down-content-bgClick':null,
]"
>第二季度</div>
</div>
<div class="many-date-down">
<div class="many-date-down-content" @click="clickFourAndTwo('three')"
:class="[
(initYear==clickYear && initItem=='three')?'many-date-down-content-bgNow':null,
(initYear==clickYear && clickItem=='three')?'many-date-down-content-bgClick':null,
]"
>第三季度</div>
<div class="many-date-down-content" @click="clickFourAndTwo('four')"
:class="[
(initYear==clickYear && initItem=='four')?'many-date-down-content-bgNow':null,
(initYear==clickYear && clickItem=='four')?'many-date-down-content-bgClick':null,
]"
>第四季度</div>
</div>
</div>
<div class="many-date" tabindex="-1" v-if="isShowTwo" ref="twoRef" @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" >{{ clickYear }}</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="clickFourAndTwo('up')"
:class="[
(initYear==clickYear-1 && initItem=='up')?'many-date-down-content-bgNow':null,
(initYear==clickYear && clickItem=='up')?'many-date-down-content-bgClick':null,
]"
>上半年</div>
<div class="many-date-down-content" @click="clickFourAndTwo('down')"
:class="[
(initYear==clickYear && initItem=='down')?'many-date-down-content-bgNow':null,
(initYear==clickYear && clickItem=='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>
(2)使用组件
<script setup>
const ruleShowForm = ref(false)
const operateText = ref('')
const ruleVersionForm = reactive({
period: '',
myDate: [],
})
const rules = {
period: [{ required: true, trigger: "blur", message: "统计周期不能为空" }],
myDate: [{ required: true, trigger: "blur", message: "日期范围不能为空" }],
};
const formRef = ref(null)
const showDialogRule = () => {
ruleShowForm.value = true;
operateText.value = '创建新版本'
formRef.value.clearValidate();
}
const createOrEdit = () => {
formRef.value.validate((valid) => {
if (valid) {
var data = {
period: ruleVersionForm.period,
myDate: ruleVersionForm.myDate,
}
updateVersion(data).then((res) => {
ruleShowForm.value = false;
getList();
}).catch(()=>{
ruleShowForm.value = false;
ElMessage("出错了");
});
} else {
ElMessage.error('表单验证失败');
}
});
}
function handleDateOk(data) {
console.log( data );
ruleVersionForm.myDate = data
formRef.value.validateField('myDate');
}
function clearSelect(key) {
formRef.value.clearValidate(key);
}
function changePeriod() {
formRef.value.clearValidate('myDate');
}
function validateSelect(key) {
formRef.value.validateField(key);
}
</script>
<template>
<div class="content-table">
<div class="content-table-header" style="padding-left: 0;">
<el-button type="primary" @click="showDialogRule()">创建新版本</el-button>
</div>
<el-dialog v-model="ruleShowForm" :title="operateText" width="600">
<el-form :model="ruleVersionForm" class="rebut-form" ref="formRef" :rules="rules" label-width="80px">
<el-form-item label="统计周期" prop="period">
<el-select
v-model="ruleVersionForm.period"
placeholder="请选择统计周期"
@focus="clearSelect('period')"
@change="changePeriod"
@blur="validateSelect('period')"
>
<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="ruleVersionForm.period"
>
<many-date :showPeriod="ruleVersionForm.period" @dateOk="handleDateOk" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="ruleShowForm = false">取消</el-button>
<el-button type="primary" @click="createOrEdit">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</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、自定义组件-水印组件Water1mark,来源:bcbf-web
附、图片加载后,只要在表单中输入文字,改变字体大小、颜色,图片的水印立即发生改变!!!
附、添加水印组件的实现逻辑
A、监听props.url变化,
a、new Image并监听其下载事件,给该实例的url赋值。在下载事件里,
b、给canvas标签设置宽高(会清空画布),
c、用标签的drawImage方法把实例绘制成图片,用标签的各种方法绘制初始水印
B、监听props.config的变化,
a、给canvas标签设置宽高(会清空画布),
b、用标签的drawImage方法把实例绘制成图片,用标签的各种方法绘制更新水印
附、为什么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: '',
font: {
fontSize: 16,
color: 'rgba(175, 175, 177, 0.5)',
},
rotate: -55,
gap: [10, 5],
offset: [10, 137],
});
const graph = ref(null);
const canvas = ref(null);
const img = ref(null);
const loading = ref(false)
const handleSave = () => {
loading.value = true
// 将canvas对象中的图像数据转换为base64编码的dataURL
// 这种dataURL可以直接在HTML中使用,或者通过JavaScript发送给服务器进行保存或传输
const imageData = canvas.value.toDataURL('image/png');
emit('watermarkUpdate', imageData, config.content);
loading.value = false
};
//以下,绘制图片
const drawCanvas = () => {
// 获取图片原始宽、高
let w = img.value.width;
let h = img.value.height;
// 按图片宽、高比例“缩放”图片(可选步骤)
const maxSize = 600;
if (w > h) {
// 宽大于高时,宽度缩放到maxSize,高度等比例缩放
h = h * (maxSize / w);
w = maxSize;
} else {
// 宽小于等于高时,高度缩放到maxSize,宽度等比例缩放
w = w * (maxSize / h);
h = maxSize;
}
// 设置Canvas尺寸(会清空画布)
canvas.value.width = w;
canvas.value.height = h;
// 在Canvas上绘制图片(从左上角开始,覆盖整个Canvas)
canvas.value.getContext('2d').drawImage(img.value, 0, 0, w, h);
// 以下,与canvas尺寸相关!!!
// 1、3种尺寸,图片尺寸、样式尺寸、绘制尺寸
// 2、canvas默认标签尺寸,宽300px;高150px;坐标原点(0,0)位于画布左上角,向右是x轴正方向,向下是y轴正方向
// 3、尺寸设置,js设置大于标签设置,同种设置时,最大尺寸大于尺寸
// 以下,drawImage,在canvas的上下文中绘制图像,项目中最常使用5个参数,使用3、5、9个参数说明
// 1、使用3个参数,drawImage(img,x,y)。不缩放,不裁切;可能不完整
// (1)img:必填,图像源。这可以是一个
// A、HTMLImageElement,创建方式为new Image()和document.getElementById("myImage")
// B、HTMLCanvasElement,创建方式为document.createElement("canvas")和document.getElementById("myCanvas")
// C、HTMLVideoElement,创建方式为document.createElement("video")和document.getElementById("myVideo")
// (2)x和y:必填,图像在画布上的起始坐标
// A、直接使用原图的自然宽高,即img.naturalWidth、img.naturalHeight
// B、如果原图比Canvas大,超出部分会被裁剪,不会自动缩放
// C、如果原图比Canvas小,剩余区域保持空白,不会自动拉伸
// 2、使用5个参数,drawImage(img, x, y, width, height)。只缩放,不裁切;可能变形
// (1)图片会被“强制缩放”至指定的width和height尺寸
// (2)若width/height比例与原图不一致,图像会发生变形(非等比例缩放)
// (3)如需保持宽高比,需手动计算并设置合适的width或height值
// 3、使用9个参数,drawImage(img,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight)。既缩放,又裁切;可能变形,可能不完整
// (1)前4个参数(sx,sy,sWidth,sHeight)用于裁切原图
// A、从原图的(sx,sy)坐标开始,截取宽为sWidth、高为sHeight的区域
// B、超出原图范围的部分会被忽略
// (2)后4个参数(dx,dy,dWidth,dHeight)用于定义裁切区域在画布上的绘制位置和尺寸
// A、将裁切后的区域绘制到画布的(dx, dy)坐标处
// B、并缩放至宽dWidth、高dHeight的尺寸
// (3)若sWidth/sHeight与dWidth/dHeight的比例不一致
// A、裁切后的区域会被拉伸或压缩,可能导致图像变形
// B、如需保持比例,需手动计算两组尺寸的比例并保持一致
// (4)此模式同时实现了“裁切原图局部”和“缩放绘制”的功能,适用于雪碧图(精灵图)展示、图片局部截取等场景
};
//以下,绘制文字
const drawText = () => {
//去掉下面拦截后产生的问题是:无法删除图片上水印的最后一个文字!!!
/* if(!config.content){//http://10.70.38.84/browse/YANSHOU-10133
return
} */
//以下,使用水印参数1/2
let textCtx = canvas.value.getContext('2d');
textCtx.fillStyle = config.font.color;
textCtx.font = config.font.fontSize + 'px Arial' ;
let baseWidth = textCtx.measureText('啊').width;
const list =config.content? config.content.split(/[(\r\n)\r\n]+/):[]; //一次水印可能有多行
let maxWidth = 0
list.forEach((item,index) => {
maxWidth = Math.max(maxWidth, item.length);
});
console.log(list)
let xAdd = config.offset[0]*baseWidth;
let yAdd = config.offset[1]*baseWidth - list.length*baseWidth;
textCtx.save();
textCtx.translate(xAdd, yAdd);
textCtx.rotate(config.rotate * Math.PI / 180);
list.forEach((item,index) => {
textCtx.fillText(item, 0, config.font.fontSize * index);
});
textCtx.restore();
};
//以下,绘制图片、绘制文字!!!
watch(() => props.imgUrl, () => {
if (props.imgUrl.length === 0) {
return;
}
img.value = new Image;
img.value.onload = function () {
drawCanvas();
//以下,生成水印参数2/2
let w = img.value.width;
let h = img.value.height;
if (w > h) {
config.font.fontSize = parseInt(w/20)
config.offset[0] = parseInt( w/2/config.font.fontSize - 5 )
config.offset[1] = parseInt( h/config.font.fontSize + 1 )
}else {
config.font.fontSize = parseInt(w/15)
config.offset[0] = parseInt( w/2/config.font.fontSize - 4.7 )
config.offset[1] = parseInt( h/config.font.fontSize - 0.3 )
}
drawText();
return canvas.value;
};
config.content = props.content
img.value.src = props.imgUrl; //1.4、生成img图片
}, { immediate: true });
//以下,(再次)绘制图片、绘制文字
watch(config, () => {
drawCanvas();
drawText()
})
</script>
<template>
<div class="Watermark__wrap">
<div ref="graph" class="Watermark__image">
<canvas ref="canvas" :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: 590px"
maxlength="50"
placeholder="请输入水印信息"
show-word-limit
v-model="config.content"
/>
</el-form-item>
<!-- YANSHOU-11161 -->
<el-form-item label="颜色">
<el-color-picker v-model="config.font.color" show-alpha />
</el-form-item>
<el-form-item label="字号">
<el-slider v-model="config.font.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.offset[0]" placeholder="左偏移" controls-position="right" />
<el-input-number v-model="config.offset[1]" placeholder="上偏移" 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;
&__div{
border: 1px solid lightgray ;
border-radius: 6px;
padding-left: 10px;
margin-bottom: 10px;
height: 32px;
line-height: 32px;
}
&__bg{
background-color: #F0F0F0;
cursor: pointer;
}
}
&__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">
<Water1mark :imgUrl="editWatermarkImageUrl" v-if="showEditWatermark" :content="watermark" @watermarkUpdate="handleWatermarkUpdate"/>
</el-dialog>
const handleWatermarkUpdate = (imageURL, content) => {//提交水印图片
showEditWatermark.value = false;
applicationInfo.value.licenses[editWatermarkIndex.value].base64Image = 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;
})
}
5、自定义组件-水印组件Water2mark,来源:bcbf-web
附、图片加载后,用固定文字,向图片添加循环“固定水印”,铺满图片,另向父组件提供下载功能!!!
附、添加水印组件的实现逻辑,监听props.url变化
A、new Image并监听其下载事件,给该实例的url赋值。在下载事件里
B、给canvas标签设置宽高(会清空画布)
C、用标签的drawImage方法把实例绘制成图片,用标签的各种方法绘制文字
(1)定义组件
<script setup>
const props = defineProps({
imgUrl: {//水印图片的url
type: String,
default: ''
},
content:{//水印文字
type: String,
default: ''
},
name:{//水印图片下载名称
type: String,
default: ''
},
size:{//水印图片大小
type: Object,
default: {}
}
});
const emit = defineEmits(['watermarkUpdate']);
let rate = 0.5;
let maxHeight = ref( 650*rate )
let maxWidth = ref( 540*rate )
const graph = ref(null);
const canvas = ref(null);
const img = ref(null);
//以下,绘制图片
const drawCanvas = () => {
// 重新设置canvas的宽高,会清空画布
let w = img.value.width;
let h = img.value.height;
canvas.value.width = w;
canvas.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
}
maxWidth.value = props.size.width||maxWidth.value
maxHeight.value = props.size.height||maxHeight.value
canvas.value.getContext('2d').drawImage(img.value, 0, 0, w, h);
};
//以下,绘制文字
const drawText = () => {
var ctx = canvas.value.getContext('2d');
if (!ctx) return;
ctx.save();
var size = img.value.width > img.value.height ? img.value.width : img.value.height;
var font = (size/30) +'px Arial'; //字体大小与图片大小绑定
ctx.font = font;
ctx.fillStyle = 'rgba(175, 175, 177, 0.5)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var radians = -45;
var textWidth = ctx.measureText(props.content).width;
var textHeight = parseInt(font, 10) || 20;
var offsetX = textWidth - 150;
var offsetY = textHeight + size/5; //y轴偏移量与图片大小绑定
// 计算对角线距离以确保全覆盖
var diagonal = Math.sqrt(canvas.value.width * canvas.value.width + canvas.value.height * canvas.value.height);
// 计算需要的水印数量
var cols = Math.ceil(diagonal / offsetX);
var rows = Math.ceil(diagonal / offsetY);
// 移动到画布中心(旋转后的中心)
ctx.translate(canvas.value.width / 2, canvas.value.height / 2);
// 绘制水印网格
for (let i = -rows; i < rows; i++) { //重复绘制水印!!!
for (let j = -cols; j < cols; j++) {
ctx.save();
// 移动到当前水印位置
ctx.translate(j * offsetX, i * offsetY);
// 旋转水印
ctx.rotate(radians);
// 绘制水印文字
ctx.fillText(props.content, 0, 0);
ctx.restore();
}
}
// 恢复画布状态
ctx.restore();
};
//以下,绘制图片、绘制文字!!!
watch(() => props.imgUrl, () => {
if (props.imgUrl.length === 0) {
return;
}
img.value = new Image;
img.value.onload = function () {
drawCanvas();
drawText();
return canvas.value;
};
img.value.src = props.imgUrl;
}, { immediate: true });
//以下,下载图片!!!
const downCanvas = () => {
const link = document.createElement('a');
link.download = (props.name || props.content || 'canvas' + '.png').replace(/\.\w+$/, '') + '.jpg';
link.href = canvas.value.toDataURL('image/png');
link.click();
};
defineExpose({
downCanvas,
});
</script>
<template>
<div class="water2mark__wrap">
<div ref="graph" class="water2mark__image">
<canvas ref="canvas" :style="{'max-height': maxHeight+'px','max-width':maxWidth+'px'}" ></canvas>
</div>
</div>
</template>
<style lang="scss">
.water2mark {
&__wrap {
display: flex;
flex-direction: row;
&__div{
border: 1px solid lightgray;
border-radius: 6px;
padding-left: 10px;
margin-bottom: 10px;
height: 32px;
line-height: 32px;
}
&__bg{
background-color: #F0F0F0;
cursor: pointer;
}
}
&__image {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
}
</style>
(2)使用组件
<Water2mark
:ref="(el) => getRef(el, index)"
:imgUrl="item.base64Image"
:content="watermarkText"
:name="'第'+(index+1)+'次频道换证证照'"
:size="sizeSmall" />
<el-button type="primary" link @click="downImg(item, index)">下载证照</el-button>
const itemRefs = ref([]);
const getRef = (el, index) => {
itemRefs.value[index] = el;
}
function downImg(item, index) {
itemRefs.value[index].downCanvas();
};
6、自定义组件-给PDF文件添加水印,来源:nm-web。内容共约1060行
附、PDF文件加载后,页面出现多张缩略图,在弹窗分页展示中,给选中的缩略图分别添加水印,暴露方法给父组件,供其获取所有的水印图片!!!
附、添加水印组件的实现逻辑,监听props.url变化,用pdfjs-dist@3.11.174提供的方法展示并更新图片
附、pdfjs-dist各版本发布时间与功能简介
A、1.0.1040,2015-09,1.x正式版,奠定PDF.js核心功能框架(基础渲染、分页、文本选择),兼容IE9+等旧浏览器
B、1.10.100,2018-05,1.x最后一个稳定版,冻结核心功能,仅保留后续安全补丁支持,兼容IE9+
C、2.0.943,2019-07,2.x正式发布,重构渲染引擎,提升性能,支持更多PDF2.0标准特性,兼容IE11+
D、2.16.105,2022-09,2.x最后一个维护版,保留IE11支持,后续版本(3.x+)放弃IE兼容,仅修复严重安全漏洞
E、3.0.279,2022-01,3.x正式发布版,升级依赖库,重构部分核心模块,首次引入.mjs模块化文件(兼容保留.js)
F、3.11.174,2023-05,3.x最后一个重要维护版,修复安全漏洞,优化PDF表单渲染兼容性
G、4.0.189,2023-07,4.x系列初始测试后的迭代版,主要优化ES模块(.mjs)的加载稳定性
H、4.10.38,2025-01,4.x偏向基础优化的版本,适配后续主流构建工具的兼容性调整
(1)定义组件
<template>
<div class="pdf-thumbnail-container">
<!-- 文件信息区域 -->
<!-- <div class="file-section"> -->
<!-- 文件信息 -->
<!-- <div v-if="fileInfo.name" class="file-info">
<p><strong>文件名:</strong>{{ fileInfo.name }}</p>
<p><strong>总页数:</strong>{{ fileInfo.totalPages }}页</p>
</div> -->
<!-- 提示信息 -->
<!-- <el-alert
v-if="showPageNotice"
title="提示:当前PDF超过10页,仅显示前10页"
type="info"
:closable="false"
show-icon
class="page-notice"
/> -->
<!-- </div> -->
<!-- 缩略图区域 -->
<div class="thumbnails-section">
<div v-if="thumbnails.length === 0" class="empty-state">
<el-empty description="请提供PDF文件URL" />
</div>
<div v-else class="thumbnails-grid">
<div
v-for="(thumbnail, index) in thumbnails"
:key="index"
class="thumbnail-item"
:class="{
selected: selectedPages.includes(thumbnail.pageNum),
watermarked: hasWatermark(thumbnail.pageNum)
}"
@click="togglePageSelection(thumbnail.pageNum)"
>
<div class="thumbnail-image">
<img :src="getThumbnailUrl(thumbnail.pageNum)" :alt="`第${thumbnail.pageNum}页`" />
<!-- “预览”按钮在右上方 -->
<div class="preview-btn top-right">
<el-button
size="small"
type="text"
@click.stop="openPreviewDialog(thumbnail.pageNum)"
>
预览
</el-button>
</div>
<!-- “已加水印”标签在左下方 -->
<!--
<div v-if="hasWatermark(thumbnail.pageNum)" class="watermark-badge bottom-left">
<el-tag size="small" type="success">已加水印</el-tag>
</div> -->
</div>
<div class="thumbnail-info">
<el-checkbox
:model-value="selectedPages.includes(thumbnail.pageNum)"
@click.stop
@change="togglePageSelection(thumbnail.pageNum)"
>
P{{ thumbnail.pageNum }}
</el-checkbox>
</div>
</div>
</div>
</div>
<!-- 大图预览弹窗 -->
<el-dialog
v-model="previewDialogVisible"
:title="`第 ${currentPreviewPageNum} 页预览`"
width="35%"
top="5vh"
:close-on-click-modal="true"
>
<div class="preview-dialog-content">
<div class="large-preview-container">
<div v-if="showLargePreviewLoading" class="loading-container">
<el-icon class="loading-icon"><Loading /></el-icon>
<span class="loading-text">大图加载中...</span>
</div>
<img
v-else
:src="currentLargePreviewImage"
alt="大图预览"
class="large-preview-image"
/>
<!--
<div v-if="hasWatermark(currentPreviewPageNum) && !showLargePreviewLoading" class="large-preview-watermark-badge">
<el-tag type="success">已加水印</el-tag>
</div> -->
</div>
<!--
<div class="preview-info">
<p><strong>页码:</strong>第 {{ currentPreviewPageNum }} 页</p>
<p><strong>尺寸:</strong>{{ currentPreviewSize }}</p>
</div> -->
</div>
<!--
<template #footer>
<div class="preview-dialog-footer">
<el-button @click="previewDialogVisible = false">关闭</el-button>
<el-button
type="primary"
@click="downloadPreviewImage"
:loading="downloading"
>
下载图片
</el-button>
</div>
</template> -->
</el-dialog>
<!-- 添加水印弹窗 -->
<el-dialog
v-model="watermarkDialogVisible"
title=""
width="50%"
:close-on-click-modal="false"
>
<!-- 水印设置 -->
<div class="watermark-dialog-content">
<!-- 预览区域 -->
<div class="preview-panel">
<div class="preview-container">
<div v-if="showPreviewLoading" class="loading-container">
<el-icon class="loading-icon"><Loading /></el-icon>
<span class="loading-text">图片加载中...</span>
</div>
<img
v-else
:src="currentPreviewImage"
alt="预览图"
class="preview-image"
/>
<!--
<div v-if="hasWatermark(currentPreviewPage) && !showPreviewLoading" class="preview-watermark-badge">
<el-tag type="warning">当前页面已有水印</el-tag>
</div> -->
</div>
<div class="pagination">
<el-button
:disabled="currentPreviewIndex === 0"
@click="showPrevPreview"
class="arrow-btn"
type="text"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<div class="page-counter">
{{ currentPreviewIndex + 1 }} <!-- / {{ selectedPages.length }} -->
</div>
<el-button
:disabled="currentPreviewIndex === selectedPages.length - 1"
@click="showNextPreview"
class="arrow-btn"
type="text"
>
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</div>
<!-- 设置区域 -->
<div class="settings-panel">
<el-form :model="watermarkSettings" label-width="100px" label-position="top">
<el-form-item label="水印信息">
<el-input
v-model="watermarkSettings.text"
placeholder="请输入水印信息"
type="textarea"
:rows="4"
:maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="颜色">
<el-color-picker v-model="watermarkSettings.color" show-alpha />
</el-form-item>
<el-form-item label="字号">
<el-slider v-model="watermarkSettings.fontSize" :min="5" :max="120" />
</el-form-item>
<el-form-item label="旋转">
<el-slider v-model="watermarkSettings.rotate" :min="-180" :max="180" />
</el-form-item>
<el-form-item label="行距">
<el-slider v-model="watermarkSettings.lineSpace" :min="6" :max="120" />
</el-form-item>
<el-form-item label="偏移">
<el-space>
<el-input-number v-model="watermarkSettings.left" placeholder="左偏移" controls-position="right" />
<el-input-number v-model="watermarkSettings.top" placeholder="上偏移" controls-position="right" />
</el-space>
</el-form-item>
</el-form>
<div class="apply-btn">
<el-button
type="primary"
@click="applyWatermarkToCurrentPage"
>确定</el-button>
<!--
<el-button
type="danger"
@click="clearWatermarkFromCurrentPage"
:disabled="!hasWatermark(currentPreviewPage)"
>清除水印</el-button> -->
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, ArrowRight, Loading } from '@element-plus/icons-vue'
/* ==================== 1、配置pdf.js ==================== */
pdfjsLib.GlobalWorkerOptions.workerSrc = import.meta.env.BASE_URL + 'pdfjs/pdf.worker.min.js'
//配置pdf.js说明,我已经把下面(1)处的文件复制到(2)处,你只需拉下代码即可
//目前使用的pdfjs-dist@3.11.174是本项目能运行的最高版本,不要试图使用此后的版本
//(1)C:\Users\Haier\Desktop\nm-web\node_modules\pdfjs-dist\legacy\build\pdf.worker.min.js
//(2)C:\Users\Haier\Desktop\nm-web\public\pdfjs\pdf.worker.min.js
/* ==================== 2、定义全局变量与方法 ==================== */
const largePreviewCanvases = ref(new Map()) //存储大图预览的canvas对象,key为页码,value为canvas对象
const pageWatermarkSettings = ref(new Map()) //存储每个页面的水印配置,key为页码,value为水印配置对象
const currPageCanvases = ref(new Map()) //存储每页的原始canvas对象,key为页码,value为canvas对象
const pastPageImages = ref(new Map()) //存储每页的原始canvas对象,key为页码,value包含宽高和dataURL
const watermarkedCanvases = ref(new Map()) //存储带水印的canvas对象,key为页码,value为已添加水印的canvas对象
const watermarkedPages = ref(new Set()) //存储已添加水印的页面编号
//保留默认配置,但不直接用于渲染
const defaultWatermarkSettings = {
text: '未来电视的PDF文件', //默认水印文字
color: '#666666', //默认水印颜色
fontSize: 30, //默认字体大小
rotate: -30, //默认旋转角度
lineSpace: 50, //默认行间距
left: 0, //默认水平偏移
top: 0 //默认垂直偏移
}
//更新watermarkSettings配置
const watermarkSettings = reactive({
text: '未来电视的PDF文件', //水印文字内容
color: '#666666', //水印颜色
fontSize: 30, //水印字体大小
rotate: -30, //水印旋转角度
lineSpace: 50, //水印行间距
left: 0, //水印水平偏移量
top: 0 //水印垂直偏移量
})
//应用水印到canvas的通用方法,支持指定页面配置
const applyWatermarkToCanvas = (context, canvasWidth, canvasHeight, pageNum = null) => {
//获取指定页面的水印配置,如果没有则使用当前watermarkSettings
const settings = pageNum && pageWatermarkSettings.value.has(pageNum)
? pageWatermarkSettings.value.get(pageNum)
: watermarkSettings
const { text, color, fontSize, rotate, lineSpace, left, top } = settings
//设置水印样式
context.fillStyle = color
context.font = `${fontSize}px Arial`
context.textAlign = 'center'
context.textBaseline = 'middle'
//测量文本宽度
const textMetrics = context.measureText(text)
const textWidth = textMetrics.width
//计算合理的间距
const horizontalGap = textWidth * 1.05
const verticalGap = lineSpace
//保存当前状态
context.save()
//应用偏移和旋转
context.translate(canvasWidth / 2 + left, canvasHeight / 2 + top)
context.rotate(rotate * Math.PI / 180)
//绘制网格水印 - 使用计算出的合理间距
for (let x = -canvasWidth; x < canvasWidth * 2; x += horizontalGap) {
for (let y = -canvasHeight; y < canvasHeight * 2; y += verticalGap) {
context.fillText(text, x, y)
}
}
//恢复状态
context.restore()
}
/* ==================== 3、页面初始化 ==================== */
const props = defineProps({ //PDF的URL
url: {
type: String,
required: true
}
})
const fileInfo = reactive({
name: '', //PDF文件名
totalPages: 0 //PDF总页数
})
const thumbnails = ref([]) //缩略图列表,包含每页的缩略图数据
const selectedPages = ref([]) //当前选中的页码数组
const showPageNotice = computed(() => fileInfo.totalPages > 10) //是否显示页数提示(超过10页时显示)
//加载PDF文档
const loadPdfDocument = async () => {
if (!props.url) return
try {
//从URL中提取文件名
const fileName = props.url.split('/').pop() || 'document.pdf'
fileInfo.name = fileName
fileInfo.totalPages = 0
//重置状态
selectedPages.value = []
currPageCanvases.value.clear()
pastPageImages.value.clear()
thumbnails.value = []
//重置水印状态
watermarkedPages.value.clear()
watermarkedCanvases.value.clear()
largePreviewCanvases.value.clear() //清除大图预览缓存
//加载PDF文档
const pdfDoc = await pdfjsLib.getDocument(props.url).promise
fileInfo.totalPages = pdfDoc.numPages
//生成缩略图(最多10页)
const displayPages = Math.min(pdfDoc.numPages, 10)
for (let pageNum = 1; pageNum <= displayPages; pageNum++) {
await generateThumbnail(pdfDoc, pageNum)
}
} catch (error) {
console.error('PDF处理错误:', error)
ElMessage.error('PDF文件处理失败')
}
}
//生成缩略图
const generateThumbnail = async (pdfDoc, pageNum) => {
try {
//获取PDF页面
const page = await pdfDoc.getPage(pageNum)
const viewport = page.getViewport({ scale: 0.2 }) //缩略图缩放比例!!!
//创建canvas
const eleCanvas = document.createElement('canvas')
const context = eleCanvas.getContext('2d') //获取2D绘图上下文
eleCanvas.width = viewport.width
eleCanvas.height = viewport.height
//渲染页面(调用PDF的方法,把PDF图片渲染到canvas里)
await page.render({
canvasContext: context,
viewport: viewport
}).promise
//保存原始canvas(用于水印处理)
const currCanvas = document.createElement('canvas')
currCanvas.width = eleCanvas.width
currCanvas.height = eleCanvas.height
currCanvas.getContext('2d').drawImage(eleCanvas, 0, 0)
currPageCanvases.value.set(pageNum, currCanvas)
//保存原始图片数据
pastPageImages.value.set(pageNum, {
width: eleCanvas.width,
height: eleCanvas.height,
dataUrl: eleCanvas.toDataURL()
})
//添加到缩略图列表
thumbnails.value.push({
pageNum,
dataUrl: eleCanvas.toDataURL()
})
} catch (error) {
console.error(`生成第${pageNum}页缩略图失败:`, error)
ElMessage.error(`第${pageNum}页缩略图生成失败`)
}
}
//点击缩略图
const togglePageSelection = (pageNum) => {
const index = selectedPages.value.indexOf(pageNum)
if (index === -1) {
selectedPages.value.push(pageNum)
} else {
selectedPages.value.splice(index, 1)
}
}
/* ==================== 4、大图预览弹窗 ==================== */
const previewDialogVisible = ref(false) //大图预览弹窗显示状态
const currentPreviewIndex = ref(0) //当前预览图片在选中页面中的索引
const currentPreviewPageNum = ref(1) //当前预览的页码
const previewLoading = ref(false)
const showPreviewLoading = computed(() => {
return previewLoading.value || !currentPreviewImage.value
})
const currentPreviewPage = computed(() => selectedPages.value[currentPreviewIndex.value]) //当前预览的页码(从选中的页面中获取)
const currentPreviewImage = computed(() => {
//优先返回带水印的图片,如果没有则返回原始图片
const watermarkedCanvas = watermarkedCanvases.value.get(currentPreviewPage.value)
if (watermarkedCanvas) {
return watermarkedCanvas.toDataURL() //返回带水印图片的dataURL
}
const canvas = currPageCanvases.value.get(currentPreviewPage.value)
return canvas ? canvas.toDataURL() : '' //返回原始图片的dataURL或空字符串
})
//打开大图预览弹窗
const openPreviewDialog = async (pageNum) => {
currentPreviewPageNum.value = pageNum
previewDialogVisible.value = true
largePreviewLoading.value = true //开始加载
//生成高质量大图预览
await generateLargePreview(pageNum)
largePreviewLoading.value = false //加载完成
}
//生成大图预览
const generateLargePreview = async (pageNum) => {
if (!props.url || largePreviewCanvases.value.has(pageNum)) {
largePreviewLoading.value = false
return
}
try {
const pdfDoc = await pdfjsLib.getDocument(props.url).promise
const page = await pdfDoc.getPage(pageNum)
const viewport = page.getViewport({ scale: 2.0 }) //更高质量的大图预览
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({
canvasContext: context,
viewport: viewport
}).promise
//如果该页面已有水印,应用水印到大图 - 使用该页面的特定配置
if (hasWatermark(pageNum)) {
applyWatermarkToCanvas(context, canvas.width, canvas.height, pageNum)
}
largePreviewCanvases.value.set(pageNum, canvas)
} catch (error) {
console.error('生成大图预览失败:', error)
ElMessage.error('大图预览生成失败')
}
}
//当前大图尺寸
const currentPreviewSize = computed(() => {
const canvas = largePreviewCanvases.value.get(currentPreviewPageNum.value)
if (canvas) {
return `${canvas.width} × ${canvas.height} 像素` //返回图片尺寸信息
}
return '未知尺寸' //默认返回未知尺寸
})
//当前大图图片
const currentLargePreviewImage = computed(() => {
const canvas = largePreviewCanvases.value.get(currentPreviewPageNum.value)
if (canvas) {
return canvas.toDataURL() //返回大图预览的dataURL
}
return getThumbnailUrl(currentPreviewPageNum.value) //如果没有大图,返回缩略图URL
})
//获取缩略图URL(优先返回带水印的缩略图)
const getThumbnailUrl = (pageNum) => {
const thumbnail = thumbnails.value.find(t => t.pageNum === pageNum)
if (!thumbnail) return ''
//如果有水印版本,返回水印版本
if (hasWatermark(pageNum)) {
const watermarkedCanvas = watermarkedCanvases.value.get(pageNum)
if (watermarkedCanvas) {
//创建缩略图尺寸的canvas
const thumbCanvas = document.createElement('canvas')
const pastImage = pastPageImages.value.get(pageNum)
if (pastImage) {
thumbCanvas.width = pastImage.width
thumbCanvas.height = pastImage.height
const context = thumbCanvas.getContext('2d')
context.drawImage(
watermarkedCanvas,
0, 0, watermarkedCanvas.width, watermarkedCanvas.height,
0, 0, thumbCanvas.width, thumbCanvas.height
)
return thumbCanvas.toDataURL()
}
}
}
//返回原始缩略图
return thumbnail.dataUrl
}
//检查页面是否有水印
const hasWatermark = (pageNum) => {
return watermarkedPages.value.has(pageNum)
}
//下载预览图片
const downloading = ref(false) //下载状态,控制下载按钮的loading效果
const largePreviewLoading = ref(false)
const showLargePreviewLoading = computed(() => {
return largePreviewLoading.value || !currentLargePreviewImage.value
})
const downloadPreviewImage = async () => {
const canvas = largePreviewCanvases.value.get(currentPreviewPageNum.value)
if (!canvas) {
ElMessage.warning('暂无预览图片可下载')
return
}
try {
downloading.value = true
const link = document.createElement('a')
link.download = `PDF页面_${currentPreviewPageNum.value}.png`
link.href = canvas.toDataURL('image/png')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
ElMessage.success('图片下载成功')
} catch (error) {
console.error('下载图片失败:', error)
ElMessage.error('图片下载失败')
} finally {
downloading.value = false
}
}
/* ==================== 5、添加水印弹窗 ==================== */
const watermarkDialogVisible = ref(false)
//打开水印弹窗
const openWatermarkDialog = async () => {
if (selectedPages.value.length === 0) return
selectedPages.value.sort((a, b) => a - b)
currentPreviewIndex.value = 0
watermarkDialogVisible.value = true
//为当前预览页面加载对应的水印配置
const currentPageNum = selectedPages.value[currentPreviewIndex.value]
if (pageWatermarkSettings.value.has(currentPageNum)) {
//如果该页面已有水印配置,加载到当前设置中
Object.assign(watermarkSettings, pageWatermarkSettings.value.get(currentPageNum))
} else {
//否则使用默认配置
Object.assign(watermarkSettings, defaultWatermarkSettings)
}
//为预览生成高质量图片
await generateHighQualityPreviews()
}
//生成高质量预览图(包含所有已选中PDF缩略图)
const generateHighQualityPreviews = async () => {
if (!props.url) return
try {
previewLoading.value = true //开始加载
const pdfDoc = await pdfjsLib.getDocument(props.url).promise
for (const pageNum of selectedPages.value) { //遍历所有已选中的PDF缩略图
//如果已经有高质量图片,跳过
if (currPageCanvases.value.get(pageNum)?.width > 800) continue
const page = await pdfDoc.getPage(pageNum)
const viewport = page.getViewport({ scale: 1.5 }) //提高预览质量!!!
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({
canvasContext: context,
viewport: viewport
}).promise
currPageCanvases.value.set(pageNum, canvas)
//如果该页面已有水印,重新应用水印到高质量图片
if (hasWatermark(pageNum)) {
await reapplyWatermarkToPage(pageNum, canvas)
}
}
} catch (error) {
console.error('生成高质量预览图失败:', error)
} finally {
previewLoading.value = false //加载完成
}
}
//重新应用水印到页面(用于高质量预览)
const reapplyWatermarkToPage = async (pageNum, currCanvas) => {
try {
const watermarkedCanvas = document.createElement('canvas')
watermarkedCanvas.width = currCanvas.width
watermarkedCanvas.height = currCanvas.height
const context = watermarkedCanvas.getContext('2d')
//先绘制原始内容
context.drawImage(currCanvas, 0, 0)
//应用水印 - 使用该页面的特定配置
applyWatermarkToCanvas(context, watermarkedCanvas.width, watermarkedCanvas.height, pageNum)
//更新存储
watermarkedCanvases.value.set(pageNum, watermarkedCanvas)
} catch (error) {
console.error('重新应用水印失败:', error)
}
}
//上一页预览
const showPrevPreview = () => {
if (currentPreviewIndex.value > 0) {
currentPreviewIndex.value--
updateWatermarkSettingsForCurrentPage()
}
}
//下一页预览
const showNextPreview = () => {
if (currentPreviewIndex.value < selectedPages.value.length - 1) {
currentPreviewIndex.value++
updateWatermarkSettingsForCurrentPage()
}
}
//更新当前页面的水印配置显示
const updateWatermarkSettingsForCurrentPage = () => {
const currentPageNum = selectedPages.value[currentPreviewIndex.value]
if (pageWatermarkSettings.value.has(currentPageNum)) {
//加载该页面的水印配置
Object.assign(watermarkSettings, pageWatermarkSettings.value.get(currentPageNum))
} else {
//使用默认配置
Object.assign(watermarkSettings, defaultWatermarkSettings)
}
}
//应用水印到当前页
const applyWatermarkToCurrentPage = async () => {
const currentPageNum = selectedPages.value[currentPreviewIndex.value]
try {
//清除旧水印(如果存在)
if (hasWatermark(currentPageNum)) {
watermarkedPages.value.delete(currentPageNum)
watermarkedCanvases.value.delete(currentPageNum)
largePreviewCanvases.value.delete(currentPageNum) //清除大图预览缓存
}
//保存当前页面的水印配置
pageWatermarkSettings.value.set(currentPageNum, {
...watermarkSettings
})
//重新生成高质量原始图片
const currCanvas = await generateHighQualityPage(currentPageNum)
if (!currCanvas) return
//创建新canvas用于添加水印,保持高质量尺寸
const watermarkedCanvas = document.createElement('canvas')
watermarkedCanvas.width = currCanvas.width
watermarkedCanvas.height = currCanvas.height
const context = watermarkedCanvas.getContext('2d')
//先绘制原始内容
context.drawImage(currCanvas, 0, 0)
//应用水印 - 使用当前页面的配置
applyWatermarkToCanvas(context, watermarkedCanvas.width, watermarkedCanvas.height, currentPageNum)
//更新存储 - 保持高质量尺寸
watermarkedCanvases.value.set(currentPageNum, watermarkedCanvas)
watermarkedPages.value.add(currentPageNum)
//更新缩略图显示
updateThumbnailDisplay(currentPageNum)
ElMessage.success('水印添加成功')
} catch (error) {
console.error('添加水印失败:', error)
ElMessage.error('水印添加失败')
}
}
//生成高质量页面
const generateHighQualityPage = async (pageNum) => {
if (!props.url) return null
try {
const pdfDoc = await pdfjsLib.getDocument(props.url).promise
const page = await pdfDoc.getPage(pageNum)
const viewport = page.getViewport({ scale: 1.5 }) //使用高质量缩放
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({
canvasContext: context,
viewport: viewport
}).promise
return canvas
} catch (error) {
console.error('生成高质量页面失败:', error)
return null
}
}
//清除当前页水印
const clearWatermarkFromCurrentPage = async () => {
const currentPageNum = selectedPages.value[currentPreviewIndex.value]
try {
await ElMessageBox.confirm(
'确定要清除当前页面的水印吗?',
'清除水印',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
watermarkedPages.value.delete(currentPageNum)
watermarkedCanvases.value.delete(currentPageNum)
largePreviewCanvases.value.delete(currentPageNum) //清除大图预览缓存
pageWatermarkSettings.value.delete(currentPageNum) //清除该页面的水印配置
//更新缩略图显示
updateThumbnailDisplay(currentPageNum)
ElMessage.success('水印清除成功')
} catch (error) {
if (error !== 'cancel') {
console.error('清除水印失败:', error)
ElMessage.error('水印清除失败')
}
}
}
//更新缩略图显示
const updateThumbnailDisplay = (pageNum) => {
const thumbnailIndex = thumbnails.value.findIndex(t => t.pageNum === pageNum)
if (thumbnailIndex !== -1) {
//触发重新渲染
thumbnails.value = [...thumbnails.value]
}
}
/* ==================== 6、暴露给父组件 ==================== */
const selectedPagesCount = computed(() => selectedPages.value.length)
//获取已选水印图片的数据
const getWatermarkedImages = async (pageNumbers) => {
const images = []
for (const pageNum of pageNumbers) {
const canvas = watermarkedCanvases.value.get(pageNum)
if (canvas) {
images.push({
pageNum,
dataUrl: canvas.toDataURL('image/png'),
width: canvas.width,
height: canvas.height
})
}
}
return images
}
//更新缩略图显示
const getWatermarkedSelectedImages = () => {
//找出既被选中又有水印的页面
const targetPages = selectedPages.value.filter(page => watermarkedPages.value.has(page))
const images = []
for (const pageNum of targetPages) {
const canvas = watermarkedCanvases.value.get(pageNum)
if (canvas) {
images.push({
pageNum,
dataUrl: canvas.toDataURL('image/png'),
width: canvas.width,
height: canvas.height,
fileName: `已加水印_第${pageNum}页.png`
})
}
}
return images
}
//监听URL变化
watch(() => props.url, (newUrl) => {
if (newUrl) {
loadPdfDocument()
}
})
//组件挂载时加载PDF
onMounted(() => {
if (props.url) {
loadPdfDocument()
}
})
//暴露给父组件的方法和属性
defineExpose({
openWatermarkDialog,
selectedPagesCount,
getSelectedPages: () => selectedPages.value,
getWatermarkedPages: () => Array.from(watermarkedPages.value),
getWatermarkedImages ,
getWatermarkedSelectedImages,
})
</script>
<style scoped>
.pdf-thumbnail-container {
margin: 0 auto;
padding: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
flex-direction: column;
}
.file-section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.file-info {
margin-top: 15px;
padding: 15px;
background: #e7f3ff;
border-radius: 6px;
}
.file-info p {
margin: 5px 0;
color: #606266;
}
.page-notice {
margin-top: 15px;
}
.thumbnails-section {
margin: 20px 0;
}
.empty-state {
text-align: center;
padding: 40px 0;
}
.thumbnails-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 20px;
margin-top: 20px;
}
.thumbnail-item {
/* border: 2px solid #e4e7ed;
border-radius: 8px; */
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.thumbnail-item:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* .thumbnail-item.selected { !!!
border-color: #409eff;
background: #f0f7ff;
} */
.thumbnail-item.watermarked {
border-color: #67c23a;
}
.thumbnail-image {
padding: 24px 12px 12px 12px;
text-align: center;
position: relative;
background: white;
}
.thumbnail-image img {
max-width: 100%;
height: auto;
/*
border-radius: 4px;
border: 1px solid #e4e7ed; */
}
/* 已加水印标签移到左下方!!! */
.watermark-badge.bottom-left {
position: absolute;
bottom: 5px;
left: 5px;
}
/* 预览按钮在右上方!!! */
.preview-btn.top-right {
position: absolute;
top: 2px;
right: 2px;
}
.preview-btn.top-right button{
color: #5321FF !important;
}
.thumbnail-info {
text-align: center;
margin-top: 8px;
}
.watermark-dialog-content {
display: flex;
gap: 30px;
min-height: 600px;
}
.preview-panel {
flex: 2;
display: flex;
flex-direction: column;
}
.preview-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
margin-bottom: 20px;
background: #f8f9fa;
max-height: 600px;
position: relative;
}
.preview-image {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
object-fit: contain;
}
.preview-watermark-badge {
position: absolute;
top: 10px;
left: 10px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
}
.arrow-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
}
.page-counter {
font-size: 16px;
font-weight: 600;
color: #606266;
min-width: 60px;
text-align: center;
}
.settings-panel {
width: 420px;
display: flex;
flex-direction: column;
}
.apply-btn {
margin-top: auto;
display: flex;
justify-content: flex-start;
gap: 10px;
}
/* 大图预览弹窗样式 */
.preview-dialog-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.large-preview-container {
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
min-height: 500px;
position: relative;
}
.large-preview-image {
max-width: 100%;
max-height: 70vh;
width: auto;
height: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
object-fit: contain;
}
.large-preview-watermark-badge {
position: absolute;
top: 20px;
left: 20px;
}
.preview-info {
padding: 15px;
background: #e7f3ff;
border-radius: 6px;
}
.preview-info p {
margin: 5px 0;
color: #606266;
}
.preview-dialog-footer {
display: flex;
justify-content: center;
gap: 15px;
}
:deep(.el-dialog__body) {
padding: 20px;
}
/* 优化弹窗样式 */
:deep(.el-dialog) {
max-width: 90%;
margin-top: 5vh !important;
}
:deep(.el-dialog__header) {
padding: 20px 20px 10px;
}
:deep(.el-dialog__title) {
font-size: 18px;
font-weight: bold;
}
/* 添加loading样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #909399;
}
.loading-icon {
font-size: 40px;
margin-bottom: 16px;
animation: rotating 2s linear infinite;
}
.loading-text {
font-size: 14px;
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 确保预览容器有合适的高度 */
.preview-container,
.large-preview-container {
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
(2)使用组件
<el-row class="license-page-row">
<div class="apply-detail-label">
证照页码:
</div>
<div class="pdf-thumbnail-wrapper">
<PdfThumbnail
ref="pdfThumbnailRef"
:url="pdfUrl"
/>
</div>
</el-row>
// 以下水印相关
import PdfThumbnail from './pdf.vue'
const pdfUrl = ref('./749.pdf')
// 获取子组件实例
const pdfThumbnailRef = ref(null)
const watermarkedImages = ref([])
// 计算属性
const hasSelectedPages = computed(() => {
return pdfThumbnailRef.value?.selectedPagesCount > 0
})
const selectedPagesCount = computed(() => {
return pdfThumbnailRef.value?.selectedPagesCount || 0
})
// 打开水印弹窗
const handleAddWatermark = () => {
if (pdfThumbnailRef.value) {
pdfThumbnailRef.value.openWatermarkDialog()
}
}
const hasWatermarkedSelected = computed(() => {
return watermarkedSelectedCount.value > 0
})
const watermarkedSelectedCount = computed(() => {
if (!pdfThumbnailRef.value) return 0
const selectedPages = pdfThumbnailRef.value.getSelectedPages?.() || []
const watermarkedPages = pdfThumbnailRef.value.getWatermarkedPages?.() || []
// 找出既被选中又有水印的页面
return selectedPages.filter(page => watermarkedPages.includes(page)).length
})
// 在父组件中调用子组件的方法
const handleGetWatermarkedImages = () => {
if (!pdfThumbnailRef.value) {
ElMessage.warning('PDF组件未初始化')
return
}
try {
// 方法1:获取已选水印图片的数据
const images = pdfThumbnailRef.value.getWatermarkedSelectedImages()
// 方法2:获取基本信息
// const info = pdfThumbnailRef.value.getWatermarkedSelectedInfo()
// 方法3:直接下载
// pdfThumbnailRef.value.downloadWatermarkedSelectedImages()
if (images.length === 0) {
ElMessage.warning('没有找到已加水印且勾选的图片')
return
}
watermarkedImages.value = images
console.log( watermarkedImages.value );
ElMessage.success(`成功获取 ${images.length} 张已加水印且勾选的图片`)
} catch (error) {
console.error('获取水印图片失败:', error)
ElMessage.error('获取水印图片失败')
}
}