整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

高质量 Vue.js 自定义弹窗组件VPopup

很早之前就有写过一个wcPop.js弹窗插件,并且在h5酒店预订、h5聊天室项目中都有使用过,效果还不错。当初想着有时间整合一个Vue版本,刚好趁着国庆节空闲时间捣鼓了个vue.js版自定义模态弹出框组件VPopup

v-popup 一款聚合Msg、Dialog、Popup、ActionSheet、Toast等功能的轻量级移动端Vue弹窗组件。

整合了有赞VantNutUI等热门Vue组件库中Popup弹出层、Toast轻提示、Notify消息提示、Dialog对话框及ActionSheet动作面板等功能。

使用组件

// 在main.js中全局引入
import Vue from 'vue'

import Popup from './components/popup'
Vue.use(Popup)

支持如下两种方式调用组件。

  • 标签式调用
<v-popup
  v-model="showPopup"
  title="标题内容" 
  content="弹窗内容,告知当前状态、信息和解决方法,描述文字尽量控制在三行内"
  type="android"
  shadeClose="false"
  xclose
  z-index="2000"
  :btns="[
    {...},
    {...},
  ]"
/>
  • 函数式调用

this.$vpopup({...}),传入参数即可使用,该函数会返回弹窗组件实例。

let $el = this.$vpopup({
  title: '标题内容',
  content: '弹窗内容,描述文字尽量控制在三行内',
  type: 'android',
  shadeClose: false,
  xclose: true,
  zIndex: 2000,
  btns: [
    {text: '取消'},
    {
      text: '确认',
      style: 'color:#f60;',
      click: () => {
        $el.close()
      }
    },
  ]
});

你可根据喜好或项目需要任意选择一种调用方式即可。下面就开始讲解下组件的实现。

在components目录下新建popup.vue页面。

组件参数配置

<!-- Popup 弹出层模板 -->
<template>
  <div v-show="opened" class="nuxt__popup" :class="{'nuxt__popup-closed': closeCls}" :id="id">
    <div v-if="JSON.parse(shade)" class="nuxt__overlay" @click="shadeClicked" :style="{opacity}"></div>
    <div class="nuxt__wrap">
      <div class="nuxt__wrap-section">
        <div class="nuxt__wrap-child" :class="['anim-'+anim, type&&'popui__'+type, round&&'round', position]" :style="popupStyle">
          <div v-if="title" class="nuxt__wrap-tit" v-html="title"></div>
          <div v-if="type=='toast'&&icon" class="nuxt__toast-icon" :class="['nuxt__toast-'+icon]" v-html="toastIcon[icon]"></div>
          <template v-if="$slots.content">
            <div class="nuxt__wrap-cnt"><slot name="content" /></div>
          </template>
          <template v-else>
            <div v-if="content" class="nuxt__wrap-cnt" v-html="content"></div>
          </template>
          <slot />
          <div v-if="btns" class="nuxt__wrap-btns">
            <span v-for="(btn,index) in btns" :key="index" class="btn" :class="{'btn-disabled': btn.disabled}" :style="btn.style" @click="btnClicked($event,index)" v-html="btn.text"></span>
          </div>
          <span v-if="xclose" class="nuxt__xclose" :class="xposition" :style="{'color': xcolor}" @click="close"></span>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  // 弹窗索引,遮罩次数,定时器
  let $index = 0, $lockCount = 0, $timer = {};
  export default {
    props: {
      ...
    },
    data() {
      return {
        opened: false,
        closeCls: '',
        toastIcon: {
          loading: '<svg viewBox="25 25 50 50"><circle fill="none" cx="50" cy="50" r="20"></circle></svg>',
          success: '<svg viewBox="0 0 1024 1024"><path fill="none" d="M75.712 445.712l240.176 185.52s13.248 6.624 29.808 0l591.36-493.872s84.272-17.968 68.64 71.488c-57.04 57.968-638.464 617.856-638.464 617.856s-38.096 21.536-74.544 0C256.272 790.256 12.816 523.568 12.816 523.568s-6.672-64.592 62.896-77.856z"/></svg>',
          fail: '<svg viewBox="0 0 1024 1024"><path fill="none" d="M450.602 665.598a62.464 62.464 0 0 0 122.88 0l40.96-563.198A102.615 102.615 0 0 0 512.042 0a105.256 105.256 0 0 0-102.4 112.64l40.96 552.958zm61.44 153.6a102.4 102.4 0 1 0 102.4 102.4 96.74 96.74 0 0 0-102.4-102.4z"/></svg>',
        }
      }
    },
    watch: {
      value(val) {
        const type = val ? 'open' : 'close';
        this[type]();
      },
    },
    methods: {
      // 打开弹窗
      open() {
        if(this.opened) return;
        this.opened = true;
        this.$emit('open');
        typeof this.onOpen === 'function' && this.onOpen();
        this.$el.style.zIndex = this.getZIndex() + 1;
        
        if(JSON.parse(this.shade)) {
          if(!$lockCount) {
            document.body.classList.add('nt-overflow-hidden');
          }
          $lockCount++;
        }
        
        // 倒计时关闭
        if(this.time) {
          $index++;
          // 防止重复点击
          if($timer[$index] !== null) clearTimeout($timer[$index])
          $timer[$index] = setTimeout(() => {
            this.close();
          }, parseInt(this.time) * 1000);
        }

        // 长按/右键弹窗
        if(this.follow) {
          // 避免获取不到弹窗宽高
          this.$nextTick(() => {
            let obj = this.$el.querySelector('.nuxt__wrap-child');
            let oW, oH, winW, winH, pos;

            oW = obj.clientWidth;
            oH = obj.clientHeight;
            winW = window.innerWidth;
            winH = window.innerHeight;
            pos = this.getPos(this.follow[0], this.follow[1], oW, oH, winW, winH);

            obj.style.left = pos[0] + 'px';
            obj.style.top = pos[1] + 'px';
          });
        }
      },
      // 关闭弹窗
      close() {
        if(!this.opened) return;
        
        this.closeCls = true;
        setTimeout(() => {
          this.opened = false;
          this.closeCls = false;
          if(JSON.parse(this.shade)) {
            $lockCount--;
            if(!$lockCount) {
              document.body.classList.remove('nt-overflow-hidden');
            }
          }
          if(this.time) {
            $index--;
          }
          this.$emit('input', false);
          this.$emit('close');
          typeof this.onClose === 'function' && this.onClose();
        }, 200);
      },

      // 点击遮罩层
      shadeClicked() {
        if(JSON.parse(this.shadeClose)) {
          this.close();
        }
      },
      // 按钮事件
      btnClicked(e, index) {
        let btn = this.btns[index];
        if(!btn.disabled) {
          typeof btn.click === 'function' && btn.click(e)
        }
      },
      // 获取弹窗层级
      getZIndex() {
        for(var $idx = parseInt(this.zIndex), $el = document.getElementsByTagName('*'), i = 0, len = $el.length; i < len; i++)
          $idx = Math.max($idx, $el[i].style.zIndex)
        return $idx;
      },
      // 获取弹窗坐标点
      getPos(x, y, ow, oh, winW, winH) {
        let l = (x + ow) > winW ? x - ow : x;
        let t = (y + oh) > winH ? y - oh : y;
        return [l, t];
      }
    },
  }
</script>

通过监听v-model值调用open和close方法。

watch: {
    value(val) {
        const type = val ? 'open' : 'close';
        this[type]();
    },
},

如果想要实现函数式调用this.$vpopup({...}),则需要使用到Vue.extend扩展实例构造器。

import Vue from 'vue';
import VuePopup from './popup.vue';

let PopupConstructor = Vue.extend(VuePopup);

let $instance;

let VPopup = function(options = {}) {
    // 同一个页面中,id相同的Popup的DOM只会存在一个
    options.id = options.id || 'nuxt-popup-id';

    $instance = new PopupConstructor({
        propsData: options
    });

    $instance.vm = $instance.$mount();
    
    let popupDom = document.querySelector('#' + options.id);
    if(options.id && popupDom) {
        popupDom.parentNode.replaceChild($instance.$el, popupDom);
    } else {
        document.body.appendChild($instance.$el);
    }

    Vue.nextTick(() => {
        $instance.value = true;
    })

    return $instance;
}

VPopup.install = () => {
    Vue.prototype['$vpopup'] = VPopup;
    Vue.component('v-popup', VuePopup);
}

export default VPopup;

这样就实现了引入 Popup 组件后,会自动在 Vue 的 prototype 上挂载 $vpopup 方法和注册 v-popup 组件。

下面就可以愉快的使用标签式及函数式调用组件了。

  • 设置圆角及关闭按钮

<v-popup v-model="showActionPicker" anim="footer" type="actionsheetPicker" round title="标题内容"
    :btns="[
        {text: '取消', click: () => showActionPicker=false},
        {text: '确定', style: 'color:#00e0a1;', click: () => null},
    ]"
>
    <ul class="goods-list" style="padding:50px;text-align:center;">
        <li>双肩包</li>
        <li>鞋子</li>
        <li>运动裤</li>
    </ul>
</v-popup>

<v-popup v-model="showBottom" position="bottom" round xclose title="标题内容">
    <ul class="goods-list" style="padding:50px;text-align:center;">
        <li>双肩包</li>
        <li>鞋子</li>
        <li>运动裤</li>
    </ul>
</v-popup>
  • 设置按钮禁用状态

按钮设置disabled: true即可禁用按钮事件。

<v-popup v-model="showActionSheet" anim="footer" type="actionsheet" :z-index="2020"
    content="弹窗内容,描述文字尽量控制在三行内"
    :btns="[
        {text: '拍照', style: 'color:#09f;', disabled: true, click: handleInfo},
        {text: '从手机相册选择', style: 'color:#00e0a1;', click: handleInfo},
        {text: '保存图片', style: 'color:#e63d23;', click: () => null},
        {text: '取消', click: () => showActionSheet=false},
    ]"
/>

另外还支持自定义slot插槽内容,当 content 和 自定义插槽 内容同时存在,只显示插槽内容。

<v-popup v-model="showComponent" xclose xposition="bottom" content="这里是内容信息"
    :btns="[
        {text: '确认', style: 'color:#f60;', click: () => showComponent=false},
    ]"
    @open="handleOpen" @close="handleClose"
>
    <template #content>当 content 和 自定义插槽 内容同时存在,只显示插槽内容!</template>
    <div style="padding:30px 15px;">
        <img src="assets/apple3.jpg" style="width:100%;" @click="handleContextPopup" />
    </div>
</v-popup>

好了,就分享到这里。希望对大家有所帮助。目前该组件正在项目中实战测试,后续会分享相关使用情况。

嗨,各位极客;在你的浏览器中装上了多少插件呢?让我猜猜:tampermonkey 油猴脚本,Chrono下载管理器,bilibilihelper,喔当然还有必不可少的 Adblock。有了解过这些插件是怎么运作的么?想要完成一个自己的插件么?快和我一起动手吧!


基础知识

本文参考 Chrome Extensions V3版本 开发指南,接下来我们简单地介绍一下一个插件的组成,

Chrome 插件通常由以下几部分组成:

  • manifest.json:插件的 meta 信息,包含插件的名称、版本号、图标、脚本文件名称等信息,从这里你可以看到全部的参数。
  • popup:点击插件触发的弹框页面,会创建一个独立的浏览器页面实例。
  • options:插件的扩展配置页面,会创建一个独立的浏览器页面实例。
  • background.js:插件的后台运行脚本,单独地运行在一个浏览器实例中。


manifest.json 示例

{
  "manifest_version": 3,
  "name": "Chrome 插件开发 示例",
  "description": "这是一个 Chrome 开发示例工程",
  "version": "1.0",
  "permissions": [ // 插件需要的权限信息 依次为:数据存储、访问活跃的标签页、执行插入脚本
    "storage", "activeTab", "scripting"
  ],
  // 时间
  "action": {
    // 设置插件在插件区域的icon
    "default_icon": {
      "16": "readIcon.png",
      "32": "readIcon.png",
      "64": "readIcon.png",
      "128": "readIcon.png"
    },
    // 插件在插件区域被点击时的弹出页面
    "default_popup": "popup.html"
  },
  // 后台运行脚本
  "background": {
    "service_worker": "background.js"
  },
  // 全局icon 会出现在配置页面的icon中
  "icons": {
    "16": "bookIcon.png",
    "32": "bookIcon.png",
    "64": "bookIcon.png",
    "128": "bookIcon.png"
  },
  // 配置页面
  "options_page": "options.html"
}

popup 事件

在插件区域点击咱们插件后将触发popup 事件,唤起 popup.html 页面

option 事件

点击右键插件中的选项按钮,将触发option 事件,唤起 options.html 页面

background 事件

当插件被载入后,将后台执行 background.js 脚本

开始动手

我们将按照官方示例完成一个示例工程,在这个工程中,我们将提供一个可以设置网页背景颜色的小工具,并且在配置页面提供多个颜色供用户选择。


那我们就开始吧!先创建一个文件夹,命名为 start,然后用编辑器打开文件夹,开始编码啦,我使用的是vscode,当然任何编辑器都可以完成这项编码。

创建 manifest.json 描述文件

创建一个 manifest.json 文件

{
  "manifest_version": 3,
  "name": "Chrome 插件开发 示例",
  "description": "这是一个 Chrome 开发示例工程",
  "version": "1.0",
  "permissions": [
    "storage", "activeTab", "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icon.png",
    "32": "icon.png",
    "64": "icon.png",
    "128": "icon.png"
  },
}

找一张你喜欢的照片,命名为icon并添加到文件夹中,这里先没有配置popup页面和option页面,不着急,一步步来。

Background Js 后台执行程序

创建一个 background.js 文件

// 初始化一个颜色值
let color = '#3aa757';

// 在chrome浏览器创建的时候,设置颜色初始值
chrome.runtime.onInstalled.addListener(() => {
  // 需要注意的是,此时设置的持久对象的键名为 color 其值为 #3aa757
  chrome.storage.sync.set({ color });
  console.log('设置一个默认的颜色,默认颜色值为绿色 %cgreen', `color: ${color}`);
});

然后就可以尝试一下插件的运行啦,进入插件页面,先在右上角开启开发者模式,然后点击加载已解压的扩展程序,找到你的 start 文件夹加载进来。

此时页面上就会出现你的插件了,你会发现在有一个Service Worker视图可以点击,点击查看一下

你就可以看到 background.js 成功运行并打印了日志

Popup 弹出页面

接下来我们配置一个弹出页面,创建 popup.html

<!doctype html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>插件弹出页面</title>
    <link rel="stylesheet" href="button.css">
  </head>
  <body>
    <button id="changeColor"></button>
    <img url="icon.png"/>
    <!-- 引入js -->
    <script src="popup.js"></script>
  </body>
</html>

创建 popup.js

// 获得 改变颜色的按钮
let changeColor = document.getElementById("changeColor");

// 获取当前已设置颜色
chrome.storage.sync.get("color", ({ color }) => {
  changeColor.style.backgroundColor = color;
});

// 创建按钮点击事件 触发对当前激活的浏览器窗口中的当前激活的选项卡设置背景颜色
changeColor.addEventListener("click", async () => {
  let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  
  // 注入执行js代码
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    function: setPageBackgroundColor,
  });
});

// 注入执行的方法块
// 设置body的背景颜色为目标颜色
function setPageBackgroundColor() {
  chrome.storage.sync.get("color", ({ color }) => {
    document.body.style.backgroundColor = color;
  });
}

创建 button.css

body {
  min-width: 357px;
  overflow-x: hidden;
}

img {
  margin: 5px;
  border: 2px solid black;
  vertical-align: middle;
  width: 75px;
  height: 75px;
}

button {
  height: 30px;
  width: 30px;
  outline: none;
  margin: 10px;
  border: none;
  border-radius: 2px;
}

button.current {
  box-shadow: 0 0 0 2px white,
              0 0 0 4px black;
}

喔当然,还要修改 manifest.json,添加上 popup.html的配置,还需要准备一张在插件区域展示的 popupIcon 照片。

{
  "manifest_version": 3,
  "name": "Chrome 插件开发 示例",
  ... 省略 ...
    "128": "icon.png"
  },
  "action": {
    "default_icon": {
      "16": "popupIcon.png",
      "32": "popupIcon.png",
      "64": "popupIcon.png",
      "128": "popupIcon.png"
    },
    "default_popup": "popup.html"
  }
}

然后在插件页面刷新重新加载

这时候我们就可以点击插件啦,按照1点击插件,然后点击2,触发按钮事件

bingo! 当前页面的背景颜色变成绿色了。

似乎只有一个绿色不太合适,我们得为用户提供更多的选择,那就再做一个选项页面,提供配置功能吧。


Option 选项配置页面

创建 options.html

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>插件选项配置页面</title>
  <link rel="stylesheet" href="button.css">
</head>

<body>
  <div id="buttonDiv">
  </div>
  <div>
    <p>选择一个新的背景颜色</p>
  </div>
  <script src="options.js"></script>
</body>

</html>

创建 options.js

// 获取按钮区域 div
let page = document.getElementById("buttonDiv");
// button.css 有一个 button current css 样式,为选中的颜色添加出黑色边框
let selectedClassName = "current";
// 设置待选颜色按钮
const presetButtonColors = ["#3aa757", "#e8453c", "#f9bb2d", "#4688f1"];

// 创建按钮事件 通过标记所选按钮 保存颜色
function handleButtonClick(event) {
  // 从事件的父级中找到之前被选中的按钮
  let current = event.target.parentElement.querySelector(
    `.${selectedClassName}`
  );
  // 从他的class列表中去掉选中状态
  if (current && current !== event.target) {
    current.classList.remove(selectedClassName);
  }

  // 获取按钮携带的数据信息 也就是我们想要的颜色
  let color = event.target.dataset.color;
  // 添加选中状态
  event.target.classList.add(selectedClassName);
  // 设置当前选中颜色
  chrome.storage.sync.set({ color });
}

// 按颜色列表依次添加按钮到页面
function constructOptions(buttonColors) {
  // 获取当前已设置的颜色
  chrome.storage.sync.get("color", (data) => {
    let currentColor = data.color;
    // 循环颜色列表
    for (let buttonColor of buttonColors) {
      // 创建按钮 赋予按钮颜色
      let button = document.createElement("button");
      button.dataset.color = buttonColor;
      button.style.backgroundColor = buttonColor;

      // 如果是当前已选中的按钮,则设置
      if (buttonColor === currentColor) {
        button.classList.add(selectedClassName);
      }

      // 创建点击事件
      button.addEventListener("click", handleButtonClick);
      // 添加回页面上
      page.appendChild(button);
    }
  });
}

// js 被加载后 自执行初始化方法 创建按钮
constructOptions(presetButtonColors);

然后再修改一次 manifest.json

{
  "manifest_version": 3,
  "name": "Chrome 插件开发 示例",
  ... 省略 ...
    "default_popup": "popup.html"
  },
  "options_page": "options.html"
}

然后再重载一次插件,bingo!右键我们的插件就多出选项页面啦

点击进入选项页面,出现了我们在代码中配置的四个颜色了,随便点选其他三种颜色。

我们就可以惊喜的发现在popup页面的按钮颜色也发生了变化了。

小结

至此,我们的起步示例工程的开发就完成了。

在这次工程中,我们完成了配置基本信息、开发了popup 弹出页面、option 配置页面,并实现了多页面间的数据共享功能,并了解到各个页面间的通信都需要通过第三者进行处理,因为本质上每个页面都是独立的进程。


那我想提个小问题,如果我想在选项配置页面选择了颜色之后,然后再点击到一个具体的选项卡中自动帮我修改背景颜色,应该怎么实现呢?


谷歌插件示例工程

https://github.com/GoogleChrome/chrome-extensions-samples

谷歌插件官方文档

https://developer.chrome.com/docs/extensions/


Vue中实现打印预览和打印功能,你可以使用原生的JavaScript方法,或者使用第三方库,如vue-html2pdf 或 print.js。

以下是使用原生JavaScript方法实现打印功能的基本步骤:

  1. 创建一个新的窗口或者iframe,将要打印的内容写入。
  2. 调用新窗口或iframe的print()方法。

以下是一个简单的Vue组件示例,它包含一个打印按钮,点击该按钮会触发打印功能:

<template>  
  <div>  
    <div ref="printSection">  
      <h1>{{ title }}</h1>  
      <p>{{ description }}</p>  
    </div>  
    <button @click="print">打印</button>  
  </div>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      title: 'Vue 打印示例',  
      description: '这是一个Vue打印功能的简单示例。'  
    }  
  },  
  methods: {  
    print() {  
      let printContents = this.$refs.printSection.innerHTML;  
      let originalContents = document.body.innerHTML;  
  
      document.body.innerHTML = printContents;  
  
      window.print();  
  
      document.body.innerHTML = originalContents;  
    }  
  }  
}  
</script>

但是这种方法有个问题,就是会改变当前页面的内容。为了避免这个问题,我们可以使用一个新的窗口或者iframe来进行打印。

这是一个使用新窗口进行打印的示例:

<template>  
  <div>  
    <div ref="printSection">  
      <h1>{{ title }}</h1>  
      <p>{{ description }}</p>  
    </div>  
    <button @click="print">打印</button>  
  </div>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      title: 'Vue 打印示例',  
      description: '这是一个Vue打印功能的简单示例。'  
    }  
  },  
  methods: {  
    print() {  
      let printContents = this.$refs.printSection.innerHTML;  
      let popup = window.open('', '_blank');  
      popup.document.write(printContents);  
      popup.document.close();  
      popup.print();  
    }  
  }  
}  
</script>

以上代码实现了基本的打印功能,但并未实现打印预览功能。实现打印预览功能通常需要使用到第三方库,或者利用浏览器的打印对话框进行预览。

如果你想要实现更复杂的打印和预览功能,我建议查看vue-html2pdf或print.js这样的库。它们提供了更多的配置选项,并且可以更好地控制打印和预览的行为。

例如,使用vue-html2pdf库,你可以很容易地将HTML转换为PDF,并提供下载或预览功能。而print.js库则提供了更多的打印选项,包括打印到PDF,打印到纸张,以及自定义打印布局等。

注意:以上代码在真实环境中可能需要进行一些调整,以适应你的具体需求。例如,你可能需要处理跨域问题,或者调整样式以适应打印布局。