前言
咱们上回书说到,咱们利用声网和腾讯云实现了语音会议的实时语音识别及会议数据的录入,并且使用markjs工具实现了会议标注的功能,详情点这里。最近产品又要加一个已标注会议的重新编辑功能,这个功能在项目开始的时候我就有提到过,大致的需求如下图:
正文
1.回显时的json结构
回显时的json结构(即标注会议时保存的taskJson)大致如下:
对于这段json需要注意的是,tasks是一只在变化的,随着我们不断push标签,tasks会一直变化。举个例子,假如我们dialogue有三句话,我没有标注第一句话,直接操作第二句话,添加了一个任务,也不操作第三句话,之后直接提交taskJson,那么保存的json数据中
dialogue[0]的tasks数据为空,剩下两项都是第二句话产生的tasks数据,这样从json数据中我们就能看出tasks的标注流程,所以我们需要在提交json数据的时候填补对话与对话之间任务的空缺。从这样的逻辑上我们不难看出,dialogue[dialogue.length - 1].tasks始终是tasks最全的数据,所以我们回显的tasks取的是dialogue[dialogue.length - 1].tasks。
2.右侧数据回显
右侧数据回显咱们就不多说了,本身tasks就是右侧数据的提交。
3.左侧数据回显
对于左侧数据,我们最终的目的是想使回显的效果和最开始手动打标签的效果是相同的,可以进行选择取消等操作,并且要显示成对应类型的标签样式。这里我们用到markjs中的实例方法automark方法。对于这个方法我在最新的文档上已经找不到了,并且最新的markjs似乎也没有了鼠标选中大标签的功能,我在源码里甚至没有找到getSelection Api的使用,我看了一下我使用的markjs源码,从中发现了这个方法,下面是automark方法的主要内容:
{
key: 'automark',
value: function automark(list, options) {
var _this2 = this;
if (list && list.length > 0) {
var index = 0;
var draw = function draw() {
var data = list[index];
var rangeData = _this2._getRangeData(data.startOffset, data.endOffset);
var selection = window.getSelection();
var range = window.document.createRange();
var sr = rangeData.startRange;
var er = rangeData.endRange;
try {
range.setStart(sr.node, sr.offset);
range.setEnd(er.node, er.offset);
selection.addRange(range);
} catch (error) {
console.error(error);
console.error(''.concat(JSON.stringify(data)));
}
var newdata = _this2._mark(data.type, data);
if (options && options.afterEach) {
options.afterEach(newdata);
}
if (++index <= list.length - 1) {
draw();
} else {
options && options.after && options.after();
}
}; // 开始标记前,清除所有选中状态(按钮和输入框被点击后也是选中状态)
window.getSelection().removeAllRanges();
draw();
}
},
}
从源码中可以看出,automark方法为Mark对象暴露出的实例方法,此方法需要两个参数:
标签列表list和配置项options,遍历标签列表内容递归重新draw标签内容。大致的使用方法为const m = new Mark(element),其中element为会议中每一句话文案的盒子dom,如下图:
由于我们目前的数据为taskList,每个task里面又分为
四种标签不同标签的四个数组,automark方法中需要的是一个一维的对象数组,所以我们首先要做的就是铺平数组,我直接粘代码:
const signMap = ['task_names', 'task_deadline', 'assignee', 'task_content'];
export const formatTaskList = (taskList) => {
const reviewSignList = [];
taskList.forEach((task) => {
for (let i = 0; i < signMap.length; i++) {
for (let j = 0; j < task[signMap[i]].length; j++) {
reviewSignList.push({
...task[signMap[i]][j],
signType: signMap[i],
});
}
}
});
return reviewSignList;
};
然后我们需要做的是分类,还好我未雨绸缪在json数据的每一个标签里加了一个voiceIndex字段用于记录每个标签出自哪句话,我们可以依据voiceIndex来做标签分类,用voiceIndex做key,每个voiceIndex对应的值为每句话的标签数组,代码如下:
export const formatDataByVoiceIndex = (reviewList) => {
const sortSignData = {};
reviewList.forEach((sign) => {
if (sortSignData[sign.voiceIndex]) {
sortSignData[sign.voiceIndex].push(sign);
} else {
sortSignData[sign.voiceIndex] = [sign];
}
});
return sortSignData;
};
得到的数据是这样的:
看起来我们现在根据
formatDataByVoiceIndex得到的数组遍历出会议中每一轮次消息调用automark方法即可回显标注的展示,然而我们忘了一点,我们之前在标注的时候会把push标签的时候往数组里推的值保存在每个标签的data属性里,为data-options,然后使用一个事件委托避免了给每个标签绑定事件,并且理论上选择过的标签是不可以再点击的,所以我们需要给所有回显的标签显示为不可点击的样式,并且将data-options中的canset值设置为0,当时我最开始没找到做这件事的时机,但是最后我在automark方法中的配置项options中看见了一个函数听名字似乎用的上,大家可以翻上去看看,options里有一个afterEach,在这个函数中我们可以进行标签回显之后的操作,具体代码如下:
// 调用回显标签方法
export const autoMarkFunc = (id, reviewList) => {
const sortSignData = formatDataByVoiceIndex(reviewList);
const voiceIndexArr = Object.keys(sortSignData);
for (let i = 0; i < voiceIndexArr.length; i++) {
const m = new Mark(document.getElementById(`textContext${voiceIndexArr[i]}${id}`));
m.automark(sortSignData[voiceIndexArr[i]], {
afterEach: (data) => {
// 构建标签属性
resetSignAttrs(data);
},
});
}
};
// 构建标签属性方法
const resetSignAttrs = (data) => {
const markDom: HTMLDivElement | null = document.querySelector(`mark[markkey=${data.key}]`);
markDom?.classList.add(`mark-sign-${SIGNTYPE[data.signType]}`);
markDom?.setAttribute('marktype', `sign-${SIGNTYPE[data.signType]}`);
const options = {
...data,
targetVoiceIndex: data.voiceIndex,
canset: 0,
type: `sign-${SIGNTYPE[data.signType]}`,
};
delete options.signType;
delete options.voiceIndex;
markDom?.setAttribute('data-options', JSON.stringify(options));
markDom?.setAttribute(
'style',
'user-select: none; background-color: rgb(151, 148, 148); cursor: not-allowed;',
);
};
至此结束。
总结
有的时候在工具文档上找不到的东西,看看工具实现的代码我们可能会找到一些灵感,多多学习已经成熟的工具封装对我们日常的工具封装组件开发等也很有益处。