Skip to content

功能

加载在线字体文件

javascript
function loadFont() {
  let url = "https://resource.hellofont.cn/fonts/exhibition2/7240.ttf";
  let font = "f-exhibition2-7240";
  let prefont = new FontFace(font, `url(${url})`);
  prefont.load().then(function (loaded_face) {
    document.fonts.add(loaded_face);
    document.body.style.fontFamily = document.body.style.fontFamily + font;
  });
}

下载一个链接

javascript
function download(link, name) {
  if (!name) {
    name = link.slice(link.lastIndexOf("/") + 1);
  }
  let eleLink = document.createElement("a");
  eleLink.download = name;
  eleLink.style.display = "none";
  eleLink.href = link;
  document.body.appendChild(eleLink);
  eleLink.click();
  document.body.removeChild(eleLink);
}

download("http://111.229.14.189/file/1.xlsx");

设置手机桌面图标

html
<link rel="apple-touch-icon" href="<%= BASE_URL %>logo.png" />
<link rel="apple-touch-icon-precomposed" href="<%= BASE_URL %>logo.png" />
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>logo.png" />
<link rel="shortcut icon" type="image/ico" href="<%= BASE_URL %>logo.png" />

遮罩时,禁止背景滚动

html
<div class="mask" @touchumove.self.prevent></div>

二三倍图 scss 方法

scss
.bg-image($url) {
  background: ~"url(${url}@2x.png)" no-repeat center;
  background-size: 100%;
  @media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) {
    background: ~"url(${url}@3x.png)" no-repeat center;
    background-size: 100%;
  }
}

vue 国际化 csv 转 i18n

i18n 官网

shell
yarn add vue-i18n@^7.8.0
javascript
// main.js
import i18n from "./i18n";

new Vue({
  i18n,
  render: (h) => h(App),
}).$mount("#app");
javascript
// i18n.js
import Vue from "vue";
import VueI18n from "vue-i18n";
import axios from "axios";
Vue.use(VueI18n);
const i18n = new VueI18n({
  locale: localStorage.getItem("locale") || "zh-cn", //设置默认语言
});

i18n.setI18nLanguage = (lang) => {
  i18n.locale = lang;
  document.querySelector("html").setAttribute("lang", lang); //设置html的语言
  localStorage.setItem("locale", lang);
};
class Translate {
  constructor() {
    this.messages = {};
    this.loadedLanguages = [];
  }
  parseSheet(data) {
    data.forEach((row, rowIndex) => {
      let key;
      row.forEach((col, colIndex) => {
        // 第一行,文件名
        if (rowIndex === 0 && colIndex !== 0) {
          if (!(col in this.messages)) {
            this.messages[col] = {};
            this.loadedLanguages.push(col);
          }
        }

        if (rowIndex >= 1) {
          if (colIndex === 0) {
            key = col;
          } else {
            const lang = this.loadedLanguages[colIndex - 1];
            this.messages[lang][key] = col;
          }
        }
      });
    });

    return this;
  }
}

axios.get("/language.csv").then(({ data }) => {
  i18n.setI18nLanguage(i18n.locale);
  let sheets = CSVToArray(data);
  let translate = new Translate().parseSheet(sheets);
  for (const key in translate.messages) {
    if (Object.hasOwnProperty.call(translate.messages, key)) {
      const message = translate.messages[key];
      i18n.setLocaleMessage(key, message);
    }
  }
});

function CSVToArray(strData, strDelimiter) {
  strDelimiter = strDelimiter || ",";
  let objPattern = new RegExp(
    "(\\" +
      strDelimiter +
      "|\\r?\\n|\\r|^)" +
      '(?:"([^"]*(?:""[^"]*)*)"|' +
      '([^"\\' +
      strDelimiter +
      "\\r\\n]*))",
    "gi"
  );

  let arrData = [[]];
  let arrMatches = null;
  let strMatchedValue = "";
  while ((arrMatches = objPattern.exec(strData))) {
    let strMatchedDelimiter = arrMatches[1];
    if (strMatchedDelimiter.length && strMatchedDelimiter != strDelimiter) {
      arrData.push([]);
    }

    if (arrMatches[2]) {
      strMatchedValue = arrMatches[2].replace(new RegExp('""', "g"), '"');
    } else {
      strMatchedValue = arrMatches[3];
    }
    arrData[arrData.length - 1].push(strMatchedValue);
  }

  return arrData;
}

export default i18n;
vue
<template>
  <div>
    {{ $t("apple") }}
  </div>
</template>

<script>
export default {
  methods: {
    // 切换语言
    handleChange(lang) {
      this.$i18n.setI18nLanguage(lang);
    },
  },
};
</script>

vue 生成当日打包次数版本号

javascript
// vue.config.js
const fs = require("fs");
const IS_PROD = process.env.NODE_ENV === "production";
process.env.VUE_APP_VERSION = require("./package.json").version;
if (IS_PROD) {
  const resetTodayBuildCount = (root) => {
    return {
      // 获取配置信息的日期和构建次数
      getSettingData(root, fileName) {
        let rgx = [/const TODAY_BUILD_COUNT = \d*/, /const DATE = \d*/];
        return new Promise((resolve) => {
          // 读取文件
          fs.readFile(root, "utf-8", (emarr, data) => {
            let todayBuildCount = data.match(rgx[0])[0];
            let date = data.match(rgx[1])[0];
            resolve({
              content: data,
              data: {
                todayBuildCount,
                date,
              },
            });
          });
        });
      },
      async run() {
        /**
         * 1.获取配置信息的日期和构建次数
         * 2.判断当前日期是否小于写入的日期
         * 3.将配置信息的内容替换
         */
        let { content, data } = await this.getSettingData(root);
        const date = +data.date.split("=")[1];
        const count = +data.todayBuildCount.split("=")[1];

        const TODAY_FORMATE = +getFormatDate({
          date: new Date(),
          pattern: 4,
        });
        const todayBuildCount =
          new Date(TODAY_FORMATE) > new Date(date) ? 1 : count + 1;

        const newTodayBuildCount = `const TODAY_BUILD_COUNT = ${todayBuildCount}`;
        const newDate = `const DATE = ${TODAY_FORMATE}`;
        content = content
          .replace(data.todayBuildCount, newTodayBuildCount)
          .replace(data.date, newDate);
        // 写入文件
        fs.writeFile(root, content, (err) => {
          console.log(err);
        });
      },
    };
  };
  resetTodayBuildCount("./src/setting.js").run();
}
// src/setting.js
const VERSION_FORMATE = process.env.VUE_APP_VERSION;
const DATE = 230117;
const TODAY_BUILD_COUNT = 1;
export const VERSION = `V ${VERSION_FORMATE} ${DATE}-${TODAY_BUILD_COUNT}`; // 版本号

vue 全局方法的两种方式

javascript
// 第一种
// main.js 把方法挂到vue实例上(不推荐)会造成vue实例过大,业务逻辑和框架逻辑混在一起
import api from './api'
Vue.prototype.$api = api
// 第二种
// vue.config.js (推荐)
const webpack = require('webpack')
module.exports = {
    configureWebpack: {
        plugins: [
            new webpack.ProvidePlugin({
                UTILS: [path.resolve(__dirname, './src/utils/Utils.js'), 'default'], // 定义的全局函数类
                TOAST: [path.resolve(__dirname, './src/utils/Toast.js'), 'default'], // 定义的全局Toast弹框方法
                LOADING: [path.resolve(__dirname, './src/utils/Loading.js'), 'default'] // 定义的全局Loading方法
                API: [path.resolve(__dirname, './src/apis/apis.js'), 'default'] // 定义的全局API方法
            })
        ]
    },
}
// eslintrc.js
module.exports = {
    "globals": {
        "UTILS": true,
        "TOAST": true,
        "LOADING": true
    }
    // ...省略N行ESlint的配置
}

vue 拖拽悬浮按钮

javascript
// main.js
import "@/directive/drag";
javascript
// directive/drag/index.js
import Vue from "vue";
// 拖拽结束自动靠边停方法
import autoStop from "./methods";
// 保存初始状态的变量
let startX, startY, firstTime, lastTime, isApp;
// 拖拽开始事件
const dragStart = (event, el) => {
  firstTime = new Date().getTime();
  // 1、如果元素上存在自定义的私有属性__VueDragTimer__,则清空该定时器
  // 2、__VueDragTimer__:自定义定时器属性,首尾双下划线挂载于元素上,方便在解绑指令时(unbind)清除定时器,并可删除属性
  if (el.__VueDragTimer__) clearInterval(el.__VueDragTimer__);
  // 通过touch属性计算获取初始位置
  const { pageX, pageY } = isApp ? event.touches[0] : event;
  const {
    // 元素距离限制区域上左距离,此变量需要实时获取
    offsetTop: eT,
    offsetLeft: eL,
  } = el;
  startX = parseInt(pageX - eL);
  startY = parseInt(pageY - eT);
};
// 拖动事件
const dragMove = (event, el, pW, pH, pT, pL, eW, eH) => {
  // 添加阻止默认事件,防止影响到元素点击事件
  event.preventDefault();
  // 获取touch事件,计算并实时改变元素位置(拖拽效果)
  const { pageX, pageY } = isApp ? event.touches[0] : event;
  let movePageX = parseInt(pageX - startX);
  let movePageY = parseInt(pageY - startY);
  // 超出限制区域,禁止越界
  if (movePageX <= pL) {
    movePageX = pL;
  } else if (movePageX >= pW - eW + pL) {
    movePageX = pW - eW + pL;
  }
  if (movePageY <= pT) {
    movePageY = pT;
  } else if (movePageY >= pH - eH + pT) {
    movePageY = pH - eH + pT;
  }
  el.style.left = movePageX + "px";
  el.style.top = movePageY + "px";
};
// 拖拽结束,自动停靠
const dragStop = (event, el) => {
  lastTime = new Date().getTime();
  autoStop(el);
  if (!isApp) {
    document.onmousemove = document.onmouseup = null;
    // 用时间差判断是拖拽还是点击
    el.setAttribute("data-flag", lastTime - firstTime > 200);
  }
};

Vue.directive("drag", {
  inserted(el) {
    Vue.nextTick(() => {
      const {
        // 存在定位的父级元素及其宽高(即元素可移动限制区域,先假设为parentNode,不是相对父级定位可以再调整)
        parentNode: {
          clientWidth: pW,
          clientHeight: pH,
          offsetTop: pT,
          offsetLeft: pL,
          offsetWidth: xW,
        },
        // 元素计算宽高(如果停靠不想完全靠边,可给元素添加对应padding)
        offsetWidth: eW,
        offsetHeight: eH,
        dataset: { app },
      } = el;

      isApp = app === "true";

      if (!isApp) {
        el.style.position = "absolute";
        el.style.cursor = "move";
      }

      if (isApp) {
        el.addEventListener("touchstart", (event) => dragStart(event, el));
        el.addEventListener("touchmove", (event) =>
          dragMove(event, el, pW, pH, pT, pL, eW, eH)
        );
        el.addEventListener("touchend", (event) => dragStop(event, el));
      } else {
        el.onmousedown = (event) => {
          dragStart(event, el);
          document.onmousemove = (event) =>
            dragMove(event, el, pW, pH, pT, pL, eW, eH);
          document.onmouseup = (event) => dragStop(event, el);
        };
      }
    });
  },
});
javascript
// directive/drag/methods.js
// 元素移动方法
const autoMove = function (el, changeAttr, endValue) {
  // 定义 起点、运动中当前位置 变量
  let currentValue;
  const {
    // 元素距离限制区域上左距离
    offsetTop: eT,
    offsetLeft: eL,
  } = el;
  // 改变属性为 'left' 时,起点赋值为:元素与限制区左边的距离
  if (changeAttr === "left") currentValue = eL;
  // 改变属性为 'top' 时,起点赋值为:元素与限制区上边的距离
  if (changeAttr === "top") currentValue = eT;
  // 移动步距设置为 终点与起点 距离的 1 / 33,值的正负表示移动方向
  const step = (endValue - currentValue) / 33;
  // 定时器存在则清除处理
  if (el.__VueDragTimer__) clearInterval(el.__VueDragTimer__);
  // 设置自动停靠定时器
  el.__VueDragTimer__ = setInterval(() => {
    // 若当前位置与终点差值 已经 小于步距,则直接定位到终点位置(js计算精度导致此判断不可少),否则缩小步距的距离
    if (Math.abs(endValue - currentValue) < Math.abs(step)) {
      el.style[changeAttr] = endValue + "px";
      clearInterval(el.__VueDragTimer__);
      delete el.__VueDragTimer__;
    } else {
      currentValue += step;
      el.style[changeAttr] = currentValue + "px";
    }
  }, 5);
};

/**
 * 自动停靠方法:
 * 停靠原则
 * 1、默认左右停靠开关永久开启
 * 2、上下停靠开关在距离上下<=50像素时开启
 * 3、最终移动方向:从停靠开关开启的方向中,取需要移动距离最小的方向
 */
const autoStop = function (el) {
  const {
    // 存在定位的父级元素及其宽高(即元素可移动限制区域,先假设为parentNode,不是相对父级定位可以再调整)
    parentNode: {
      clientWidth: pW,
      clientHeight: pH,
      offsetTop: pT,
      offsetLeft: pL,
    },
    // 元素距离限制区域上左距离
    offsetTop: eT,
    offsetLeft: eL,
    // 元素计算宽高(如果停靠不想完全靠边,可给元素添加对应padding)
    offsetWidth: eW,
    offsetHeight: eH,
  } = el;
  /**
   * 停靠配置
   * changeAttr: 当前配置方案改变的元素属性 'top'| 'left'
   * endValue: 停靠动画结束时,需要改变的属性的值(终点)
   * distance:当前配置,距离目标结束位置的距离
   * toggle: 配置开启开关
   */
  const stopConfigs = [
    {
      // 上移贴边配置
      name: "top",
      changeAttr: "top",
      endValue: pT,
      distance: eT - pT,
      toggle: eT <= 50 + pT,
    },
    {
      // 下移贴边配置
      name: "bottom",
      changeAttr: "top",
      endValue: pH + pT - eH,
      distance: pH - eT - eH + pT,
      toggle: pH - eT - eH <= 50 - pT,
    },
    {
      // 左移贴边配置
      name: "left",
      changeAttr: "left",
      endValue: pL,
      distance: eL - pL,
      toggle: true,
    },
    {
      // 右移贴边配置
      name: "right",
      changeAttr: "left",
      endValue: pW + pL - eW,
      distance: pW - eL - eW + pL,
      toggle: true,
    },
  ];
  // 先重配置中选出开关为true的,然后按distance排序后选出其值最小的配置,并获取对应的changeAttr、endValue
  const { changeAttr, endValue } = stopConfigs
    .filter((o) => o.toggle)
    .sort((a, b) => a.distance - b.distance)[0];
  autoMove(el, changeAttr, endValue);
};

export default autoStop;
vue
<!-- 组件内使用 -->
<template>
  <div class="panel-center" v-if="state">
    <div class="wrap">
      <div
        class="box-darg"
        v-drag
        @click="say"
        ref="dragbtn"
        @mousedown="isShowIframeDiv = true"
        @mouseup="isShowIframeDiv = false"
        :data-app="`${isApp}`"
      ></div>
      <!-- iframeDiv解决拖拽时卡顿 -->
      <div>
        <div class="iframeDiv" v-show="isShowIframeDiv"></div>
        <iframe
          id="game_iframe"
          src="https://chencan123.gitee.io/"
          style="width: 100%; height: 100vh"
          frameborder="0"
        ></iframe>
      </div>
    </div>
  </div>
</template>

<script>
/***
 * 1.解决iframe拖拽时卡顿
 * 2.解决拖拽时触发点击事件
 * 3.兼容PC和APP模式
 * 4.拖拽新增容器居中逻辑(调整限制边界和吸附边界)
 */
export default {
  data() {
    return {
      state: false,
      isApp: true,
      isShowIframeDiv: false,
    };
  },
  methods: {
    show() {
      this.state = true;
    },
    hide() {
      this.state = false;
    },
    say() {
      const isClick = this.$refs.dragbtn.getAttribute("data-flag");
      isClick !== "true" && console.log("hello");
    },
  },
};
</script>

<style lang="scss" scoped>
.panel-center {
  align-items: center;
  display: flex;
  height: 100%;
  justify-content: center;
  left: 0;
  position: fixed;
  top: 0;
  transition: 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), z-index 1ms;
  width: 100%;
}

.wrap {
  transform-origin: center center;
  max-width: 600px;
  width: 100%;
  overflow-y: auto;
}

$num: 36px;

.box-darg {
  width: $num;
  height: $num;
  position: absolute;
  display: flex;
  padding: 10px;
  box-sizing: content-box;
  z-index: 99999;
  background: blue;
}
.iframeDiv {
  width: 100%;
  height: 100%;
  position: absolute;
}
</style>

vue 雪碧图

vue
<!-- 指定编号默认和激活情况 -->
<template>
  <div :style="bgPosition(1, 50)"></div>
</template>

<script>
export default {
  data: () => ({
    userSprite: {
      1: {
        default: [152, 486],
        active: [302, 482],
        sid: 1,
      },
      2: {
        default: [152, 636],
        active: [606, 152],
        sid: 2,
      },
    },
    sid: 2,
  }),
  methods: {
    /**
     * @param sid  指定图片id
     * @param iconWidth icon宽度
     * @param iconHeight icon高度
     * @example bgPosition(1, 50)
     */
    bgPosition(sid, iconWidth = 146, iconHeight = iconWidth) {
      let [spriteIconX, spriteIconY] =
        this.sid == sid
          ? this.userSprite[sid].active
          : this.userSprite[sid].default;
      let spriteWidth = 904;
      let spriteHeight = 900;
      let spriteIconWidth = 146;
      let spriteIconHeight = 146;
      return {
        width: `${iconWidth}px`,
        height: `${iconHeight}px`,
        background: `url(${require("@/assets/user.png")}) no-repeat`,
        "background-position": `${
          (spriteIconX / (spriteWidth - spriteIconWidth)) * 100
        }% ${(spriteIconY / (spriteHeight - spriteIconHeight)) * 100}%`,
        "background-size": `${spriteWidth * (iconWidth / spriteIconWidth)}px  ${
          spriteHeight * (iconHeight / spriteIconHeight)
        }px`,
      };
    },
  },
};
</script>
javascript
// loadSprite.js
/**
 * @param module  雪碧图文件夹下的图片名称
 */
export default function (module) {
  const map = require(`/public/sprite/${module}`);
  return {
    //获取icon背景
    /**
     * @module map中的模块名【文件名】
     * @param name  需要显示的图片名称
     * @param iconWidth icon宽度
     * @param iconHeight icon高度
     * @example loadSprite('dots').get('free_email', 49, 47)
     */

    get(name, iconWidth, iconHeight) {
      const {
        x: spriteIconX,
        y: spriteIconY,
        w: spriteIconWidth,
        h: spriteIconHeight,
        width: spriteWidth,
        height: spriteHeight,
      } = map[name];
      return {
        width: `${spriteIconWidth * (iconWidth / spriteIconWidth)}px`,
        height: `${spriteIconHeight * (iconHeight / spriteIconHeight)}px`,
        background: `url(${require(`/public/sprite/${module}.png`)}) no-repeat`,
        "background-position": `${
          (spriteIconX / (spriteWidth - spriteIconWidth)) * 100
        }% ${(spriteIconY / (spriteHeight - spriteIconHeight)) * 100}%`,
        "background-size": `${spriteWidth * (iconWidth / spriteIconWidth)}px  ${
          spriteHeight * (iconHeight / spriteIconHeight)
        }px`,
      };
    },
  };
}

分页

vue
<template>
  <view class="content">
    <u-sticky>
      <u-tabs-swiper
        ref="uTabs"
        class="u-border-bottom"
        :list="tabsList"
        :current="tabsCurrent"
        :is-scroll="false"
        :active-color="$u.color['error']"
        bar-width="150"
        bar-height="4"
        @change="$u.throttle(tabsChange, 1000)"
      ></u-tabs-swiper>
    </u-sticky>

    <u-empty
      v-show="list.length === 0 && isComplete === true"
      mode="list"
    ></u-empty>

    <u-cell-group
      v-show="list.length > 0 && isComplete === true"
      :border="false"
    >
      <u-cell-item
        v-for="item in list"
        :key="item.id"
        :title="item.title"
        :arrow="false"
        hover-class="none"
      ></u-cell-item>
      <u-loadmore :status="status" style="line-height: 100rpx"></u-loadmore>
    </u-cell-group>
  </view>
</template>

<script>
export default {
  data() {
    return {
      title: "Hello",
      page: 0,
      list: [],
      status: "loadmore",
      isComplete: false,
      // tabs
      tabsCurrent: 0,
      tabsList: [
        {
          name: "商品列表",
        },
        {
          name: "商家信息",
        },
        {
          name: "会员评价",
        },
      ],
    };
  },
  onReachBottom() {
    this.loadData();
  },
  onLoad(options) {
    this.loadData();
  },
  methods: {
    tabsChange(index) {
      this.$u.http.requestTask.abort(); //tab切换时取消上次接口请求
      this.list = [];
      this.page = 0;
      this.tabsCurrent = index;
      this.loadData();
    },
    loadData() {
      if (this.status == "nomore") return;
      this.status = "loading";
      this.$u.api
        .goods({ page: ++this.page, keywords: "水" })
        .then((res) => {
          this.list = [this.list, ...(res.data.list || [])];
          this.status = res.data.list?.length
            ? res.data.list?.length < 10
              ? "nomore"
              : "loadmore"
            : "nomore";
        })
        .catch((err) => {
          //
        })
        .finally(() => {
          this.isComplete = true;
        });
    },
  },
};
</script>

<style>
.content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.logo {
  width: 200rpx;
  height: 200rpx;
  margin: 200rpx auto 50rpx auto;
}

.text-area {
  display: flex;
  justify-content: center;
}

.title {
  font-size: 36rpx;
  color: #8f8f94;
}
</style>

基于 MIT 许可发布