Vue-components封装组件

  • Post author:
  • Post category:vue



目录


1、Input 输入框


2、Table 表格


3、Tree 树形控件


4、Pagination 分页


5、Dialog 对话框


6、ECharts 图表


7、wangEditor 富文本编辑器


整理一下自己在 Vue2 项目中常用到的一些封装组件 :

大部分是结合 element-ui 组件库 进行的二次封装 :

饿了么组件库  =>

Element – The world’s most popular Vue UI framework


1、Input 输入框

通过鼠标或键盘输入字符


场景需求 : 输入框限制用户只能输入数字类型


开始

封装

项目  /  src  /  components  /  common  /  numInput.vue

<!-- 作者 : 小灰狼
     功能 : 数字框
     时间 : 2022/05 -->
<template>
  <div>
    <el-input
      v-if="inputShow"
      :readonly="readonly"
      :disabled="disabled"
      :size="inputSize"
      :style="inputStyle"
      :maxlength="maxlength"
      :placeholder="placeholder"
      v-model="modelValue.val"
      @input="handleNumInput($event, modelValue.val)"
    >
    </el-input>
  </div>
</template>

<script>
// 导入数字型输入框封装函数
import { digitalInput } from "../../utils/digitalInput";

export default {
  name: "numInput",
  props: {
    inputShow: {
      // 控制是否显示数字框
      type: Boolean,
      required: true,
      default: false,
    },
    readonly: {
      // 控制数字框是否只读
      type: Boolean,
      required: true,
      default: false,
    },
    disabled: {
      // 控制数字框是否禁用
      type: Boolean,
      default: false,
    },
    inputSize: {
      // 控制数字框大小
      type: String,
      required: true,
      default: "small",
    },
    inputStyle: {
      // 控制数字框样式
      type: Object,
      required: true,
      default: {},
    },
    maxlength: {
      // 控制数字框内容长度
      type: String,
      required: true,
      default: "1",
    },
    placeholder: {
      // 数字框提示文本
      type: String,
      required: true,
      default: "正整数",
    },
    modelValue: {
      // 数字框绑定值
      // type: Object,
      required: true,
      default: {},
    },
  },
  methods: {
    handleNumInput(event, val) {
      // 限制用户输入非数字内容
      this.modelValue.val = digitalInput(event, val);
      // this.$emit("handleNumInput", digitalInput(event, val));
    },
  },
};
</script>

<style lang="scss" scoped>
</style>

项目  /  src  /  utils  /

digitalInput.js 文件

/** 数字型输入框
 * @param {} event 事件对象
 * @param {} modelValue 输入框绑定值
 * @return {} 处理好的内容
 */
// 限制用户只能输入数字
export const digitalInput = (event, modelValue) => {
  // 限制用户输入非数字内容 且 不能以 0 开头
  if (!/^[0-9]*[1-9][0-9]*$/.test(event)) {
    modelValue = event.replace(/\D/g, "").replace(/^0/g, "");
  }
  console.log(modelValue, "封装函数");
  return modelValue;
};

开始使用 :

项目  /  src  /  views  /  edit  /  index.vue

<template>
  <div>
    <!-- 数字框组件 -->
    <num-input
      :inputShow="quInputType === '3'"
      :readonly="false"
      :inputSize="'small'"
      :inputStyle="{ width: '300px' }"
      :maxlength="numlength"
      :placeholder="'请输入正整数'"
      :modelValue="modelValue"
    ></num-input>
  </div>
</template>

<script>
// 导入数字框组件
import numInput from "../../../components/common/numInput.vue";

export default {
  name: "container",
  components: {
    numInput, // 数字框组件
  },
  data() {
    return {
      quInputType: "3", // 类型
      numlength: "1", // 控制数字位数
      // 利用对象的复杂数据类型特点来进行父子组件间的值修改
      modelValue: {
        val: "1",
      },
    };
  },
  methods: {},
};
</script>

2、Table 表格

用于展示多条结构类似的数据,可对数据进行排序、筛选、对比或其他自定义操作。

开始封装 :

项目  /  src  /  components  /  common  /  publicTable.vue

<!-- 作者 : 小灰狼
     功能 : 所有事项
     时间 : 2022/05 -->
<template>
  <div>
    <el-table
      style="width: 100%"
      v-loading="loading"
      :data="tableList"
      :header-cell-style="{ background: '#EFEFEF', textAlign: 'center' }"
      :border="false"
    >
      <el-table-column
        width="55"
        type="selection"
        align="center"
        v-if="showCheckBox"
      ></el-table-column>
      <el-table-column
        label="序号"
        width="120"
        align="center"
        v-if="showNumber"
      >
        <template slot-scope="scope">
          {{ scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column label="" min-width="35" align="center">
        <template slot-scope="scope">
          <!-- 模板图片插槽 -->
          <slot
            name="templateImg"
            :row="scope.row"
            v-if="scope.row.survey_model === 2"
          ></slot>
        </template>
      </el-table-column>
      <el-table-column
        v-for="(item, index) in headerList"
        :key="index"
        :prop="item.props"
        :label="item.label"
        :min-width="item.minWidth"
        :align="item.props === 'surveyName' ? '' : 'center'"
      ></el-table-column>
      <el-table-column
        label="操作"
        v-if="showHandle"
        min-width="300"
        align="center"
      >
        <template slot-scope="scope">
          <!-- 事项操作插槽 -->
          <slot name="matterOperation" :row="scope.row"></slot>
          <!-- 场景操作插槽 -->
          <slot name="sceneOperation" :row="scope.row"></slot>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  name: "Table",
  props: {
    headerList: {
      type: Array,
    },
    tableList: {
      type: Array,
    },
    showCheckBox: {
      // 是否显示多选框
      type: Boolean,
      default: false,
    },
    showNumber: {
      // 是否显示序号
      type: Boolean,
      default: false,
    },
    showHandle: {
      // 是否显示操作栏
      type: Boolean,
      default: true,
    },
    content: {
      type: String,
      default: "删除",
    },
    loading: {
      // 是否显示 Loading 加载
      type: Boolean,
      default: false,
    },
  },
};
</script>

<style lang="scss" scoped>
.cell button {
  border: none;
  padding-left: 0;
}
</style>


开始使用 :

项目  /  src  /  views  /  list  /  index.vue

<template>
  <div>
    <!-- Table 表格 -->
    <public-table
      :loading="formData.loading"
      :tableList="formData.tableList"
      :headerList="formData.headerList"
    >
      <!-- 模板图片插槽 -->
      <template #templateImg>
        <div>
          <img src="" alt="" />
        </div>
      </template>
      <!-- 事项操作插槽 -->
      <template #matterOperation="row">
        <div class="btn-group">
          <el-tooltip content="设计">
            <el-button
              size="small"
              icon="el-icon-edit-outline"
              @click="rowEditHandle(row.row)"
            ></el-button>
          </el-tooltip>
          <el-tooltip content="收集答卷">
            <el-button size="small" icon="el-icon-edit-round"></el-button>
          </el-tooltip>
        </div>
      </template>
    </public-table>
  </div>
</template>

<script>
import publicTable from "@/components/common/publicTable.vue";
export default {
  components: {
    publicTable, // 公共表格组件
  },
  data() {
    return {
      formData: {
        tableList: [], // 后端返回的列表数据
        showNumber: false, // 是否显示序号
        headerList: [], // 列表表头数据
        loading: false, // Loading 加载
      },
    };
  },
  methods: {
    rowEditHandle() {
      // 设计
      console.log(row.surveyId, "SCOPE");
      this.$router.push({
        path: "edit",
        query: {
          title: row.surveyName,
          currentSurveyId: row.surveyId,
          type: "2",
        },
      });
    },
  },
};
</script>


第二种封装写法 :

<template>
	<el-table
		width="100%"
		:data="list"
		:height="height"
		:row-style="rowStyle"
		:header-style="headerStyle"
		:header-cell-style="{ 'text-align': 'left' }"
		v-loading="loading"
		@row-click="queryDetail"
		@selection-change="handleSelectionchange"
	>
		<el-table-column
			v-if="isShowSection"
			type="selection"
			width="120"
			align="left"
		>
			<template v-for="item in tableTitle">
				<slot v-if="item.slot" :name="item.slot"> </slot>
				<el-table-column
					v-else
					align="left"
					:key="item.field"
					:prop="item.field"
					:width="item.width"
					:fixed="item.place"
					:label="item.label"
				>
					<template slot-scope="scope">
						<span v-if="item.field == 'typeIndex'">{{
							scope.$index + 1
						}}</span>
						<span>{{ scope.row[item.field] }}</span>
					</template>
				</el-table-column>
			</template>
		</el-table-column>
	</el-table>
</template>

<script>
export default {
	props: {
		height: {
			type: String,
		},
		list: {
			type: Array,
			required: true,
			default: () => [],
		},
		loading: {
			type: Boolean,
			default: false,
		},
		isShowSection: {
			type: Boolean,
			default: false,
		},
		tableTitle: {
			type: Array,
			required: true,
		},
	},
	methods: {
		// 点击行
		queryDetail(row) {
			this.$emit('queryRow', row);
		},
		// 表格选择(可单选,多选,全选)
		handleSelectionchange(row) {
			this.$emit('handleSelectionChange', row);
		},
	},
};
</script>

<style lang="scss" scoped></style>

3、Tree 树形控件

用清晰的层级结构展示信息,可展开或折叠。

<template>
  <el-dialog
    title="添加机构"
    custom-class="dialogStyle"
    :visible.sync="isTree"
    :close-on-click-model="false"
    @close="hideTree"
  >
    <div class="tree-content">
      <el-form ref="treeForm" @submit.native.prevent>
        <!-- 阻止单个输入框默认回车提交事件 -->
        <el-form-item label="">
          <el-input
            placeholder="请输入关键字"
            v-model="filterText"
            clearable
          ></el-input>
        </el-form-item>
      </el-form>
      <el-tree
        ref="tree"
        node-key="id"
        class="filter-tree"
        :data="treeData"
        :props="defaultProps"
        :show-checkbox="true"
        :height-current="true"
        :check-on-click-node="true"
        :default-expand-all="false"
        :filter-node-method="filterNode"
        :default-expanded-keys="checkedKeys"
        @check="handleCheckChange"
      >
      </el-tree>
    </div>
    <span slot="footer" class="dialog-footer">
      <el-button size="mini" @click="hideTree">取 消</el-button>
      <el-button size="small" type="primary" @click="addPage">确 定</el-button>
    </span>
  </el-dialog>
</template>

<script>
import { unique } from "../../utils/tools";

export default {
  name: "Tree",
  props: ["isTree", "treeData"],
  data() {
    return {
      checkedKeys: [],
      expandedKeys: [],
      defaultProps: {
        // 渲染规则
        children: "branchs",
        label: "name",
      },
      defaultKey: [2, 3], // 默认勾选 id=2,3 的节点, 若包含父节点, 子节点全部勾选
      filterText: "", // 查询机构的输入框
    };
  },
  watch: {
    filterText(val) {
      this.$refs.tree.filter(val);
    },
  },
  methods: {
    // 检索输入的组织信息
    filterNode(value, data) {
      // 需要设置 filter-node-method
      if (!value) return true;
      return data.name.indexOf(value) !== -1;
    },
    handleCheckChange(data) {
      console.log(data, "data");
    },
    // 清空时候打开的, 设置选中的 key 为空
    setCheckedKeys() {
      this.$refs.tree.setCheckedKeys([]);
    },
    // 设置选中的 Node
    setCheckedNodes(data) {
      this.$refs.tree.setCheckedNodes(data);
    },
    // 确定选中机构
    addPage() {
      let arrData = this.$refs.tree.getCheckedNodes(); // 获取选中的节点
      console.log(arrData, "arrData");
      let selectNodeIdArr = []; // 选中的节点的 id
      let addPageList = []; // 列表中需要展示的数据
      arrData.forEach((el) => {
        if (el.branchs && el.branchs.length == 0) {
          // 这种情况, 是对于有子级的父节点, 点了全选某个机构/部门, 给后台只传子节点的 id
          // 父节点的 id 过滤掉, 对于没有子节点的父级, 把父级 id 传过去
          selectNodeIdArr.push(el.id);
          // el.parentName = el.name;
          el.parentId = el.id;
          addPageList.push(el); // 把数组中没有子节点的都放到 addPageList , 便于页面显示
        } else {
          const { branchs } = el || [];
          branchs.forEach((item) => {
            // item.parentName = el.name;
            item.parentId = el.id;
          });
        }
      });
      let tableData = unique(addPageList);
      this.$emit("addPage", tableData);
      this.$emit("hideTree");
    },
    // 关闭弹窗
    hideTree() {
      this.$emit("hideTree");
    },
  },
};
</script>

<style lang="scss" scoped>
::v-deep {
  .el-from {
    > .el-form-item {
      margin-bottom: 0;
    }
  }
}
</style>

项目  /  src  /  utils  /

tools.js 文件

/**
 * 数组去重
 */
const unique = function (arr) {
  return arr.filter((item, index) => {
    return arr.indexOf(item, 0) === index;
  });
};

开始使用 :

项目  /  src  /  views  /  index.vue

<template>
  <div>
    <UnitTree
      ref="tree"
      v-show="isTree"
      :isTree="isTree"
      :treeData="treeData"
      @addPage="addPage"
      @hideTree="hideTree"
    ></UnitTree>
  </div>
</template>

<script>
import UnitTree from "../../components/common/unitTree.vue";
export default {
  components: {
    UnitTree, // 带搜索框的树组件
  },
  data() {
    return {
      isTree: false, // 控制树形结构弹窗的显示与否
      treeData: [], // 组织结构全部数据
      tableList: [], // 组织结构列表数据
      isgetTreeDodeFlage: false, // 第一次进来不让获取选中节点的方法
    };
  },
  methods: {
    addPage(data) {
      // 确定选中机构
      this.tableList = data;
      this.hideTree();
    },
    hideTree() {
      // 关闭选机构的弹窗
      this.isTree = false;
      this.isgetTreeDodeFlage = true;
    },
  },
};
</script>

4、Pagination 分页

当数据量过多时,使用分页分解数据。

开始封装 :

项目  /  src  /  components  /  page  /  index.vue

<!-- 功能 : 分页组件 -->
<template>
  <div class="common-page">
    <el-pagination
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      :page-size="pageSize"
      :page-sizes="pageSizes"
      :current-page="currentPage"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

<script>
export default {
  name: "Page",
  props: {
    currentPage: {
      type: Number,
    },
    total: {
      type: Number,
    },
    pageSize: {
      type: Number,
    },
    pageSizes: {
      type: Array,
    },
  },
  methods: {
    handleSizeChange(val) {
      this.$emit("handleSizeChange", val);
    },
    handleCurrentChange(val) {
      this.$emit("handleCurrentChange", val);
    },
  },
};
</script>

<style lang="scss" scoped>
.common-page {
  text-align: center;
  margin: 50px 0;
}
</style>

开始使用 :

项目  /  src  /  views  /  list  /  index.vue

<template>
  <div>
    <common-page
      layout="total, sizes, prev, pager, next, jumper"
      :total="pageData.total"
      :pageSize="pageData.pageSize"
      :pageSizes="[10, 20, 30, 40]"
      :currentPage="pageData.currentPage"
      @handleSizeChange="handleSizeChange"
      @handleCurrentChange="handleCurrentChange"
    ></common-page>
  </div>
</template>

<script>
import commonPage from "../../components/page/index.vue";
export default {
  components: {
    commonPage, // 分页组件
  },
  data() {
    return {
      pageData: {
        total: 0, // 总条数
        totalPage: 0, // 总页数
        pageSize: 20, // 一页显示的条数
        currentPage: 1, // 当前页数
      },
      tableList: [], // 后端返回来的表格数据
    };
  },
  methods: {
    handleSizeChange(val) {
      // 改变每页显示条数时执行(每页显示多少条)
      this.pageData.pageSize = val;
    },
    handleCurrentChange(val) {
      // 改变当前页数时执行(显示第几页的数据)
      this.pageData.currentPage = val;
      this.tableList = [];
    },
  },
};
</script>


第二种封装方法 :

<template>
	<div :class="{ hidden: hidden }" class="pagination-container">
		<el-pagination
			:total="total"
			:layout="layout"
			:background="background"
			:page-sizes="pageSizes"
			:page-size.sync="pageSize"
			:current-page.sync="currentPage"
			v-bind="$attrs"
			@size-change="handleSizeChange"
			@current-change="handleCurrentChange"
		/>
	</div>
</template>

<script>
export default {
	name: 'Pagination',
	props: {
		total: {
			required: true,
			type: Number,
		},
		page: {
			type: Number,
			default: 1,
		},
		limit: {
			type: Number,
			default: 10,
		},
		pageSizes: {
			type: Array,
			default() {
				return [10, 20, 30, 40, 50];
			},
		},
		layout: {
			type: String,
			default: 'total, sizes, prev, pager, next, jumper',
		},
		background: {
			type: Boolean,
			default: true,
		},
		sutoScroll: {
			type: Boolean,
			default: true,
		},
		hidden: {
			type: Boolean,
			default: false,
		},
	},
	computed: {
		currentPage: {
			get() {
				return this.page;
			},
			set(val) {
				this.$emit('update:page', val);
			},
		},
	},
	methods: {
		handleSizeChange(val) {
			this.$emit('pagination', { page: this.currentPage, limit: val });
			if (this.autoScroll) {
				scrollTo(0, 800);
			}
		},
		handleCurrentChange(val) {
			this.$emit('pagination', { page: val, limit: this.pageSize });
			if (this.autoScroll) {
				scrollTo(0, 800);
			}
		},
	},
};
</script>

<style lang="scss" scoped>
.pagination-container {
	// background: #fff;
	margin: 0;
	height: 30px;
	padding: 10px 16px;
	text-align: center;
}
.pagination-container.hidden {
	display: none;
}
</style>


使用方法 :

<template>
	<div>
		<CustomTable
			:headerList="headerList"
			:tableList="tableList"
			:indexN="indexN"
		></CustomTable>
		<Pagination
			v-show="queryParams.total > 0"
			layout="prev, pager, next,sizes, jumper"
			:total="queryParams.total"
			:page.sync="queryParams.page"
			:limit.sync="queryParams.rows"
			@pagination="getSelectAllUserList"
		></Pagination>
	</div>
</template>

<script>
import CustomTable from '@/components/CustomTable';
import Pagination from '@/components/Pagination';
import { selectAllUserApi } from '@api/hrdmeApi';

export default {
	components: { CustomTable, Pagination },
	data() {
		return {
			tableList: [], // 表格内数据
			indexN: null, // 表格前序列号
			// 查询参数
			queryParams: {
				total: 0, // 总条数
				page: 1, // 总页数
				rows: 10, // 一页显示的条数
				currentPage: 1, // 当前页数
				typeId: undefined,
			},
		};
	},
	methods: {
		// 列表页请求数据
		async getSelectAllUserList() {
			this.indexN = (this.queryParams.page - 1) * this.queryParams.rows;
			let obj = {
				branchId: this.branchId,
				page: this.queryParams.page,
				pageSize: this.queryParams.rows, // 一页显示的条数
			};
			let res = (await selectAllUserApi(obj)) || {};
			const { code, data } = res;
			if (code == 200) {
				this.tableList = data.rows;
				this.queryParams.total = data.total;
			}
		},
	},
};
</script>

5、Dialog 对话框

在保留当前页面状态的情况下,告知用户并承载相关操作。

开始封装 :

项目  /  src  /  components  /  common  /  publicDialog.vue

<!--
 * @Author: xiaohuilang
 * @Date: 2022-05-01
 * @LastEditors: xiaohuilang
 * @LastEditTime: 2022-05-01
-->
<template>
  <el-dialog
    class="app-dialog"
    :title="title"
    :visible.sync="showDialogVisible"
    :width="width"
    :before-close="dialogClose"
    :top="top"
    :close-on-click-modal="false"
    :lock-scroll="lockScroll"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <!-- Dialog 标题, 可通过具名 slot 传入 -->
    <template #title>
      <slot name="title"></slot>
    </template>
    <!-- Dialog 中间内容部分 -->
    <div class="app-dialog-container">
      <slot name="dialog-container" />
    </div>
    <!-- Dialog 底部按钮部分 -->
    <div v-show="showButton" class="t-right app-dialog-button">
      <slot name="dialog-button">
        <el-button
          type="primary"
          size="medium"
          :disabled="disabled"
          @click="handleSave"
          v-loading="isLoading"
          element-loading-spinner="el-icon-loading"
          element-loading-background="rgba(0,0,0,0.6)"
          >{{ saveText }}</el-button
        >
        <el-button size="medium" :disabled="disabled" @click="handleCancel">{{
          cancelText
        }}</el-button>
      </slot>
    </div>
  </el-dialog>
</template>

<script>
export default {
  name: "Dialog",
  props: {
    visible: {
      // 控制是否显示 Dialog
      type: Boolean,
      required: true,
      default: false,
    },
    title: {
      // Dialog 标题
      type: String,
      default: "",
    },
    width: {
      // Dialog 宽度
      type: String,
      default: "60rem",
    },
    top: {
      // Dialog 中 margin-top 值
      type: String,
      default: "40vh",
    },
    showButton: {
      // v-show 是否显示按钮
      type: Boolean,
      default: true,
    },
    disabled: {
      // disabled 是否禁用按钮
      type: Boolean,
      default: false,
    },
    saveText: {
      type: String,
      default: "确定",
    },
    cancelText: {
      type: String,
      default: "取消",
    },
    lockScroll: {
      // 是否在 Dialog 出现时将 body 滚动锁定
      type: Boolean,
      default: true,
    },
    isLoading: {
      type: Boolean,
      default: false,
    },
  },
  computed: {
    showDialogVisible: {
      get() {
        return this.visible;
      },
      set(val) {
        this.$emit("update:visible", val);
      },
    },
  },
  methods: {
    dialogClose() {
      this.$emit("dialogClose", true);
    },
    handleSave() {
      this.$emit("handleSave");
    },
    handleCancel() {
      this.$emit("handleCancel");
    },
  },
};
</script>

<style lang="scss" scoped>
::v-deep {
  .el-dialog {
    margin: 0 auto;
    border-radius: 8px;
    .el-dialog__header {
      background-color: #f2f2f2;
      padding: 10px 20px 10px 20px;
      border-radius: 8px;
    }
    .el-dialog__body {
      padding: 5px 10px;
    }
    .el-loading-spinner {
      line-height: 40px;
    }
  }
}
</style>

开始使用 :

项目  /  src  /  views  /  submit  /  index.vue

<template>
  <div class="su-submit">
    <!-- 选择用户组 -->
    <div class="user-group-box">
      <h3>选择用户组</h3>
      <div class="user-groups">
        <el-button type="primary" plain size="small" @click="openUserAlert"
          >添加用户组</el-button
        >
        <el-button type="danger" plain size="small" @click="userGroupDelete"
          >删除用户组</el-button
        >
      </div>
      <!-- 添加用户组 Dialog -->
      <public-dialog
        :title="userTitle"
        :visible="dialogVisible"
        :width="userWidth"
        @dialogClose="dialogClose"
        @handleSave="dialogSave"
        @handleCancel="dialogCancel"
      >
        <!-- Dialog 标题, 可通过具名 slot 传入 -->
        <template #title>
          <!-- 新建用户分组 -->
        </template>
        <template #dialog-container>
          <!-- 新建用户分组 -->
          <el-input
            size="small"
            class="userInput"
            v-model="userInput"
            placeholder="定义分组名称"
          >
            <el-button size="small" style="margin-left: 20px"
              >上传分组文件</el-button
            >
            <el-button size="small">下载分组模板</el-button>
          </el-input>
        </template>
      </public-dialog>
    </div>
  </div>
</template>

<script>
import publicDialog from "@/components/common/publicDialog.vue";
export default {
  components: {
    publicDialog, // 公共弹框组件
  },
  data() {
    return {
      userTitle: "新建用户分组", // Dialog 的标题
      dialogVisible: false, // 控制添加用户组的 Dialog 显示
      userWidth: "30%", // Dialog 的宽度
      userInput: "", // 定义分组名称绑定值
    };
  },
  methods: {
    dialogClose() {
      // 右上角关闭按钮
      this.$confirm("确认关闭?")
        .then((_) => {
          this.dialogVisible = false;
        })
        .catch((_) => {});
    },
    dialogSave() {
      // 确定保存
      this.dialogVisible = false;
    },
    dialogCancel() {
      // 取消保存
      this.dialogVisible = false;
    },
  },
};
</script>

6、ECharts 图表

一个基于 JavaScript 的开源可视化图表库


Apache ECharts

开始封装 :

项目  /  src  /  components  /  echarts  /  echartsChart.vue

<!-- 
* 功能:echarts 图表
* 日期:2022-11
* 作者:小灰狼
-->
<template>
	<div>
		<div id="id" class="myChart" :style="setStyle"></div>
	</div>
</template>

<script>
import * as echarts from 'echarts';

export default {
	name: 'echartsChart',
	props: {
		id: {
			type: String, // 类型
			default: null, // 默认值
		},
		options: {
			type: Object, // 类型
			default: null,
		},
		setStyle: {
			type: Object, // 类型
			default: null,
		},
	},
	data() {
		return {
			myChart: null,
			myChartStyle: { height: '300px' }, // 图标样式
			tooltip: {
				trigger: 'item',
			},
			legend: {
				origin: 'vertical',
				left: 'left',
			},
		};
	},
	watch: {
		option: {
			handler(newValue) {
				this.$nextTick(() => {
					if (document.getElementById(this.id)) {
						if (!this.myChart) {
							this.myChart = echarts.init(
								document.getElementById(this.id)
							);
							// 新增配置 Echarts 图表的自带 showLoading 方法
							this.myChart.showLoading({
								text: '加载中...',
							});
							this.myChart.setOption(newValue, true);
							this.myChart.hideLoading(); // 关闭Echarts的Loading
						}
					}
				});
			},
		},
		deep: true,
	},
	mounted() {
		this.initChart(this.id, this.options);
		window.addEventListener('resize', function () {
			if (this.myChart) {
				this.myChart.resize();
			}
		});
	},
	methods: {
		initChart(id, options) {
			if (document.getElementById(id)) {
				this.myChart = echarts.init(document.getElementById(id));
				// 新增配置 Echarts 图表的自带 showLoading 方法
				this.myChart.showLoading({
					text: '加载中...',
				});
				// this.myChart.setOption(options, true);
				setTimeout(() => {
					this.myChart.setOption(options, true);
					this.myChart.hideLoading(); // 关闭Echarts的Loading
				}, 100);
			}
		},
		// 解决Echarts页面切换卡顿的问题
		beforeDestroy() {
			this.myChart.clear();
		},
	},
};
</script>

<style lang="scss" scoped>
// .myChart {
// 	width: 95%;
// 	height: 400px;
// }
</style>

7、wangEditor 富文本编辑器

开源 Web 富文本编辑器,开箱即用,配置简单


wangEditor


安装

wangEditor

npm 安装



npm i wangeditor --save

注:

wangeditor

全小写

package.json 文件

“wangeditor”: “^4.7.11”


封装 wangEditor 组件(wangEditor.vue)

src  /  components  /  wangEditor  /  index.vue

<!-- 组件功能:wangEditor 富文本编辑器 -->
<template lang="html">
  <div class="editor">
    <div ref="toolbar" class="toolbar"></div>
    <div ref="editor" class="text"></div>
  </div>
</template>

<script>
import E from "wangeditor";
import fileMenu from "./fileMenu";

export default {
  name: "editoritem",
  data() {
    return {
      editor: "",
      info_: null,
    };
  },
  model: {
    prop: "value",
    event: "change",
  },
  props: {
    value: {
      type: String,
      default: "",
    },
    isClear: {
      type: Boolean,
      default: false,
    },
    quTitle: {
      type: String,
      default: "",
    },
  },
  watch: {
    isClear(val) {
      // 触发清除文本域内容
      if (val) {
        this.editor.txt.clear();
        this.info_ = null;
      }
    },
    value: function (value) {
      if (value !== this.editor.txt.html()) {
        this.editor.txt.html(this.value);
      }
    },
    // value 为编辑框输入的内容,这里我监听了一下值,当父组件调用得时候,如果给value赋值了,子组件将会显示父组件赋给的值
  },
  mounted() {
    this.seteditor();
    this.editor.txt.html(this.quTitle);
    // this.editor.unFullScreen()
  },
  methods: {
    seteditor() {
      // http://192.168.2.125:8080/admin/storage/create
      this.editor = new E(this.$refs.toolbar, this.$refs.editor);
      this.editor.config.uploadImgShowBase64 = false; // base 64 存储图片
      // this.editor.config.uploadImgServer = 'http://baidu.com' // 配置服务器地址, 这个是处理图片上传问题的
      // this.editor.config.uploadImgServer = Settings.apiUrl + "/api/CoreService/File/UploadFile"; // 配置服务器端地址
      this.editor.config.uploadImgHeaders = {}; // 自定义 header
      this.editor.config.uploadFileName = "file"; // 后端接受上传文件的参数名
      this.editor.config.uploadImgMaxSize = 6 * 1024 * 1024; // 将图片大小限制为 6M
      this.editor.config.uploadImgMaxLength = 6; // 限制一次最多上传 6 张图片
      this.editor.config.uploadImgTimeout = 3 * 60 * 1000; // 设置超时时间

      // 配置菜单
      this.editor.config.menus = [
        "head", // 标题
        "bold", // 粗体
        "fontSize", // 字号
        "fontName", // 字体
        "italic", // 斜体
        "underline", // 下划线
        "strikeThrough", // 删除线
        "foreColor", // 文字颜色
        "backColor", // 背景颜色
        "link", // 插入链接
        "list", // 列表
        "justify", // 对齐方式
        "quote", // 引用
        "emoticon", // 表情
        "image", // 插入图片
        "emoticon", // 插入表情
        "table", // 表格
        "video", // 插入视频
        "code", // 插入代码
        "undo", // 撤销
        "redo", // 重复
        "fullscreen", // 全屏
        // 附件 , 多图上传
      ];
      // 加上以下代码, 上传视频的 tab 和图标显示出来
      this.editor.config.uploadVideoServer = "/api/upload-video";
      this.editor.config.showFullScreen = true;
      this.editor.config.showLinkImg = false; // 隐藏网络图片的 tab
      this.editor.config.showLinkImgAlt = false; // 配置 alt 选项
      this.editor.config.showLinkImgHref = false; // 配置超链接
      // this.editor.fullScreen()
      // this.editor.unFullScreen()

      // 上传图片
      this.editor.config.uploadImgHooks = {
        fail: (xhr, editor, result) => {
          // 插入图片失败回调
        },
        success: (xhr, editor, result) => {
          // 图片上传成功回调
          if (result.assertion) {
            console.log(result.message);
          }
        },
        timeout: (xhr, editor) => {
          // 网络超时的回调
        },
        error: (xhr, editor) => {
          // 图片上传错误的回调
        },
        customInsert: (insertImg, result, editor) => {
          // 图片上传成功,插入图片的回调
          // result: 上传图片成功时返回的数据,打印返回格式是: data:[{url:"路径的形式"},...]
          // console.log(result.data[0].url)
          // insertImg()为插入图片的函数
          // 循环插入图片
          const { code, data, msg } = result;
          if (code == 0) {
            insertImg("/CMMS" + data.image_url);
          } else {
            message.error(msg);
          }
          // let url = "http://otp.cdinfotech" + result.url;
          // let url = Settings.apiUrl + ":1889/" + result.objectEntity;
          // insertImg(url);
        },
      };

      // 自己实现上传图片
      this.editor.config.customUploadImg = function (files, insert) {
        console.log(files, "files-----------");
        // files 是 input 中选中的文件列表
        // insert 是获取图片 url 后 , 插入到编辑器的方法
        // let formData = new FormData()
        // for (let i = 0; i < files.length; i++) {
        //   formData.append('file', files[i], files[i].name)  // 多张图片放进一个 formData
        // }
        // insert(imgUrl)
      };

      // 上传视频 server接口返回格式, 很重要!
      // 接口返回 application/json 格式, 格式要求如下 :
      /*
        {
          // error 即错误代码, 0: 表示没有错误
          // 如果有错误, error != 0, 可通过下文中的监听函数 fail 拿到该错误进行自定义处理
          "error": 0
          // data 是一个对象, 返回视频的线上地址
          "data": {
            "url": "视频1地址"
          }
        }
      */

      this.editor.config.onchange = (html) => {
        let data = this.editor.txt.getJSon;
        console.log(data, "data---------");
        this.info_ = html; // 绑定当前逐渐地值
        this.$emit("change", this.info_); // 将内容同步到父组件中
      };
      this.editor.config.onchangeTimeout = 500; // 修改为 500ms

      // 用户选取操作自动触发
      this.editor.config.onSelectionChange = (newSelection) => {
        console.log("onSelectionChange", newSelection);
      };

      // 创建富文本编辑器
      this.editor.create();
      fileMenu(this.editor, this.$refs.toolbar);
    },
    openFileAlert(value) {
      this.showEnclosureAlert = value;
    },
    // 保存
    advancedSave() {
      console.log("附件保存");
    },
  },
};
</script>

<style lang="css">
.editor {
  width: 100%;
  margin: 0 auto;
  position: relative;
  z-index: 0;
}
.toolbar {
  border: 1px solid #ccc;
}
.text {
  border: 1px solid #ccc;
  min-height: 500px;
}
.upload-file-input {
  width: 40px;
  height: 40px;
  position: absolute;
  top: 50%;
  left: 0;
  margin-top: -20px;
  opacity: 0;
}
.w-e-toolbar .w-e-menu .upload-file-span {
  width: 100px !important;
}
.attachAlert {
  display: none;
}
</style>


this.editor.customConfig.uploadImgServer = Settings.apiUrl + ‘/api/CoreService/File/UploadFile’  // 配置服务器端地址

,这个是处理图片上传问题的

src  /  components  /  wangEditor  /

fileMenu.js

/**
 * editor: wangEditor 的实例
 * editorSelector: wangEditor 挂载点的节点
 * options: 一些配置
 */
import uploadFile from "./uploadFile";
import vue from "vue";
export default (editor, editorSelector, options) => {
  editor.fileMenu = {
    init: function (editor, editorSelector) {
      const div = document.createElement("div");
      div.className = "w-e-toolbar w-e-menu";
      div.style.zIndex = 10001;
      div.setAttribute("data-title", "附件");
      const rdn = new Date().getTime();
      div.onclick = function (e) {
        if (e.stopPropagation) {
          e.stopPropagation();
        } else {
          e.cancelBubble = true;
        }
        // document.getElementById(`up-${rdn}`).click()
      };
      const input = document.createElement("input");
      input.type = "file";
      input.name = "file";
      input.id = `up-${rdn}`;
      input.className = "upload-file-input";
      div.innerHTML = `<i class="el-icon-folder-add"></i>`;
      div.appendChild(input);
      editorSelector.getElementsByClassName("w-e-toolbar")[0].appendChild(div);
      input.onchange = (e) => {
        console.log(e, "change");
        // 使用 uploadFile 上传文件
        // uploadFile(e.target.files, {
        //   onOk: (data) => {
        //     console.log(data);
        //     // 可以使用 editor.txt.html(data) 进行更新
        //   },
        //   onFail: (err) => {
        //     console.log(err);
        //   },
        //   onprogress: (percent) => {
        //     console.log(percent);
        //   },
        // });
        uploadFile(e.target.files, {
          onOk: (data) => {
            // console.log(data, '文件上传');
            const { code, result } = data || {};
            if (code == 0) {
              e.target.value = ""; // 解决 input onchange 事件第二次调不起的解决方法
              let fileName = result.fileName.toLowerCase();
              let fileIconUrl = ""; // 存放附件前面小 icon 地址的变量
              let _index = fileName.lastIndexOf(".") + 1;
              let fileDot = fileName.substring(_index, fileName.length);
              if (fileDot == "doc") {
                fileIconUrl =
                  "http://83.12.234.567:8080/icon_doc.gif";
              } else if (fileDot == "txt") {
                fileIconUrl =
                  "http://83.12.234.567:8080/icon_doc.gif";
              } else if (fileDot == "xls" || fileDot == "xlsx") {
                fileIconUrl =
                  "http://83.12.234.567:8080/icon_doc.gif";
              }
              editor.txt.append(
                `<p class="insertFileContent"><img class="insertFileIcon" src=${fileIconUrl} alt="" /><a href="#" onclick="window.location.href="/ic/329.doc"" title=${fileName}>${fileName}</a></p>`
              );
            }
          },
          onFail: (err) => {
            console.log(err);
          },
          onprogress: (percent) => {
            console.log(percent);
          },
        });
      };
    },
  };
  // 创建完之后立即实例化
  editor.fileMenu.init(editor, editorSelector);
};

src  /  components  /  wangEditor  /

uploadFile.js

import { message } from "element-ui";
import { uploadFileApi } from "@/api/listApi";

function uploadFile(files, options) {
  if (!files || !files.length) {
    return;
  }
  // let uploadFileServer = commonApi.imgUploadApi; // 文件上传地址
  let uploadFileServer = uploadFileApi; // 文件上传地址
  const maxSize = 100 * 1024 * 1024; // 100M
  const maxSizeM = maxSize / 1000 / 1000;
  const maxLength = 1;
  const uploadFileName = "file";
  const uploadFileParams = {};
  const uploadFileParamsWithUrl = {};
  const timeout = 5 * 60 * 1000; // 5 min
  // ---------------------- 验证文件信息 ---------------------------
  const resultFiles = [];
  const errInfo = [];
  for (let file of files) {
    const name = file.name;
    const size = file.size;
    // chrome 低版本 name ===== undefined
    if (!name || !size) return;
    if (maxSize < size) {
      // 上传附件过大
      errInfo.push("\u3010" + name + "\u3011\u5927\u4E8E" + maxSizeM + "M");
      return;
    }
    // 验证通过的加入结果列表
    resultFiles.push(file);
  }
  // 抛出验证信息
  if (errInfo.length) {
    this._alert("附件验证未通过, \n" + errInfo.join("\n"));
    return;
  }
  if (resultFiles.length > maxLength) {
    this._alert("一次最多上传" + maxLength + "个文件");
    return;
  }
  // ----------------------- 自定义上传 ----------------------
  const formdata = new FormData();
  for (let file of resultFiles) {
    const name = uploadFileName || file.name;
    formdata.append("file", file);
    // formdata.append(name, file);
  }
  // 附件上传
  uploadFileApi(fromdata)
    .then((res) => {
      options.onOk && options.onOk(res);
    })
    .catch((err) => {
      options.onFail && options.onFail(err);
    });

  // // ----------------------- 上传附件 -------------------------
  // if (uploadFileServer && typeof uploadFileServer === "string") {
  //   for (key in uploadFileParams) {
  //     val = encodeURIComponent(uploadFileParams[val]);
  //     formdata.append(key, val);
  //   }
  // }

  // // 定义 xhr
  // const xhr = new XMLHttpRequest();
  // xhr.open("POST", uploadFileServer);
  // // 设置超时
  // xhr.timeout = timeout;
  // xhr.ontimeout = function () {
  //   if (options.timeout && typeof options.timeout === "function") {
  //     options.timeout(xhr, editor);
  //   }
  //   message.error("上传附件超时");
  // };
  // // 监控 progress
  // if (xhr.upload) {
  //   xhr.upload.onprogress = function (e) {
  //     let percent = void 0;
  //     // 进度条
  //     if (e.lengthComputable) {
  //       percent = e.loaded / e.total;
  //       if (options.onprogress && typeof options.onprogress === "function") {
  //         options.onprogress(percent);
  //       }
  //     }
  //   };
  // }
  // // 返回数据
  // xhr.onreadystatechange = function () {
  //   let result = void 0;
  //   if (xhr.status < 200 || xhr.status >= 300) {
  //     if (options.onFail && typeof options.onprogress === "function") {
  //       options.onFail(xhr, editor);
  //     }
  //     return;
  //   }
  //   result = xhr.responseText;
  //   if (
  //     (typeof result === "undefined" ? "undefined" : typeof result) !== "object"
  //   ) {
  //     try {
  //       result = JSON.parse(result);
  //     } catch (ex) {
  //       if (options.onFail && typeof options.onFail === "function") {
  //         options.onFail(xhr, editor, result);
  //       }
  //       return;
  //     }
  //   }
  //   const data = result || [];
  //   if (data.code == 0) {
  //     options.onOk && options.onOk(data.data);
  //   }
  // };
  // // 自定义 headers
  // for (let key in uploadFileHeaders) {
  //   xhr.setRequestHeader(key, uploadFileHeaders[key]);
  // }
  // // 跨域传 token
  // xhr.widthCredentials = false;
  // // 发送请求
  // xhr.send(formdata);
}

export default uploadFile;


导入并引用组件

src  /  views  /  edit  /  components  /

parameter.vue

<template>
  <div>
    <editorBar
      :isClear="isClear"
      :quTitle.sync="titleName"
      @change="editorTitleChange"
    ></editorBar>
  </div>
</template>
 
<script>
import editorBar from "../../../components/wangEditor/index.vue";
 
export default {
  components: {
    editorBar, // 注册 富文本编辑器 组件
  },
  data() {
    return {
      isClear: false,
      titleName: "", // 标题
      tempTitle: "",
    };
  },
  methods: {
    // 标题高级编辑器change事件
    editorTitleChange(val) {
      this.tempTitle = val;
    },
  },
};
</script>


展示效果



版权声明:本文为weixin_58099903原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。