客服、聊天页面嵌入embed

主要代码

给window绑定我们的embed,用于控制嵌入页

/* eslint no-undef: 0 */
/* eslint promise/param-names: 0 */
/* eslint-disable */
//
import iFrameResize from 'iframe-resizer';
import { IOptions } from './models/options';
import {InitOptions} from "./models/InitOptions";
import {PostMessage} from "./models/PostMessage";
import {BtnWidth} from "./models/BtnWidth";
import {EMBED_CARE_RELOAD} from "./models/PostMsgType";

// 随便找一个嵌入页
const IFRAME_URL='https://3oz0g9vvay2n.webertest.top/';
const WEASL_WRAPPER_ID = 'embed-container';
const IFRAME_ID = 'embed-iframe-element';

class Embed {


  private debugMode: boolean;
  private onloadFunc: (b: any) => void;
  private iframe: HTMLIFrameElement | undefined;
  private wrapper: HTMLDivElement | undefined;
  private floatingBtn: HTMLDivElement | undefined;
  private countDiv: HTMLDivElement|undefined;
  private options?: IOptions;
  private wrapper_display: true | false | undefined = false;
  private listeners: { [key: string]: (data: any) => void } = {};
  private is_pc:boolean=true;
  private init_otps: InitOptions ={} as InitOptions;
  private msgCount: number=0;
  private floatBtnWidth: number =40;

  constructor(onloadFunc = function () {}) {
    this.debugMode = false;
    this.onloadFunc = onloadFunc;
  }

  on = (name: string, cb: (data: any) => void) => {
    this.listeners[name] = cb;
  };

  init =async (opts:InitOptions) => {

    const def={
        locale:'zh-CN',
        enable_log:false,
    } as InitOptions;
    this.init_otps=Object.assign(def,opts);
    const that = this;
    this.is_pc=this.isPC();
    const defaultOptions={
        position:{
            bottom:'40px',
            bottom_mobile:'20px',
            right:'20px',
            left:'20px',
            frame_width:'350px',
            btn_width:'60px',
            btn_size:'normal',
            btn_size_mobile:'normal',
            frame_height:'70%',
            side:'left',
        },
        titleList:[
            'Hello, do you need help?',
            'Hi, welcome to contact me if you have any questions?',
            'Welcome to Chat with us.'
        ],
    } as IOptions;
    this.options=defaultOptions;

    this.floatBtnWidth=  this.getFloatBtnWidth();
    this.initFloatingButton();
    this.initializeIframe();
    this.mountIframe();
    window.onmessage=(event: any)=>{
          // 接收嵌入页的事件,并关闭展开嵌入页
    }
    window.onresize=()=>{
       const old=this.is_pc;
       this.is_pc=that.isPC();
       if(old!==this.is_pc){
           this.log('窗口调整',this.is_pc)
           that.calcPosition();
       }
    }
    // 模拟初始化完成后,显示浮动按钮
    setTimeout(function () {
        that.showFloatingBtn();
    },1000)

  };
  log(...data:any[]){
      if(this.init_otps?.enable_log){
          console.log(...data);
      }
  }
  setOpts(opts:InitOptions){
      this.init_otps=Object.assign(this.init_otps,opts);
      if(this.iframe){
          this.setIframeSrc(this.iframe)
      }
      this.log('配置更新','参数->',opts,'新配置->',this.init_otps)
      return true;
  }
  initFloatingButton = () => {
     let that=this;
      const btn = document.createElement('div');
      btn.className = 'embed-floating-btn';
      // @ts-ignore
      const bottom=this.options?.position[this.is_pc?'bottom':'bottom_mobile'];
      (
        btn as any
      ).style = `z-index: ${Number.MAX_SAFE_INTEGER - 10 }; 
      width: ${this.floatBtnWidth}px; 
      height: ${this.floatBtnWidth}px;   
      bottom: ${bottom}; 
      background:#408eff;
      border-radius: 50%;
      cursor: pointer;
      box-shadow: 0 0 20px #777777a3;
      border: 0;
      justify-content: center;
      align-items: center;
      display: none;
      -webkit-tap-highlight-color:rgba(255,255,255,0);
      position: fixed;`;
      // @ts-ignore
      ( btn as any).style[this.getFloatingBtnSide()]=`${this.options?.position[this.getFloatingBtnSide()]}`

      let inner = document.createElement('div');
      (inner as any).style= `
           flex: 1;
           display: flex;
           justify-content: center;
           align-items: center;
           width: 100%; 
           height: 100%;
           -webkit-tap-highlight-color:rgba(255,255,255,0);
           position: relative;`;
      let _style=`border-radius: 50%;height:${this.floatBtnWidth / 6 }px;width:${this.floatBtnWidth / 6 }px;background-color:#FFF;`;
      let ch1 = document.createElement('div');
      (ch1 as any).style=_style;
      let ch2 = document.createElement('div');
      (ch2 as any).style=_style+`margin:0 ${this.floatBtnWidth / 10 }px;`;
      let ch3 = document.createElement('div');
      (ch3 as any).style=_style;
      inner.appendChild(ch1);
      inner.appendChild(ch2);
      inner.appendChild(ch3);
      let countDiv = document.createElement('div');
      (countDiv as any).style=`
      border-radius: 50%;
      background: #fa3c4c;
      color: white;
      z-index: ${Number.MAX_SAFE_INTEGER - 9 };
      display: none;
      position: absolute;
      justify-content: center;
      align-items: center;
      width: ${this.floatBtnWidth / 2.4 }px; 
      height: ${this.floatBtnWidth / 2.4  }px;
      font-size: ${this.floatBtnWidth / 3.75 }px;
      bottom: 0;
      left: 45%;
      `;
      inner.appendChild(countDiv);
      btn.appendChild(inner);
      btn.onclick = ()=>{
          that.toggleWrapper(undefined);
      }

      this.randemTitle(btn);
      btn.onmouseleave=()=>{
          that.randemTitle(btn);
      }
      this.floatingBtn=btn;
      this.countDiv=countDiv;
      document.body.appendChild(btn);
    
  };
  showFloatingBtn(){
      // @ts-ignore
      this.floatingBtn?.style.display='flex';
  }
  randemTitle=(btn: HTMLDivElement) => {
        if (this.options && this.options?.titleList?.length) {
            const _index =parseInt((Math.random() * 1000) % (this.options?.titleList.length)+'');
            btn.title = this.options?.titleList[_index] as string
        }
  };
  initializeIframe= () => {
      let that=this;
      if (!document.getElementById(IFRAME_ID)) {
          const iframe = document.createElement('iframe');
          this.setIframeSrc(iframe);
          iframe.onload = () =>{
                  (iFrameResize as any).iframeResize(
                  {
                    log: false,
                    autoResize: true,
                    onMessage: ({ message }: any) => {  },
                    enablePublicMethods: true, // Enable methods within iframe hosted page
                    heightCalculationMethod: 'max',
                    widthCalculationMethod: 'max',
                    sizeWidth: true,
                  },
                  `#${IFRAME_ID}`
                );

          };
          iframe.id = IFRAME_ID;
          (iframe as any).style=`border:none;width: ${this.isPC()?this.options?.position?.frame_width:'100%'};height: 100%;`;
          (iframe as any).crossorigin = 'anonymous';
          this.iframe = iframe;

      }
  }

  private setIframeSrc(iframe:HTMLIFrameElement) {
        let query = '';
        for (const file in this.init_otps) {
            // @ts-ignore
            let val=this.init_otps[file];
            if(typeof val === 'object'){
                val=escape(JSON.stringify(val));
            }else{
                val=escape(val);
            }
            query += `&${file}=${val}`
        }
        query = query.replace('&', '');
        const url=`${IFRAME_URL}?${query}`;
        this.log('嵌入链接',url);
        if(!iframe.src){
            iframe.src = url;
        }else{
            // 延迟载入页面
            setTimeout(function (){
                iframe?.contentWindow?.postMessage({
                    type:EMBED_CARE_RELOAD,
                    payload:{
                        url,
                    }
                } as PostMessage,IFRAME_URL);
            },1000)

        }
    }

  mountIframe = () => {
    let that=this;
    if (!document.getElementById(IFRAME_ID) && this.iframe) {
      // window.addEventListener('message', this.receiveMessage, false);

      const wrapper = document.createElement('div');

      wrapper.className = 'wrapper-embed-widget';
      // wrapper.style.display = 'none';
      wrapper.id = WEASL_WRAPPER_ID;
      (wrapper as any ).style = `
      z-index: ${Number.MAX_SAFE_INTEGER - 9 };
      width: 0; 
      height: 0;   
      opacity: 0;    
      box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
      overflow: hidden;
      background-color:#FFFFFF;
      -webkit-transition: width 0.3s, height 0.3s, opacity 0.3s, visibility 0.3s; 
      transition: width 0.3s, height 0.3s, opacity 0.3s, visibility 0.3s;
      position: fixed;`;


      wrapper.appendChild(this.iframe);
      this.wrapper=wrapper;
      document.body.appendChild(wrapper);
      this.calcPosition();
    }
  };
  calcPosition(){
      if(this.is_pc){
          if(this.wrapper){
              (this.wrapper as any ).style.bottom=`calc( ${this.options?.position?.bottom} + 55px)`;
              // @ts-ignore
              (this.wrapper as any ).style[this.getFloatingBtnSide()]=this.options?.position[this.getFloatingBtnSide()];
              (this.wrapper as any ).style.borderRadius='8px';
          }

      }else{
          if(this.wrapper){
              (this.wrapper as any ).style.bottom=0;
              (this.wrapper as any ).style[this.getFloatingBtnSide()]=0;
              (this.wrapper as any ).style.borderRadius='0px';
          }

      }
      this.toggleWrapper(this.wrapper_display);
  };

  private getFloatingBtnSide() {
        return this.options?.position?.side || 'right';
    }

  toggleWrapper(display:true|false|undefined){
    let that=this;
    let _display=this.wrapper_display;
    let wrapper=this.wrapper as any;

    if(display===undefined){
      _display = !_display;
    }else{
      _display = display;
    }
    if(_display===false){
      wrapper.style.opacity='0';
      wrapper.style.height='0';
      wrapper.style.width='0';
    }else{
      wrapper.style.opacity='1';
      if(this.is_pc){
          wrapper.style.height=`${this.options?.position?.frame_height}`;
          wrapper.style.width=`${this.options?.position?.frame_width}`;
      }else{
          wrapper.style.height='100%';
          wrapper.style.width='100%';
      }
    }
    this.wrapper_display=_display;

  }
  isPC=() => {
        const userAgentInfo = navigator.userAgent;
        const Agents = ["Android", "iPhone",
            "SymbianOS", "Windows Phone",
            "iPad", "iPod"];
        let flag = true;
        for (let v = 0; v < Agents.length; v++) {
            if (userAgentInfo.indexOf(Agents[v]) > 0) {
                flag = false;
                break;
            }
        }
        if(flag===true&&window.innerWidth<400){
            flag=false;
        }
        return flag;
  };

  private getFloatBtnWidth() {

        const ispc=this.isPC();
        let dev=ispc?'pc':'mobile';
        let btn_size=ispc?'btn_size':'btn_size_mobile';
        this.log('判断设备类型',dev,btn_size);
        // @ts-ignore
        const size=BtnWidth[dev][this.options?.position[btn_size]]
        return size;
    }
}


export default ((window: any) => {
  const onloadFunc =
    window.embed && window.embed.onload && typeof window.embed.onload === 'function' ? window.embed.onload : function () {};

  const initCall = window.embed._c.find((call: string[]) => call[0] === 'init');
  const embedApi: any = () => {};
  const embed = new Embed(onloadFunc);

  embedApi.init = embed.init;
  embedApi.on = embed.on;


  if (initCall) {
    // eslint-disable-next-line prefer-spread
    embedApi[initCall[0]].apply(embedApi, initCall[1]);
    const onCall = window.embed._c.find((call: string[]) => call[0] === 'on');
    if (onCall) {
      embedApi[onCall[0]].apply(embedApi, onCall[1]);
    }
  } else {
          // eslint-disable-next-line no-param-reassign
    (window as any).embed.init = embed.init;

    // eslint-disable-next-line no-param-reassign
    (window as any).embed.on = embed.on;

  }
  (window as any).embed.setOpts = embed.setOpts.bind(embed);
})(window);





package.json

build 构建目标脚本,start:docker用于本地测试

{
  "name": "care_embed",
  "version": "1.0.0",
  "description": "客服嵌入脚本",
  "main": "dist/embed.umd.min.js",
  "module": "dist/embed.es5.min.js",
  "typings": "dist/types/embed.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc && cross-env ENVIRONMENT=local rollup -c rollup.config.ts",
    "start:docker": "pnpm build && http-server -p 4701 ."
  },
  "author": "",
  "license": "ISC",
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint"
    ],
    "{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [
      "prettier --ignore-path .eslintignore --parser json --write"
    ],
    "*.{html,md,yml}": [
      "prettier --ignore-path .eslintignore --single-quote --write"
    ]
  },
  "config": {
    "commitizen": {
      "path": "node_modules/cz-conventional-changelog"
    }
  },
  "jest": {
    "transform": {
      ".(ts|tsx)": "ts-jest"
    },
    "testEnvironment": "node",
    "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js"
    ],
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/test/"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 90,
        "functions": 95,
        "lines": 95,
        "statements": 95
      }
    },
    "collectCoverageFrom": [
      "src/*.{js,ts}"
    ]
  },
  "devDependencies": {
    "@commitlint/cli": "^7.1.2",
    "@commitlint/config-conventional": "^7.1.2",
    "@novu/notification-center": "^0.7.1",
    "@rollup/plugin-replace": "^2.4.2",
    "@types/jest": "27.4.0",
    "@types/node": "^14.14.16",
    "colors": "1.4.0",
    "commitizen": "^3.0.0",
    "concurrently": "^5.3.0",
    "coveralls": "^3.0.2",
    "cross-env": "^5.2.0",
    "cz-conventional-changelog": "^2.1.0",
    "http-server": "^0.12.3",
    "husky": "^1.0.1",
    "jest": "^27.0.6",
    "jest-config": "^27.4.7",
    "lint-staged": "^8.0.0",
    "lodash.camelcase": "^4.3.0",
    "prettier": "^1.14.3",
    "prompt": "^1.0.0",
    "replace-in-file": "^3.4.2",
    "rimraf": "^2.6.2",
    "rollup": "^0.67.0",
    "rollup-plugin-commonjs": "^9.1.8",
    "rollup-plugin-json": "^3.1.0",
    "rollup-plugin-node-resolve": "^3.4.0",
    "rollup-plugin-sourcemaps": "^0.4.2",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-typescript2": "^0.18.0",
    "rollup-plugin-uglify": "^6.0.4",
    "semantic-release": "^19.0.3",
    "shelljs": "^0.8.3",
    "travis-deploy-once": "^5.0.9",
    "ts-jest": "^27.1.3",
    "ts-node": "^7.0.1",
    "tslib": "^2.3.1",
    "typescript": "4.1.3"
  },
  "dependencies": {
    "@types/iframe-resizer": "^3.5.8",
    "iframe-resizer": "^4.3.1"
  }
}


rollup.config.ts文件

配置构建工具rollup

import builtins from 'rollup-plugin-node-builtins';
import globals from 'rollup-plugin-node-globals';
import resolve from 'rollup-plugin-node-resolve';
import { uglify }  from 'rollup-plugin-uglify';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';

import {terser} from 'rollup-plugin-terser';
import camelCase from 'lodash.camelcase';
import typescript from 'rollup-plugin-typescript2';
import json from 'rollup-plugin-json';
import replace from '@rollup/plugin-replace';

const pkg = require('./package.json');


const libraryName = 'embed';

export default {
  input: `src/${libraryName}.ts`,
  output: [
    {
      file: pkg.main,
      name: camelCase(libraryName),
      format: 'iife',
      sourcemap: false,
      plugins: [],
    },
    {
      file: pkg.module,
      format: 'es',
      sourcemap: false,
      plugins: [],
    },
  ],
  // Indicate here external modules you don't want to include in your bundle (i.e.: 'lodash')
  external: [],
  watch: {
    include: 'src/**',
  },
  plugins: [
    replace({
      preventAssignment: true,
      values: {
        'process.env.ENVIRONMENT': JSON.stringify(process.env.ENVIRONMENT),
        'process.env.WIDGET_URL': JSON.stringify(process.env.WIDGET_URL),
      },
    }),
    // Allow json resolution
    json(),
    // Compile TypeScript files
    typescript({ useTsconfigDeclarationDir: true }),
    // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
    commonjs({ extensions: ['.js', '.ts'] }),

    /*
     * Allow node_modules resolution, so you can use 'external' to control
     * which external modules to include in the bundle
     * https://github.com/rollup/rollup-plugin-node-resolve#usage
     */
    resolve(),

    // Resolve source maps to the original source
    sourceMaps(),
    // uglify(),
    globals(),
    builtins(),
    terser(),
  ],
};

在主页面放置嵌入脚本


<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>嵌入测试</title>
<script>
  (function(n,o,t,i,f) {
    n[i] = {}; var m = ['init','setOpts']; n[i]._c = [];m.forEach(me => n[i][me] = function() {n[i]._c.push([me, arguments])});
    var elt = o.createElement(f); elt.type = "text/javascript"; elt.async = true; elt.src = t;
    var before = o.getElementsByTagName(f)[0]; before.parentNode.insertBefore(elt, before);
  })(window, document, 'http://127.0.0.1:4701/dist/embed.umd.min.js', 'embed', 'script');


</script>
<script>
    embed.init({
      locale:'zh_HK',
      enable_log:true,

  });
  // setTimeout(function () {
  //    const rs=tkcare.setOpts({locale:'en'});
  //     console.log(rs)
  // },3000);
</script>
</head>

<body style="background-color: azure">

</body>
</html>

显示效果

在这里插入图片描述
展开的情形
在这里插入图片描述
移动端自动全屏
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值