Giter Club home page Giter Club logo

vue3-virtual-scroller's Issues

About component usage

hi~
I used the vue3-virtual-scroller component in the project
code show as below

<template>
  <div class="home-index-full home-index-head">
    <DynamicScroller
      class="scroller"
      :items="resetMenuList"
      key-field="name"
      :min-item-size="80"
    >
      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[
            item.message,
          ]"
          :data-index="index"
        >
          <menu-warp :base-data="item" />
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </div>
</template>

You can see the elements normally,However, no virtual scrolling effect is seen!

Dragging the scrollbar quickly results in empty content sometimes

Hello, first of all, thank you very much for the work you have done to support vue3 with vue-virtual-scroller. It is great, but I encountered a small problem. The problem is: when I drag the scroll bar quickly (of course I release the mouse after the drag is completed), I sometimes encounter the problem that the content cannot be loaded and no error is reported. However, after this problem occurs, when I scroll the wheel, the content will appear normally. I'm very confused. I hope you can help me. Thank you very much.

// table_utils.js
const Events = {
  Change: "change",
  CellChange: "cell-change",
  Error: "error",
  Delete: "delete",
  Operate: "Operate",
  Blur: "blur",
  Focus: "focus",
  UpdateModelValue: "update:modelValue",
  Expand: "expand",
};
const { h, defineComponent, ref } = Vue;
const { ElInput, ElButton, ElIcon, ElTooltip } = ElementPlus;
const { Close } = ElementPlusIconsVue;
const { cloneDeep, isNumber, isUndefined } = _;

const CustomInput = {
  name: "custom-input",
  emits: [
    Events.Change,
    Events.Error,
    Events.Focus,
    Events.Blur,
    Events.UpdateModelValue,
  ],
  components: { ElInput },
  props: {
    modelValue: {
      required: true,
      type: [String, Number, undefined],
    },
    config: {
      required: true,
      type: Object,
    },
  },
  template: /*html*/ `
    <el-input
      :class="getInputCss"
      ref="inputRef"
      v-model="value"
      @input="onInput"
      @change="onChange"
      @blur="onBlur"
      @focus="onFocus"
      @mouseover="onMouseOver"
      @mouseleave="onMouseLeave"
      size="small"
    >
    </el-input>
  `,
  data() {
    return {
      value: cloneDeep(this.modelValue),
      error: "",
    };
  },
  computed: {
    getInputCss() {
      return {
        "is-error": Boolean(this.error),
      };
    },
  },
  watch: {
    modelValue: {
      handler(new_value) {
        this.value = cloneDeep(new_value);
      },
    },
  },
  methods: {
    focus() {
      this.$refs.inputRef.focus();
    },
    onMouseOver() {
      console.log("custom input onMouseOver");
      if (this.error) this.$emit(Events.Error, this.error, this.$refs.inputRef);
    },
    onMouseLeave() {
      if (this.error) this.$emit(Events.Error); // 取消错误框的显示
    },
    onBlur() {
      this.validate();
      if (this.error) return;
      this.$emit(Events.Blur);
    },
    onFocus() {
      this.$emit(Events.Focus);
    },
    onInput() {
      this.validate();
    },
    onChange() {
      this.validate();
      if (this.error) return;
      const value = outFormatter(this.config.outType, this.value);
      this.value = value;
      this.$emit(Events.Change, value);
      this.$emit(Events.UpdateModelValue, value);
    },
    validate() {
      if (this.config.required) {
        this.error = requiredValidate(this.value);
        if (this.error) {
          this.$emit(Events.Error, this.error, this.$refs.inputRef);
          return;
        }
      }
      if (!this.config.validate) return;
      else {
        this.error = this.config.validate(this.value);
        this.$emit(Events.Error, this.error, this.$refs.inputRef);
        return;
      }
    },
  },
};

const EditableText = {
  name: "editable-text",
  emits: [Events.Change, Events.Error],
  components: { CustomInput },
  props: {
    rowData: {
      required: true,
      type: Object,
    },
    column: {
      required: true,
      type: Object,
    },
  },
  template: /*html*/ `
    <div class="editable-text">
      <custom-input
        v-if="isEdit"
        ref="inputRef"
        v-model="value"
        :config="column"
        @change="onChange"
        @blur="onExitEditMode"
        @focus="onEnterEditMode"
        @error="onError"
        @mouseover="onMouseOver"
        @mouseleave="onMouseLeave"
      >
      </custom-input>
      <div v-else @dblclick="onEnterEditMode" :title="value">
        {{formattedText}}
      </div>
    </div>
  `,
  data() {
    return {
      editing: false,
      value: cloneDeep(this.rowData[this.column.dataKey]),
      error: "",
    };
  },
  computed: {
    isEdit() {
      return this.rowData._creating || this.editing;
    },
    formattedText() {
      if (this.column.textFormatter)
        return this.column.textFormatter(this.value);
      else return this.value;
    },
  },
  watch: {
    rowData: {
      handler(new_value) {
        this.value = new_value[this.column.dataKey];
      },
    },
  },
  methods: {
    onMouseOver() {
      if (this.error) this.$emit(Events.Error, this.error, this.$refs.inputRef);
    },
    onMouseLeave() {
      if (this.error) this.$emit(Events.Error); // 取消错误框的显示
    },
    onExitEditMode() {
      this.editing = false;
    },
    onEnterEditMode() {
      if (!this.rowData._editable) return;
      this.editing = true;
      this.$nextTick(() => {
        this.$refs.inputRef.focus();
      });
    },
    onChange(value) {
      this.rowData[this.column.dataKey] = this.value;
      this.editing = false;
      this.$emit(Events.Change, value);
    },
    onError(...args) {
      this.$emit(Events.Error, ...args);
    },
  },
};

const DeleteIcon = {
  name: "delete-icon",
  emits: [Events.Delete],
  components: { Close, ElButton, ElIcon },
  template: /*html*/ `
    <el-button
      circle
      type="danger"
      size="small"
      class="delete_button"
      @click="$emit('${Events.Delete}')"
    >
      <el-icon>
        <Close />
      </el-icon>
    </el-button>
  `,
};

const createTextFormatter = (precision = 4) => {
  return (value) => {
    if (value === null) return "-";
    else {
      const num = Number(value);
      return Number.isNaN(num) ? value : num.toFixed(precision);
    }
  };
};

const outFormatter = (type, value) => {
  let _value;
  switch (type) {
    case "number":
      _value = Number(value);
      break;
    default:
      _value = value;
      break;
  }
  return _value;
};

const numberValidate = (value) => {
  const num = Number(value);
  if (Number.isNaN(num) || !isNumber(num)) return "please input number";
  else return "";
};

const requiredValidate = (value) => {
  if ([undefined, ""].includes(value)) {
    return "field required";
  } else return "";
};

const ExpandText = {
  name: "expand-text",
  emits: [Events.UpdateModelValue],
  props: {
    modelValue: {
      type: Boolean,
      required: true,
    },
  },
  template: /*html*/ `
        <div class="expand-icon" @click="onClick">
            <div :class="getEditableTextClass"></div>
            <div class="expand-icon-label">
                <slot></slot>
            </div>
        </div>
    `,
  data() {
    return {};
  },
  computed: {
    getEditableTextClass() {
      return {
        "expand-icon-box": true,
        expand: this.modelValue,
      };
    },
  },
  methods: {
    onClick() {
      this.$emit(Events.UpdateModelValue, !this.modelValue);
    },
  },
};

const TableCellComponent = {
  ExpandText: "expand-text",
  EditableText: "editable-text",
  DeleteIcon: "delete-icon",
  Text: "text",
  Input: "input",
};

const TableCell = defineComponent({
  name: "table-cell",
  emits: [Events.Change, Events.Expand, Events.Error],
  components: {
    ExpandText,
    EditableText,
    DeleteIcon,
    CustomInput,
  },
  props: {
    rowData: {
      type: Object,
      required: true,
    },
    column: {
      type: Object,
      required: true,
    },
  },
  template: /*html*/ `
    <div>
      <div v-if="getComponent === 'editable-text'">
        <editable-text
          :rowData="rowData"
          :column="column"
          @change="handleChange"
          @error="onError"
        >
      </editable-text>
      </div>
      <div v-else-if="getComponent === 'delete'">
        <delete-icon @delete="handleOperate('delete', rowData, column)"></delete-icon>
      </div>
      <div v-else-if="getComponent === 'expand-text'">
        <expand-text
          v-model="rowData.is_expand"
          @update:modelValue="handleExpand"
        >
          {{ rowData[column.dataKey] }}
        </expand-text>
      </div>
      <div v-else-if="getComponent === 'text'">
        {{ rowData[column.dataKey] }}
      </div>
      <div v-else-if="getComponent === 'input'">
        <custom-input v-model="rowData[column.dataKey]" :config="column" change="handleChange" @error="onError"></custom-input>
      </div>
    </div>
  `,
  computed: {
    getComponent() {
      if (this.column.component) return this.column.component;
      const component_index = isUndefined(this.rowData.children) ? 1 : 0;
      return this.column.components[component_index];
    },
  },
  methods: {
    handleChange(value) {
      this.$emit(Events.Change, value, this.rowData, this.column);
    },
    onError(msg, errorRef) {
      return this.$emit(Events.Error, msg, errorRef);
    },
    handleOperate(operation, rowData, column) {
      if (operation === "delete") {
        this.$parent.$parent.handleOperate(operation, rowData, column);
      }
    },
    handleExpand(value) {
      this.rowData.is_expand = value;
      this.$emit(Events.Expand, value);
    },
    isUndefined(value) {
      return typeof value === "undefined";
    },
  },
});

const popperOptions = {
  modifiers: [
    {
      name: "computeStyles",
      options: {
        adaptive: false,
        enabled: false,
      },
    },
  ],
};

const VirtualTable = defineComponent({
  name: "virtual-table",
  props: {
    tableData: {
      type: Array,
      required: true,
    },
    maxTableHeight: {
      type: Number,
      default: 500,
    },
    columns: {
      type: Array,
      required: true,
    },
    addition: {
      type: Boolean,
      default: false,
    },
  },
  components: {
    RecycleScroller: Vue3VirtualScroller.RecycleScroller,
    TableCell,
    ElTooltip,
    ElButton,
  },
  template: /*html*/ `
    <div class="virtual-table">
      <table class="fixed-header">
        <thead>
          <tr>
            <th v-for="column in getColumns" :style="{width: column.width}">{{column.title}}</th>
          </tr>
        </thead>
      </table>
      <div class="scroller-container">
        <recycle-scroller
          ref="scroller"
          class="scroller"
          :items="rows"
          :item-size="itemSize"
          key-field="id"
          :buffer="2000"
          :style="{height: getTableHeight}"
          v-slot="{ item, index }"
        >
          <tr >
            <td v-for="column in getColumns">
              <table-cell
                :row-data="item"
                :column="column"
                :style="{width: column.width}"
                @change="handleCellChange"
                @expand="handleExpandElement(item, index)"
                @error="handleInputError"
              >
              </table-cell>
            </td>
          </tr>
        </recycle-scroller>
      </div>
      <table class="addtion-table">
        <tr v-for="(item, index) in getAdditonData">
          <td v-for="column in getAdditonColumns">
            <table-cell
              :row-data="item"
              :column="column"
              :style="{width: column.width}"
              @change="handleCellChange"
              @error="handleInputError"
            >
            </table-cell>
          </td>
        </tr>
      </table>
      <div v-if="addition" class="addition-button">
          <el-button type="primary" @click="handlerAddRowClick" round> + </el-button>
          <el-button
              v-if="showSubmitButton"
              type="primary"
              @click="handlerSubmitRowClick"
              round
          >

          </el-button>
        </div>
        <el-tooltip
          v-if="error"
          popper-class="property-error-popper"
          effect="light"
          :content="error"
          :visible="Boolean(error)"
          :virtual-ref="errorRef"
          virtual-triggering
          :popper-options="popperOptions"
          trigger="hover"
        ></el-tooltip>
    </div>
  `,
  data() {
    return {
      rows: this.initRows(),
      error: "",
      itemSize: 23,
      errorRef: ref(),
      temp_rows: {},
      popperOptions,
    };
  },
  computed: {
    getTableHeight() {
      const height = this.rows.length * this.itemSize;
      return `${Math.min(this.maxTableHeight, height)}px`;
    },
    element_map() {
      const element_map = {};
      this.tableData.forEach((element_row) => {
        element_map[element_row.name] = element_row.children;
      });
      return element_map;
    },
    getAdditonData() {
      return Object.values(this.temp_rows);
    },
    getAdditonColumns() {
      return this.getColumns.map((item) => {
        return {
          ...item,
          component: item.component ?? item.components[2],
        };
      });
    },
    getColumns() {
      return this.columns.map((item) => {
        if (item.textFormatter) {
          const _textFormatter = item.textFormatter;
          item.textFormatter = (...args) => _textFormatter(...args, this);
        }
        if (item.cellRenderer) {
          const _cellRenderer = item.cellRenderer;
          item.cellRenderer = (...args) => _cellRenderer(...args, this);
        }
        if (item.validate) {
          const _validate = item.validate;
          item.validate = (...args) => _validate(...args, this);
        }
        return item;
      });
    },
    getColumnsMap() {
      const maps = {};
      const columns = this.getColumns;
      Object.keys(columns).forEach((index) => {
        const value = columns[index];
        maps[value.dataKey] = columns[index];
      });
      return maps;
    },
    showSubmitButton() {
      return Boolean(Object.keys(this.temp_rows).length);
    },
  },
  mounted() {
    this.initRows();
    this.fixScrollEmptyBug();
  },
  methods: {
    initRows() {
      const rows = new Array(this.tableData.length);
      for (let i = 0; i < this.tableData.length; i += 1) {
        rows[i] = this.tableData[i];
      }
      return rows;
    },
    handleExpandElement(item, index) {
      if (item.is_expand) {
        this.rows[index].is_expand = true;

        // 计算新数组的大小
        let newSize = this.rows.length + this.element_map[item.name].length;

        // 创建一个新的具有正确大小的数组
        let newArray = new Array(newSize);

        // 复制插入点之前的数据
        for (let i = 0; i <= index; i++) {
          newArray[i] = this.rows[i];
        }

        // 插入新数据
        for (let i = 0; i < this.element_map[item.name].length; i++) {
          newArray[index + 1 + i] = this.element_map[item.name][i];
        }

        // 复制插入点之后的数据
        for (let i = index + 1; i < this.rows.length; i++) {
          newArray[this.element_map[item.name].length + i] = this.rows[i];
        }

        // 使用新数组替换旧数组
        this.rows = newArray;
      } else {
        this.rows[index].is_expand = false;

        // 计算新数组的大小
        let newSize = this.rows.length - this.element_map[item.name].length;

        // 创建一个新的具有正确大小的数组
        let newArray = new Array(newSize);

        // 复制删除点之前的数据
        for (let i = 0; i < index + 1; i++) {
          newArray[i] = this.rows[i];
        }

        // 复制删除点之后的数据,跳过要删除的元素
        for (
          let i = index + 1 + this.element_map[item.name].length;
          i < this.rows.length;
          i++
        ) {
          newArray[i - this.element_map[item.name].length] = this.rows[i];
        }

        // 使用新数组替换旧数组
        this.rows = newArray;
      }
    },
    handleInputError(msg, inputRef) {
      if (isUndefined(msg)) {
        // 取消错误提示框
        this.error = "";
        return;
      }
      this.error = msg;
      this.errorRef = inputRef;
    },
    handleCellChange(value, rowData, column) {
      if (rowData._creating) return;
      this.$emit(Events.CellChange, value, rowData, column);
    },
    handleOperate(op_type, rowData, column) {
      if (op_type === "delete" && rowData._creating) {
        this.clearSubmitItem(rowData.id);
        return;
      }
      this.$emit(Events.Operate, op_type, rowData, column);
    },
    clearSubmitItem(id) {
      delete this.temp_rows[id];
    },
    /**
     * @description 提交成功后的清理工作
     */
    clearSubmit(clear_all = false) {
      if (clear_all) {
        const temp_rows_keys = Object.keys(this.temp_rows);
        if (temp_rows_keys.length) {
          temp_rows_keys.forEach((key) => {
            this.clearSubmitItem(key);
          });
        }
      }
    },
    handlerAddRowClick() {
      const uuid = String(new Date().getTime());
      const new_row = {
        id: uuid,
        children: [],
        _creating: true,
      };
      this.temp_rows[new_row.id] = new_row;
      this.getColumns.forEach((column) => {
        if (column.dataKey) new_row[column.dataKey] = "";
      });
    },
    handlerSubmitRowClick() {
      let temp_rows = Object.values(cloneDeep(this.temp_rows));
      temp_rows = temp_rows.filter((row) => {
        const columnsMap = this.getColumnsMap;
        return Object.keys(row).every((key) => {
          const column = columnsMap[key];
          if (column) {
            const err_msg = column.validate && column.validate(row[key]);
            if (err_msg) return false;
            if (column.outType)
              row[key] = outFormatter(column.outType, row[key]);
          }
          return true;
        });
      });
      if (!temp_rows.length) return;
      this.$emit(Events.Operate, "create", temp_rows);
    },
    fixScrollEmptyBug() {
      let scrollTimeout;
      console.log(this);
      const handleScroll = (event) => {
        // 清除旧的定时器
        if (scrollTimeout) {
          clearTimeout(scrollTimeout);
        }

        // 设置新的定时器
        scrollTimeout = setTimeout(() => {
          console.log("滚动已停止,触发滚动键");

          // 在这里触发滚动键
          // 例如:模拟向下箭头键的按下
          const keyboardEvent = new KeyboardEvent("keydown", {
            key: "ArrowDown",
            code: "ArrowDown",
          });
          document.dispatchEvent(keyboardEvent);
        }, 1050);
      };

      // 监听滚动事件
      document
        .querySelector(".scroller")
        .addEventListener("scroll", handleScroll);
    },
  },
});

var TableUtils = {
  VirtualTable,
  createTextFormatter,
  numberValidate,
  TableCellComponent,
};
// index.html
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/vue3-virtual-scroller/dist/vue3-virtual-scroller.css"
/>
<link rel="stylesheet" href="./element-plus.min.css" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/[email protected]/dist/index.full.js"></script>
<script src="https://unpkg.com/@element-plus/[email protected]/dist/index.iife.min.js"></script>

<script src="https://unpkg.com/[email protected]/lodash.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue3-virtual-scroller.min.js"></script>
<link rel="stylesheet" href="./big-table.css" />
<script src="./table_utils.js"></script>
<div id="app" style="width: 351px">
  <virtual-table
    :table-data="tableData"
    :columns="columns"
    :max-table-height="maxTableHeight"
    expand-column-key="name"
    @cell-change="handleCellChange"
    @operate="handlerOperation"
    :addition="isEditable"
  >
  </virtual-table>
</div>

<script>
  {
    class Element {
      static is_valid_symbol(element) {
        return element === "C";
      }
    }
    const { TableCellComponent, VirtualTable, numberValidate } = TableUtils;
    const { createApp } = Vue;
    const elements = ["Ca", "S", "O"];
    const tableData = [];
    const site_length = 1000000;
    elements.forEach((element, index) => {
      let i = 0;
      const children = new Array(site_length);
      const row = {
        children,
        is_expand: false,
        _editable: false,
        id: element,
        name: element,
        a: "-",
        b: "-",
        c: "-",
        occupancy: "-",
      };
      tableData.push(row);
      const id_offset = site_length * index;
      while (i < site_length) {
        const name = `${element}${i}`;
        children[i++] = {
          id: name,
          name,
          a: 1,
          b: 2,
          c: 3,
          occupancy: 0.5,
          _editable: true,
        };
      }
    });
    const isEditable = true;
    const isPeriodicity = true;
    const app = createApp({
      components: {
        VirtualTable,
      },
      data() {
        return {
          tableData,
          maxTableHeight: 506,
          columns: this.initColumn(),
          isEditable,
        };
      },

      methods: {
        translate(value) {
          return value;
        },
        initColumn() {
          const isCoordsEditable = true;
          const _numberValidate = numberValidate;
          const columns = [
            {
              title: "原子",
              width: "80",
              dataKey: "name",
              required: true,
              components: [
                TableCellComponent.ExpandText,
                TableCellComponent.Text,
                TableCellComponent.Input,
              ],
              validate: (value) => {
                const res = Element.is_valid_symbol(value);
                if (!res) return this.translate("unknown element symbol");
                else return "";
              },
            },
            {
              title: "a",
              width: "60",
              dataKey: "a",
              outType: "number",
              required: true,
              textFormatter: createTextFormatter(4),
              components: [
                TableCellComponent.Text,
                TableCellComponent.EditableText,
                TableCellComponent.Input,
              ],
              validate: _numberValidate,
            },
            {
              title: "b",
              width: "60",
              dataKey: "b",
              outType: "number",
              required: true,
              textFormatter: createTextFormatter(4),
              components: [
                TableCellComponent.Text,
                TableCellComponent.EditableText,
                TableCellComponent.Input,
              ],
              validate: _numberValidate,
            },
            {
              title: "c",
              width: "60px",
              dataKey: "c",
              outType: "number",
              required: true,
              textFormatter: createTextFormatter(4),
              components: [
                TableCellComponent.Text,
                TableCellComponent.EditableText,
                TableCellComponent.Input,
              ],
              validate: _numberValidate,
            },
          ];
          if (isPeriodicity) {
            columns.push({
              key: "occupancy",
              dataKey: "occupancy",
              title: "占比",
              width: "40",
              outType: "number",
              required: true,
              textFormatter: createTextFormatter(2),
              components: [
                TableCellComponent.Text,
                TableCellComponent.EditableText,
                TableCellComponent.Input,
              ],
              validate: (value) => {
                let res = _numberValidate(value);
                if (res) return res;
                else {
                  if (value <= 0 || value > 1) {
                    res = this.translate("occupancy should be within (0, 1]");
                  } else res = "";
                  return res;
                }
              },
            });
          }
          if (isEditable) {
            columns.push({
              key: "operate",
              title: "",
              width: "20",
              component: TableCellComponent.DeleteIcon,
            });
          }
          return columns;
        },
        handleCellChange() {},
        handlerOperation() {},
      },
    });

    app.mount("#app");
  }
</script>
// big-table.css
:root {
  --table-border-color: rgba(102, 120, 208, 0.5);
  --border-color: var(--color-main);
  --white: #fff;
  --balck: #fff;
  --color-main: #6678d0;
  --font-color: #606266;
  width: 100%;
  color: var(--font-color);
  font-family: "Microsoft YaHei", "Lucida Grande", "Segoe UI", "Ubuntu",
    "Cantarell", sans-serif;
  font-size: 12px;
  font-weight: normal;
}

.scroller {
  border: 1px solid #ddd;
}

th,
td {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-align: center;
  vertical-align: top;
  height: 22px;
}

.fixed-header {
  margin-bottom: -1px; /* 避免双重边框 */
}

.scroller-container {
  overflow-y: auto;
}

.addition-button {
  margin-top: 2px;
  display: flex;
  justify-content: center;
}
.addition-button .el-button {
  height: 14px;
  padding-bottom: 2px;
  border: none;
  font-size: 12px;
  padding: 0px 8px;
  line-height: 12px;
  background: var(--color-main);
  box-shadow: 0px 2px 6px 0px var(--color-main);
  color: var(--color-white);
  z-index: 1;
}

.delete_button.el-button.el-button--small.is-circle {
  width: 12px;
  height: 12px;
}

.vue-recycle-scroller {
  border: none;
  /* border-bottom: 1px solid var(--table-border-color); */
}

.vue-recycle-scroller__item-view + .vue-recycle-scroller__item-view {
  border-top: 1px solid var(--table-border-color);
}

.vue-recycle-scroller__item-wrapper td + td,
.fixed-header th + th {
  border-left: 1px solid var(--table-border-color);
}

.virtual-table .addtion-table td + td {
  border-left: 1px solid var(--table-border-color);
}

.virtual-table .addtion-table tr {
  border-top: 1px solid var(--table-border-color);
}

.fixed-header {
  border-bottom: 1px solid var(--table-border-color);
}

.virtual-table table {
  border-collapse: collapse;
}

.virtual-table .el-input__inner {
  border: none;
}
.virtual-table .el-input__wrapper.is-focus .el-input__inner {
  outline: none;
}

.virtual-table input.el-input__inner {
  color: var(--font-color);
  font-family: "Microsoft YaHei", "Lucida Grande", "Segoe UI", "Ubuntu",
    "Cantarell", sans-serif;
  font-size: 12px;
  font-weight: normal;
}

.expand-icon {
  display: flex;
}

.expand-icon .expand-icon-box {
  display: inline-block;
  position: relative;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  margin-right: 2px;
  margin-top: 2px;
}

.expand-icon .expand-icon-label {
  display: inline-block;
  width: fit-content;
}

.expand-icon .expand-icon-box::before {
  position: absolute;
  content: " ";
  width: 6px;
  height: 6px;
  border-top: 0.5px solid var(--font-color);
  border-left: 0.5px solid var(--font-color);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(-225deg);
  transition: all 0.3s;
}

.expand-icon .expand.expand-icon-box::before {
  transform: translate(-50%, -50%) rotate(-135deg);
}

.el-popper.property-error-popper {
  opacity: 0.7;
  border: 1px solid red;
  color: red;
}

.el-popper[data-popper-placement^="bottom"].property-error-popper
  .el-popper__arrow::before {
  border-top-color: red;
  border-left-color: red;
}

.is-error .el-input__wrapper {
  box-shadow: 0 0 0 1px var(--el-color-danger) inset;
}

.virtual-table .el-input--small {
  height: 18px;
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.