icodingicoding
主页
JavaScript
Vue
React
TypeScript
Node
bug
笔记
时间线
主页
JavaScript
Vue
React
TypeScript
Node
bug
笔记
时间线
  • Markdown 语法
  • 插件
  • Vue
  • 组件设计
  • Element-Ui
  • WebSocket
  • CSS
  • Uniapp
  • 进阶
  • 扫码枪
  • Nginx
  • Nuxt.js
  • Vue 时钟
  • Learning
  • Linux
  • 打包优化
  • 大屏可视化
  • Jenkins
  • SVN
  • JsDocs
  • 代码规范

Element-Ui

el-table 自动滚动

  • setTimeout 实现滚动
<template>
  <div class="rtc-table" @mouseenter="stopScroll" @mouseleave="startScroll">
    <el-table
      ref="rollTable"
      :data="tableData"
      style="width: 100%"
      v-bind="tableConfig"
    >
      <template slot="empty"> 暂无数据 </template>
      <el-table-column
        v-if="showIndex"
        type="index"
        :index="indexMethod"
        label="序号"
        align="center"
      >
      </el-table-column>
      <el-table-column
        v-for="item in propsList"
        :key="`${item.prop}_${item.label}`"
        :prop="item.prop"
        :label="item.label"
        :width="item.width"
        :align="item.align || 'center'"
        v-bind="item.otherConfig"
      >
        <template v-if="item.child">
          <el-table-column
            v-for="sitem in item.child"
            :key="`${sitem.prop}_${sitem.label}`"
            :prop="sitem.prop"
            :label="sitem.label"
            :width="sitem.width"
            :align="sitem.align || 'center'"
            v-bind="sitem.otherConfig"
          >
            <template slot-scope="scope">
              <slot :name="sitem.slotName" :scope="scope">{{
                scope.row[sitem.prop]
              }}</slot>
            </template>
          </el-table-column>
        </template>
        <template slot-scope="scope">
          <slot :name="item.slotName" :scope="scope">{{
            scope.row[item.prop]
          }}</slot>
        </template>
        <template #[item.tableSlotName]="scope">
          <slot :name="item.tableSlotName" :scope="scope">{{
            scope.column.label
          }}</slot>
        </template>
      </el-table-column>
    </el-table>
    <div v-if="showPagination" class="table-bottom mt-20">
      <slot name="table-bottom"></slot>
      <div class="pagination" style="text-align: right">
        <el-pagination
          background
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="pagingInfo.pageIndex"
          :page-sizes="[10, 20, 50]"
          :page-size="pagingInfo.pageSize"
          :total="pagingInfo.total"
          v-bind="pagingConfig"
        >
        </el-pagination>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "RtcTable",
  props: {
    tableData: {
      type: Array,
      default: () => [],
    },
    tableConfig: {
      type: Object,
      default: () => {
        return {};
      },
    },
    propsList: {
      type: Array,
      default: () => [],
      required: true,
    },
    showIndex: {
      type: Boolean,
      default: false,
    },
    showPagination: {
      type: Boolean,
      default: false,
    },
    pagingInfo: {
      type: Object,
      default: () => ({
        pageIndex: 1,
        pageSize: 10,
        total: 0,
      }),
    },
    pagingConfig: {
      type: Object,
      default: () => ({
        layout: "total, sizes, prev, pager, next, jumper",
      }),
    },
    scroll: {
      type: Boolean,
      default: false,
    },
    handle: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      scrollT: 0,
      scrollTime: null,
    };
  },
  computed: {
    isAutoScroll() {
      // 是否自动滚动
      return this.scroll && !this.handle;
    },
    canScroll() {
      // 是否可以滚动
      return (
        this.isAutoScroll &&
        Array.isArray(this.tableData) &&
        this.tableData.length > 0
      );
    },
  },
  watch: {
    canScroll(nv) {
      if (nv) {
        console.log("canScroll");
        this.toScroll();
      }
    },
  },
  methods: {
    indexMethod(index) {
      return index + 1;
    },
    handleSizeChange(val) {
      this.$emit("update:pagingInfo", {
        ...this.pagingInfo,
        pageSize: val,
        pageIndex: 1,
      });
      this.$emit("pagingChange", {
        ...this.pagingInfo,
        pageSize: val,
        pageIndex: 1,
      });
    },
    handleCurrentChange(val) {
      this.$emit("update:pagingInfo", { ...this.pagingInfo, pageIndex: val });
      this.$emit("pagingChange", { ...this.pagingInfo, pageIndex: val });
    },
    toScroll() {
      this.stopScroll();
      if (!this.tableData.length || !this.scroll || !this.$refs.rollTable) {
        // if (!this.$refs.rollTable) {
        //   this.$emit("end");
        // }
        this.$emit("notScroll");
        return;
      }
      let divData = this.$refs.rollTable.bodyWrapper;
      if (this.handle && divData.clientHeight >= divData.scrollHeight) {
        this.$emit("notScroll");
        return;
      }
      let initTop = 0;
      if (this.scrollT < 0) {
        initTop = this.scrollT * -1;
      }
      // this.scrollTime = setInterval(() => {
      //   divData.scrollTop += 1;
      //   initTop++;
      //   if (initTop >= divData.offsetTop / 2) {
      //     initTop = 0;
      //   }
      //   divData.style.Top = "-" + initTop + "px";
      //   if (divData.clientHeight + divData.scrollTop == divData.scrollHeight) {
      //     divData.scrollTop = 0;
      //     this.$emit("end");
      //   }
      // }, 100);
      const scrollFn = () => {
        divData.scrollTop += 1;
        initTop++;
        if (initTop >= divData.offsetTop / 2) {
          initTop = 0;
        }
        divData.style.Top = "-" + initTop + "px";
        if (divData.clientHeight + divData.scrollTop == divData.scrollHeight) {
          divData.scrollTop = 0;
          this.$emit("end");
        }

        this.$nextTick(() => {
          this.scrollTime = setTimeout(scrollFn, 100);
        });
      };
      scrollFn();
    },
    stopScroll() {
      this.scrollTime && clearTimeout(this.scrollTime);
    },
    startScroll() {
      if (!this.scroll || !this.$refs.rollTable) return;
      let target = this.$refs.rollTable.bodyWrapper;
      this.scrollT = target.offsetTop;
      this.toScroll();
    },
    autoScroll() {
      if (this.scroll && !this.handle) this.toScroll();
    },
  },
  beforeDestroy() {
    this.stopScroll();
  },
};
</script>

<style lang="scss" scoped>
.rtc-table {
  .table-bottom {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0;
    .pagination {
      flex: 1;
    }
  }
  ::v-deep .el-table {
    tr,
    td {
      border-bottom: none;
    }
  }
}
</style>
  • requestAnimationFrame 实现滚动
<template>
  <div class="rtc-table" @mouseenter="stopScroll" @mouseleave="startScroll">
    <el-table
      ref="rollTable"
      :data="tableData"
      style="width: 100%"
      v-bind="tableConfig"
    >
      <template slot="empty"> 暂无数据 </template>
      <el-table-column
        v-if="showIndex"
        type="index"
        :index="indexMethod"
        label="序号"
        align="center"
      >
      </el-table-column>
      <el-table-column
        v-for="item in propsList"
        :key="`${item.prop}_${item.label}`"
        :prop="item.prop"
        :label="item.label"
        :width="item.width"
        :align="item.align || 'center'"
        v-bind="item.otherConfig"
      >
        <template v-if="item.child">
          <el-table-column
            v-for="sitem in item.child"
            :key="`${sitem.prop}_${sitem.label}`"
            :prop="sitem.prop"
            :label="sitem.label"
            :width="sitem.width"
            :align="sitem.align || 'center'"
            v-bind="sitem.otherConfig"
          >
            <template slot-scope="scope">
              <slot :name="sitem.slotName" :scope="scope">{{
                scope.row[sitem.prop]
              }}</slot>
            </template>
          </el-table-column>
        </template>
        <template slot-scope="scope">
          <slot :name="item.slotName" :scope="scope">{{
            scope.row[item.prop]
          }}</slot>
        </template>
        <template #[item.tableSlotName]="scope">
          <slot :name="item.tableSlotName" :scope="scope">{{
            scope.column.label
          }}</slot>
        </template>
      </el-table-column>
    </el-table>
    <div v-if="showPagination" class="table-bottom mt-20">
      <slot name="table-bottom"></slot>
      <div class="pagination" style="text-align: right">
        <el-pagination
          background
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="pagingInfo.pageIndex"
          :page-sizes="[10, 20, 50]"
          :page-size="pagingInfo.pageSize"
          :total="pagingInfo.total"
          v-bind="pagingConfig"
        >
        </el-pagination>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "RtcTable",
  props: {
    tableData: {
      type: Array,
      default: () => [],
    },
    tableConfig: {
      type: Object,
      default: () => {
        return {};
      },
    },
    propsList: {
      type: Array,
      default: () => [],
      required: true,
    },
    showIndex: {
      type: Boolean,
      default: false,
    },
    showPagination: {
      type: Boolean,
      default: false,
    },
    pagingInfo: {
      type: Object,
      default: () => ({
        pageIndex: 1,
        pageSize: 10,
        total: 0,
      }),
    },
    pagingConfig: {
      type: Object,
      default: () => ({
        layout: "total, sizes, prev, pager, next, jumper",
      }),
    },
    scroll: {
      type: Boolean,
      default: false,
    },
    handle: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      scrollT: 0,
      scrollTime: null,
      lastTime: 0,
      interval: 100,
    };
  },
  computed: {
    isAutoScroll() {
      // 是否自动滚动
      return this.scroll && !this.handle;
    },
    canScroll() {
      // 是否可以滚动
      return (
        this.isAutoScroll &&
        Array.isArray(this.tableData) &&
        this.tableData.length > 0
      );
    },
  },
  watch: {
    canScroll(nv) {
      if (nv) {
        console.log("canScroll");
        this.toScroll();
      }
    },
  },
  methods: {
    indexMethod(index) {
      return index + 1;
    },
    handleSizeChange(val) {
      this.$emit("update:pagingInfo", {
        ...this.pagingInfo,
        pageSize: val,
        pageIndex: 1,
      });
      this.$emit("pagingChange", {
        ...this.pagingInfo,
        pageSize: val,
        pageIndex: 1,
      });
    },
    handleCurrentChange(val) {
      this.$emit("update:pagingInfo", { ...this.pagingInfo, pageIndex: val });
      this.$emit("pagingChange", { ...this.pagingInfo, pageIndex: val });
    },
    toScroll() {
      this.stopScroll();
      if (!this.tableData.length || !this.scroll || !this.$refs.rollTable) {
        this.$emit("notScroll");
        return;
      }
      let divData = this.$refs.rollTable.bodyWrapper;
      if (this.handle && divData.clientHeight >= divData.scrollHeight) {
        this.$emit("notScroll");
        return;
      }
      let initTop = 0;
      if (this.scrollT < 0) {
        initTop = this.scrollT * -1;
      }

      const scrollFn = (currentTime) => {
        if (!this.lastTime) this.lastTime = currentTime;
        const elapsed = currentTime - this.lastTime;

        if (elapsed > this.interval) {
          this.lastTime = currentTime - (elapsed % this.interval);

          divData.scrollTop += 1;
          initTop++;
          if (initTop >= divData.offsetTop / 2) {
            initTop = 0;
          }
          divData.style.Top = "-" + initTop + "px";
          if (
            divData.clientHeight + divData.scrollTop ==
            divData.scrollHeight
          ) {
            divData.scrollTop = 0;
            this.$emit("end");
          }
        }
        this.scrollTime = requestAnimationFrame(scrollFn);
      };
      scrollFn();
    },
    stopScroll() {
      this.scrollTime && cancelAnimationFrame(this.scrollTime);
    },
    startScroll() {
      if (!this.scroll || !this.$refs.rollTable) return;
      let target = this.$refs.rollTable.bodyWrapper;
      this.scrollT = target.offsetTop;
      this.toScroll();
    },
    autoScroll() {
      if (this.scroll && !this.handle) this.toScroll();
    },
  },
  beforeDestroy() {
    this.stopScroll();
  },
};
</script>

<style lang="scss" scoped>
.rtc-table {
  .table-bottom {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0;
    .pagination {
      flex: 1;
    }
  }
  ::v-deep .el-table {
    tr,
    td {
      border-bottom: none;
    }
  }
}
</style>

Button 组件 type = "text" 时,拓展状态样式

$--color-danger: #f56c6c !default;
$--color-warning: #e6a23c !default;
$custom-colors: (
  "danger": $--color-danger,
  "warning": $--color-warning,
);

// 拓展状态样式 text-danger, text-warning
@each $name, $color in $custom-colors {
  .el-button--text.text-#{$name} {
    color: $color !important;
    &:hover {
      color: lighten($color, 5%) !important;
    }
    &:active {
      color: $color !important;
    }
  }
}

定制主题

  1. 创建 element-variables.scss 文件,内容如下:
$--color-primary: #1fa3b0 !default;
$--color-danger: #f56c6c !default;
$--color-warning: #e6a23c !default;
/* 改变 icon 字体路径变量,必需 */
$--font-path: "~element-ui/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";
  1. 在 main.js 中引入 element-variables.scss 文件:
import Vue from "vue";
import ElementUI from "element-ui";
import "./element-variables.scss";

Vue.use(ElementUI);
  1. vue-cli 生产环境打包,element-ui 字体图标有时会乱码

问题描述

  • 开发环境正常,生产环境乱码(不是必现,打开控制台查看字体图标时会出现乱码,关闭控制台后恢复正常)。

  • 生产环境打包, 所有 el-icon 的样式中 content 属性的值为 "" 或者是其他乱码字符,如:

    .el-icon-delete:before {
      content: "";
    }
    

解决方案:

  • 安装 css-unicode-loader
npm install css-unicode-loader --save-dev
  • 修改 vue.config.js 文件,增加 css-unicode-loader 配置:
// vue.config.js
module.exports = {
  configureWebpack: (config) => {
    if (process.env.NODE_ENV === "production") {
      const sassLoader = require.resolve("sass-loader");
      config.module.rules
        .filter((rule) => rule.test.toString().includes("scss"))
        .forEach((rule) => {
          rule.oneOf.forEach((oneOfRule) => {
            const sassIndex = oneOfRule.use.findIndex(
              (item) => item.loader === sassLoader
            );
            oneOfRule.use.splice(sassIndex, 0, {
              loader: require.resolve("css-unicode-loader"),
            });
          });
        });
    }
  },
  css: {
    loaderOptions: {
      sass: {
        sassOptions: {
          quietDeps: true, // 禁用终端输出 sass 警告信息
        },
      },
    },
  },
};

定制主题(完整)

  • element-variables.scss 文件内容:
:root {
  --color-white: #fff;
  // --color-black: #000;
  // 1fa3b0
  // --color-primary: #1fa3b0;
  --color-primary: #2e57e5;
  --color-primary-light-1: color-mix(
    in srgb,
    var(--color-white) 10%,
    var(--color-primary)
  );
  --color-primary-light-2: color-mix(
    in srgb,
    var(--color-white) 20%,
    var(--color-primary)
  );
  --color-primary-light-3: color-mix(
    in srgb,
    var(--color-white) 30%,
    var(--color-primary)
  );
  --color-primary-light-4: color-mix(
    in srgb,
    var(--color-white) 40%,
    var(--color-primary)
  );
  --color-primary-light-5: color-mix(
    in srgb,
    var(--color-white) 50%,
    var(--color-primary)
  );
  --color-primary-light-6: color-mix(
    in srgb,
    var(--color-white) 60%,
    var(--color-primary)
  );
  --color-primary-light-7: color-mix(
    in srgb,
    var(--color-white) 70%,
    var(--color-primary)
  );
  --color-primary-light-8: color-mix(
    in srgb,
    var(--color-white) 80%,
    var(--color-primary)
  );
  --color-primary-light-9: color-mix(
    in srgb,
    var(--color-white) 90%,
    var(--color-primary)
  );
  --color-success: #13b881;
  --color-warning: #fcb242;
  --color-danger: #e75e5e;
  --color-info: #909399;
}
// 覆盖 sass 内置的 mix 函数,使得其支持var变量
@function mix($color1, $color2, $p: 50%) {
  @return color-mix(in srgb, $color1 $p, $color2);
}
/* 改变主题色变量 */
$--color-white: var(--color-white) !default;
// $--color-black: var(--color-black) !default;
$--color-primary: var(--color-primary) !default;
$--color-primary-light-1: var(--color-primary-light-1) !default;
$--color-primary-light-2: var(--color-primary-light-2) !default;
$--color-primary-light-3: var(--color-primary-light-3) !default;
$--color-primary-light-4: var(--color-primary-light-4) !default;
$--color-primary-light-5: var(--color-primary-light-5) !default;
$--color-primary-light-6: var(--color-primary-light-6) !default;
$--color-primary-light-7: var(--color-primary-light-7) !default;
$--color-primary-light-8: var(--color-primary-light-8) !default;
$--color-primary-light-9: var(--color-primary-light-9) !default;
$--color-success: var(--color-success) !default;
$--color-warning: var(--color-warning) !default;
$--color-danger: var(--color-danger) !default;
$--color-info: var(--color-info) !default;

/* 改变 icon 字体路径变量,必需 */
$--font-path: "~element-ui/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";
  • 创建themeColor.vue,动态修改 CSS 变量:
<template>
  <el-color-picker v-model="themeColor" @change="updateThemeColor" />
</template>

<script>
export default {
  data() {
    return {
      themeColor: localStorage.getItem("themeColor") || "#409EFF",
      predefineColors: [
        "#ff4500",
        "#ff8c00",
        "#ffd700",
        "#90ee90",
        "#00ced1",
        "#1e90ff",
        "#c71585",
        "rgba(255, 69, 0, 0.68)",
        "rgb(255, 120, 0)",
        "hsv(51, 100, 98)",
        "hsva(120, 40, 94, 0.5)",
        "hsl(181, 100%, 37%)",
        "hsla(209, 100%, 56%, 0.73)",
        "#c7158577",
      ],
    };
  },
  methods: {
    handleThemeChange(color) {
      // 动态修改CSS变量
      const root = document.documentElement;
      root.style.setProperty("--color-primary", color);
      // 扩展其他变量(如辅助色)
      const successColor = this.generateSuccessColor(color);
      root.style.setProperty("--color-success", successColor);
      localStorage.setItem("themeColor", color);
    },
    generateSuccessColor(primary) {
      // 基于主色生成辅助色算法[6](@ref)
      return tinycolor(primary).spin(120).toHexString();
    },
    updateThemeColor(color) {
      if (!color) {
        color = "#2e57e5";
        this.themeColor = color;
      }
      const root = document.documentElement;
      root.style.setProperty("--color-primary", color);
      localStorage.setItem("themeColor", color);
    },
  },
  // mounted() {
  //   const savedColor = localStorage.getItem("themeColor");
  //   if (savedColor) this.handleThemeChange(savedColor);
  // },
};
</script>

<style></style>

图标打包问题

config.module
  .rule("scss")
  .oneOfs.values()
  .forEach((oneOf) => {
    oneOf
      .use("css-unicode-loader")
      .loader(require.resolve("css-unicode-loader"))
      .before("sass-loader");
  });
module.exports = defineConfig({
  publicPath: "/",
  outputDir: "dist",
  assetsDir: "static",
  lintOnSave: isDev,
  productionSourceMap: false,
  transpileDependencies: isProd ? transpileDependencies : [],
  devServer: {
    client: {
      overlay: false,
    },
    port: port,
    //代理本地接口地址
    proxy: {
      [process.env.VUE_APP_BASE_API]: {
        // http://192.168.1.106:18351
        // 204 37
        target: "http://192.168.1.12:5227/",
        changeOrigin: true,
        pathRewrite: {
          ["^" + process.env.VUE_APP_BASE_API]: "",
        },
      },
    },
    //before: require('./mock/mock-server.js')
  },
  css: {
    loaderOptions: {
      sass: {
        sassOptions: {
          quietDeps: true,
        },
      },
    },
  },
  configureWebpack: {
    // provide the app's title in webpack's name field, so that
    // it can be accessed in index.html to inject the correct title.
    name: name,
    resolve: {
      alias: {
        "@": resolve("src"),
      },
      fallback: {
        path: require.resolve("path-browserify"),
        process: require.resolve("process/browser"),
      },
    },
    plugins: [
      // 全局注入 process 变量
      new webpack.ProvidePlugin({
        process: "process/browser",
      }),
    ],
  },
  chainWebpack(config) {
    config.module.rule("svg").uses.clear();
    config.module.rule("svg").exclude.add(resolve("src/icons")).end();
    config.module
      .rule("icons")
      .test(/\.svg$/)
      .include.add(resolve("src/icons"))
      .end()
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({ symbolId: "icon-[name]" })
      .end();

    // set preserveWhitespace
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => {
        options.compilerOptions.preserveWhitespace = true;
        return options;
      })
      .end();

    config.module
      .rule("scss")
      .oneOfs.values()
      .forEach((oneOf) => {
        oneOf
          .use("css-unicode-loader")
          .loader(require.resolve("css-unicode-loader"))
          .before("sass-loader");
      });

    config.when(isProd, (config) => {
      config.optimization.splitChunks({
        chunks: "all",
        cacheGroups: {
          libs: {
            name: "chunk-libs",
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: "initial", // only package third parties that are initially dependent
          },
          elementUI: {
            name: "chunk-elementUI", // split elementUI into a single package
            priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
            test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
          },
          commons: {
            name: "chunk-commons",
            test: resolve("src/components"), // can customize your rules
            minChunks: 3, //  minimum common number
            priority: 5,
            reuseExistingChunk: true,
          },
        },
      });
    });
  },
});
最后编辑:
上一页
组件设计
下一页
WebSocket