# 数据标红功能的实现
在数据资源汇聚平台中, 有一个标红显示字段的功能, 这个功能的需求是, 在表格内每一cell都进行一次文本过滤, 将匹配的字符串(精确匹配)的进行 标红 / 标红并显示星号 **
这个功能其实是对标签的二次渲染, 因为初次渲染是直接由数据驱动, 数据本身是不具备标红的信息的, 二次渲染比较好的方法就是通过自定义指令来实现
# 前置准备
在了解如何实现之前, 如果还不太熟悉自定义指令, 可以看看这里: 自定义指令, 也可以去vue官网查看更详细的指令教程
前置知识也准备好了, 回到正题. 正如上述所说, 数据本身是不具备标红信息的, 它只是纯粹的数据, 这时需要额外的 数据来源 来确定需要标红的字符串数组(兼容多个词语的情况), 然后我们再来创建一些用于标红的源数据, 大致就是这样:
data() {
return {
redTags: ['这是', '标红', '词语'],
sourceData: [
{
name: '这是源数据',
info: '看看标红不'
},
{
name: '这是不是词语',
info: '看看省略不'
},
{
name: '这是短语',
info: '看看省略不'
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
然后再注册好指令:
directives: {
redTag: {
bind(el, binding, vNode) {
return binding.value // 这里先直接返回, 我们下一步再来完善逻辑
}
}
}
2
3
4
5
6
7
对比数据准备好之后再来看看使用指令时的参数传递, 指令是建立在页面标签的基础之上的, 所以在template
中需要传递足够的信息来让指令判断是否需要标红
<p v-for="item in sourceData" style="color: blue;" :key="item.name" v-redTag="item.name"></p>
让我们先把这些数据渲染出来吧:
# 初步校验
在本部分要校验两个东西: 类型, 数据是否为空, 还有一个是数据否出现在标红列表中的校验, 我们在下一小节来讨论
这两样有一样不满足时, 直接返回原值就行了
校验的方法可以访问这里[判断数据类型]](./OriginalJavaScript.md#对象), 下面直接贴代码了
bind(el, binding, vNode) {
let getType = Object.prototype.toString
// 类型校验
if(getType.call(binding.value) != '[object Number]' && getType.call(binding.value) != '[object String]') {
el.innerHTML = binding.value
return false
}
// 判断非空
const redTagsLength = vnode.context.redTags.length
if(!binding.value || && redTagsLength == 0) {
// 这里同时处理了一下 binding.value === 0 的情况 因为 (0 || '') >> 输出 >> ('')
el.innerHTML = `${binding.value == 0 ? '0' : (binding.value || '')}` || ''
return binding.value
}
el.innerHTML = binding.value + ' 初步校验成功的'
// ... to be continue
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
为什么数字类型也要校验
因为数字类型(Number
)也将会进行标红逻辑, 其显示在HTML中与String
无异, 所以其也应进入校验的逻辑
其余数据类型其实也不必太过担心显示到页面上会有多可怕, 因为只要数据通过JSON传递自动转换成String
/Number
/null
/Object
类型, 如果无法解析时, 会被JSON自动舍弃, 如果觉得不放心时可以执行一次JSON.parse(JSON.stringify())
JSON.parse(JSON.stringify({
a: NaN,
b: (a) => {return 'a'},
c: new Date(),
d: undefined,
e: null,
f: new Object(),
g: new Array()
}))
// >> 输出 >> {a: null, c: '2021-03-17T06:58:48.831Z', e: null, f: {}, g: []}
2
3
4
5
6
7
8
9
10
经过上一步之后, 现在仍在执行的函数内肯定是一个非空字符串啦
实现标红效果的最基本逻辑就是对比字符串, 如果有匹配项则进行标红处理, 当不存在任何标红信息时直接跳过所有逻辑
# 寻找位置
遍历标红词语redTags
, 找出其在源数据字符串中出现的位置和其本身的长度, 这里用while()
累加查找
// 记录原始字符串, 避免数字类型导致对比出错
let valueString = `${binding.value}`
let redMatched = []
vnode.context.redTags.forEach(word => {
// 轮训查找字符串 直到字符串结束
let index = 0; //开始的位置
while ((index = valueString.indexOf(word, index)) != -1) {
// 如果是-1情况,说明找完了
redMatched.push({
index: index,
length: word.length
})
index += word.length;
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
此时的redMatched
的三次输出分别为
[{index: 0, length: 2}] // '这是'
[{index: 0, length: 2}, {index: 2, length: 2}] // '这是', '词语'
[{index: 0, length: 2}] // '这是'
2
3
# 拆分vnode
有了上一步找出的标红具体的顺序要想实现标红就好办了, 当然纯粹的#text
节点当然无法表现出红色的信息, 所以我们借助HTML的特性, 给其添加color
属性, 在需要的节点改变color
的值即可
先将源数据binding.value
逐个拆分成由单个字符标签代码片段
let valueArray = []
if(valueString.length > 0) {
valueArray = valueString.split('').map(value => {
return `<span style="color: blue;">${value}</span>`
})
} else {
valueArray = [valueString]
}
2
3
4
5
6
7
8
注意
单个字符也需要将其处理成数组哦!
'这是源数据'
拆完之后的valueArray
变成这样:
[
'<span style="color: blue;">这</span>',
'<span style="color: blue;">是</span>',
'<span style="color: blue;">源</span>',
'<span style="color: blue;">数</span>',
'<span style="color: blue;">据</span>'
]
2
3
4
5
6
7
# 替换color属性
所有的准备工作已就绪, 然后就是根据redMatched
的信息将拆分好的数组进行重组
resultArray = valueArray.map((value, valueIndex) => {
let flag = false
flag = redMatched.some(matchInfo => {
return (valueIndex >= matchInfo.index
&& valueIndex < matchInfo.index + matchInfo.length)
})
return flag ? value.replace('blue' , 'red') : value // 替换颜色
})
2
3
4
5
6
7
8
此时resultArray
为:
[
'<span style="color: red;">这</span>',
'<span style="color: red;">是</span>',
'<span style="color: blue;">源</span>',
'<span style="color: blue;">数</span>',
'<span style="color: blue;">据</span>'
]
2
3
4
5
6
7
# 重组vnode
最后一步啦, 现在数组resultArray
组合起来即可啦, 按照此种方法处理后无须担心数据重复处理, 毕竟已经被标红的内容再次执行replace()
也会是无事发生
el.innerHTML = resultArray.join('')
然后来看看执行的结果吧(可以打开控制台检查每个字符)~
# 完整代码
其中参数部分本例中暂未使用, 写出来是作为以后可以参考写法
提示
在使用动态参数v-directive:[arg]
时, 如果需要自己组合数据当做参数时, 注意不要使用空格:
<!-- 错误写法-->
<div v-directive:[{ a: scope.row, b: scope }]="scope.row.data"></div>
<!-- 正确写法-->
<div v-directive:[{a:scope.row,b:scope}]="scope.row.data"></div>
2
3
4
5
同时arg中的值全部为表达式, 传入字符串是不行的哦
<template>
<div>
<p v-for="item in sourceData" style="color: blue;" :key="item.name" v-redTag:[{param:item.info,params:item.name}]="item.name"></p>
</div>
</template>
<script>
export default {
directives: {
redTag: {
bind: function(el, binding, vnode) {
let getType = Object.prototype.toString
// 类型校验
if(getType.call(binding.value) != '[object Number]' && getType.call(binding.value) != '[object String]') {
el.innerHTML = binding.value
return false
}
// 判断非空
const redTagsLength = vnode.context.redTags.length
if(!binding.value || redTagsLength == 0) {
// 这里同时处理了一下 binding.value === 0 的情况 因为 (0 || '') >> 输出 >> ('')
el.innerHTML = `${binding.value === 0 ? '0' : (binding.value || '')}` || ''
return binding.value
}
// 记录原始字符串, 避免数字类型导致对比出错
let valueString = `${binding.value}`
let valueArray = []
if(valueString.length > 0) {
valueArray = valueString.split('').map(value => {
return `<span style="color: blue;">${value}</span>`
})
} else {
valueArray = [valueString]
}
// 开始记录当前字符串内是否有标红词语出现, 出现则记录其开始index位置以及该标红词的长度length
let redMatched = []
vnode.context.redTags.forEach(word => {
// 轮训查找字符串 直到字符串结束
let index = 0; //开始的位置
while ((index = valueString.indexOf(word, index)) != -1) {
// 如果是-1情况,说明找完了
redMatched.push({
index: index,
length: word.length
})
index += word.length;
}
})
let resultArray = []
resultArray = valueArray.map((value, valueIndex) => {
let flag = false
flag = redMatched.some(matchInfo => {
return (valueIndex >= matchInfo.index && valueIndex < matchInfo.index + matchInfo.length)
})
return flag ? value.replace('blue' , 'red') : value // 替换颜色
})
el.innerHTML = resultArray.join('')
}
}
},
data() {
return {
redTags: ['这是', '标红', '词语'],
sourceData: [
{
name: '这是源数据',
info: '看看标红不'
},
{
name: '这是不是词语',
info: '看看省略不'
},
{
name: '这是短语',
info: '看看省略不'
}
]
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# data的定义规范
每个页面中通常都需要定义大量的data数据, 页面越复杂data自然越臃肿:
data() {
return {
name: '',
gender: 1,
age: 1
// 通常是同区块的数据放置在一起 不同的区块用换行隔开
dialogOneVisible: false,
dialoOneTitle: '标题1',
dialoOneContent: '内容1',
dialogTwoVisible: false,
dialogTwoTitle: '标题2',
dialogTwoContent: '内容2',
form: {
keyword: '',
type: 1,
},
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
为了更直观的定义data
, 可以将不同区域的数据直接放置于不同的对象下, 方便统一维护, 同时也避免了在同一根对象下命名太冗长的问题:
data() {
return {
userInfo: {
name: '',
gender: 1,
age: 1
},
form: { ... }, // 置于上方
dialogOneInfo: {
visible: false,
title: '标题1',
content: '内容1',
}
dialogTwoInfo: {
visible: false,
title: '标题2',
content: '内容2',
}
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
参照流式文档的模式, data
数据在定义时可以遵循页面的文档流顺序, 方便阅读与对应
例如 <dialog>
组件一般直接放置于文档流最末尾, 表格所对应的查询表单<form>
通常在文档流靠前的位置等. 实际情况当然遵循页面实际的文档流顺序来调整
除此之外, 当出现复杂对话框内容却因业务特殊(无处复用)而无需拆分组件时, 同样可以按照上述的规则进行嵌套
data() {
return {
userInfo: { ... },
form: { ... },
dialogInfo: {
visible: false,
title: '新建对话框'
},
complexDialogInfo: {
visible: false,
title: '复杂对话框',
searchForm: { ... }, // 对话框-表格 对应的筛选条件form
tableData: [], // 对话框-表格 数据
pageInfo: { ... } // 对话框-表格 分页信息
}
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# data 通用命名思路
经过上面的改造, data()
内目前算是规整了许多, 但你可能也经常遇到如下这种情况: 新的A页面跟已写好的B页面长得几乎一样, 功能也非常类似就是换了一些文本名称, 然后你就开始直接Ctrl+C
+ Ctrl+V
复制A页面
但是复制过去的页面有非常多琐碎的问题: 最恶心的莫过于跟A页面内与其功能指代高度耦合的字段名, 为了代码可读性这些肯定是要改的, 所以为了尽量避免这个问题, data()
(甚至methods
)都可以采用尽量通用的方式来命名, 这样复用代码也会更快乐, 那么来试试下面的命名规则:
data() {
return {
searchForm: { // -- 泛指页面内的主要查询表单
name: '',
// ...表单里面的字段无法做到通用化...
}
tableData: [], // -- 泛指页面内的主要table数据
pageInfo: { // -- 泛指页面内的主要table对应的分页信息
pageNo: 1,
pageSize: 10,
total: 999 // 这三个字段可以做到通用
}
}
},
created() {
this.findPreData() // 也可以放在mounted
},
mounted() {
this.findTableList()
},
methods: {
/**
* 查询页面所需前置数据(仅限在页面初始就进行查询的前置数据)
*/
findPreData() { ... },
/**
* 查询主要列表
*/
findTableList() { ... },
/**
* 查询列表项详情
*/
findDetail(id) { ... },
/**
* 提交主要表单
*/
submitForm() { ... },
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
上述例子中主要都围绕着, 代码命名空间解耦页面功能来展开, 越低的耦合度意味着更高的复用度~
# 文件的统一上传
通常的代码中, 我们通过<el-upload>
组件上传文件时, 总是通过action
属性直接上传, 这样在上传数量较多的内容时会多次反复调用上传接口, 而且还会导致在填写表单时, 还未进行保存就已经上传, 导致上传了很多无用数据
于是我们来自定义一下这个上传, 通过http-request
属性来重写
<template>
<el-upload
action="/api/v1/fs/store"
multiple
:limit="1000"
:on-change="handleChanged"
:http-request="handleUpload">
<el-button>点击上传</el-button>
</el-upload>
<el-button @click="handleSubmit">点击提交</el-button>
</template>
<script>
// ...
methods: {
handleChanged(file, fileList) {
this.fileList = fileList
},
handleUpload(data) {
// 用此方法阻止默认的action提交
console.log(this.fileList)
},
// 在需要的时候统一再提交
handleSubmit(data) {
// 校验文件是否过大, 注意fileList中的文件是在raw字段内
const isLt100M = this.fileList.every((file) => file.raw.size / 1024 / 1024 < 100);
if (!isLt100M) {
this.$message.error('请检查,上传文件大小不能超过100MB!');
} else {
let formData = new FormData()
this.fileList.forEach(item => {
// 参数名以后台需求为准
formData.append('files', item.raw)
})
this.$http.uploadStoreImgs(formData).then((res) => {
if (res.status == 200) {
this.$message.success('上传成功');
}
})
.finally(() => {
this.loading = false;
});
}
},
}
// ...
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48