目录
整理一下自己在 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 的开源可视化图表库
开始封装 :
项目 / 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
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>
展示效果