【vue】编辑器段落对应材料同步滚动交互

场景需求

  • 编辑器段落对应显示材料
  • 编辑器滚动时,材料同步滚动
  • 编辑器段落无数据时,材料不显示

实现方法

  1. 编辑器与材料组件左右布局
  2. 获取编辑器高度,材料高度与编辑器高度一致
  3. 禁用材料组件的滚动事件
  4. 获取编辑器段落距离顶部的位置,对应材料的顶部高度与段落一致
  5. 监听编辑器滚动高度,对应材料组件同步滚动
  6. 当段落为空时,材料不显示
  7. 当段落比材料短的时候,对应材料进行下移,避免材料重叠

html代码

javascript 复制代码
// 编辑器中段落代码
<div class="ws-container">
	<div class="ws-result" data-set="第一段" data-set-code="dyd" :style="[normal]">
	  <div :style="[indentbase]">这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段这是第一段</div>
	</div>
	<div class="ws-result" data-set="第二段" data-set-code="ded" :style="[normal]">
	  <div :style="[indentbase]" >这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段这是第二段</div>
	</div>
	<div class="ws-result" data-set="第三段" data-set-code="dsd" :style="[normal]">
	  <div :style="[indentbase]">这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段这是第三段</div>
	</div>
</div>

// 对应段落材料代码
<div class="doc-source" @touchmove.prevent @mousewheel.prevent>
	<div ref="sourceMode">
	  <div class="source-mode" :ref="index" v-for="(item, index) in list" :key="index">
	    <div class="source-mode-item" v-for="(item1, index1) in item" :key="index1" :title="item1.name">
	      <div class="flag"></div>
	      <div class="file">来源:{{ item1.name}}</div>
	    </div>
	  </div>
	</div>
</div>

JS代码

javascript 复制代码
export const SOURCEMODE = [
  { name: '第一段', code: 'dyd' },
  { name: '第二段', code: 'ded' },
  { name: '第三段', code: 'dsd' },
];
// 编辑器更新时
contentChange() {
 const dom = this.editor.document.querySelectorAll('.ws-result');
 SOURCEMODE.forEach(item => {
   let flag = false;
   for (let i = 0; i < dom.length; i++) {
     // 有情节且对应dom有高度则表示有内容
     if (item.name === dom[i].getAttribute('data-set') && dom[i].scrollHeight > 0) flag = true;
   }
   this.$emit('updateSourceFile', item.code, flag);
 });
}
// 编辑器删除段落时,更新材料状态
updateSourceFile(type, flag) {
  if (!flag && this.sourceModeFile[type]) delete this.sourceModeFile[type];
  const sourceKeys = Object.keys(this.sourceModeFile);
  const dom = this.$refs.editor.editor.document.querySelectorAll('.ws-result');
  for (let i = 0; i < dom.length; i++) {
    const dataSet = dom[i].getAttribute('data-set');
    const index = SOURCEMODE.findIndex(sourceItem => sourceItem.name === dataSet);
    if (index > -1) {
      dom[i].style.color = 'rgb(24, 144, 255)';
      if (this.$refs[dom[i].getAttribute('data-set-code')]) {
        let currentOffsetTop = dom[i].offsetTop + 26;
        // 当段落比证据短的时候,整体证据进行下移
        // 思路:1. 判断当前dom对应的证据是否有上一个 sourceKeysIndex - 1 > -1
        // 2.拿到上一个距离顶部的高度,拿到当前距离顶部的高度,currentOffsetTop - prevOffsetTop
        // 3.相减后的数字小于上一个的高度prevHeight,说明会重叠,用prevHeight - gap得到重叠高度
        // 4.将重叠高度加上dom距离顶部的高度给当前证据
        // 5.特殊情况,相减后大于上一个高度时,并且差距小于20,则加上26进行情节区分
        const sourceKeysIndex = sourceKeys.findIndex(source => source === dom[i].getAttribute('data-set-code'));
        if (sourceKeysIndex - 1 > -1) {
          const prevOffsetTop = this.$refs[sourceKeys[sourceKeysIndex - 1]][0].offsetTop;
          const prevHeight = this.$refs[sourceKeys[sourceKeysIndex - 1]][0].clientHeight;
          const gap = currentOffsetTop - prevOffsetTop;
          if (gap < prevHeight) {
            currentOffsetTop = currentOffsetTop + (prevHeight - gap) + 26;
          } else {
            if (gap - prevHeight < 20) {
              currentOffsetTop = currentOffsetTop + 26;
            }
          }
        }
        this.$refs[dom[i].getAttribute('data-set-code')][0].style.top = `${currentOffsetTop}px`;
      }
    }
  }
  const containerDom = this.$refs.editor.editor.document.querySelector('.ws-container');
  if (containerDom) this.$refs.sourceMode.style.height = `${containerDom.scrollHeight}px`;
  // 解决当编辑器不触发滚动时,手动让证据滚动到编辑器滚动位置
  const scrollTop = this.$refs.sourceMode.parentElement.scrollTop;
  if (containerDom && scrollTop !== containerDom.scrollTop) {
    this.editorScroll(containerDom.scrollTop);
  }
  // 编辑器滚动时,材料同步滚动
  this.$refs.editor.openEditorScroll();
},
// 编辑器滚动高度,同步右侧材料滚动
editorScroll(scrollTop) {
 this.$refs.sourceMode.parentElement.scrollTo(0, scrollTop);
},

材料列表数据

javascript 复制代码
"list": {
	"dyd": [
	    { "name": "第一段材料1" },
	    { "name": "第一段材料2" },
	    { "name": "第一段材料3" }
	],
	"ded": [
	    { "name": "第二段材料1" },
	    { "name": "第二段材料2" },
	    { "name": "第二段材料3" }
	],
	"dsd": [
	    { "name": "第三段材料1" },
	    { "name": "第三段材料2" },
	    { "name": "第三段材料3" },
	    { "name": "第三段材料4" }
	]
}

效果图

总结:

  1. 最开始为了减少监听带来的影响,只针对当前操作的段落进行处理,但是后面发现,当操作上一段,下一段有材料时,材料的定位不准确,于是改成了只要段落变化,全部的材料位置计算一次
  2. 在当段落高度少于右侧来源高度时,思考了很久,最开始想的是用相邻段落的高度设置给材料,但是因为不是每个段落都有材料,不一定会准确,加上段落组件层级太深,组件之前取值不好维护,因此放弃了这种方式,选择用相邻材料的高度差值处理。
相关推荐
乌夷1 小时前
Vue.js 3 的设计思路
前端·javascript·vue.js
星云_byto1 小时前
五分钟从零配置好VSCode + Claude Code + DeepSeek V4 Pro
ide·vscode·编辑器·cursor·codex·claude code·deepseekv4
前端之虎陈随易10 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
格子软件13 小时前
2026年GEO贴牌代理:分布式多级分账状态机源码深度解构
java·vue.js·分布式·vue·geo
之歆13 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
2501_9437823514 小时前
【共创季稿事节】摇骰子:用 ArkTS 实现随机动画与交互反馈
运维·nginx·交互·harmonyos·鸿蒙·鸿蒙系统
奶油mm19 小时前
公司技术债堆积如山,我一人之力用 Vue3 偷换了整个前端架构
前端·vue.js
林熙蕾LXL21 小时前
VSCode调试
ide·vscode·编辑器
格子软件1 天前
2026年分布式GEO代理流量调度:源码级状态机防重挂实战
java·vue.js·人工智能·spring boot·分布式·vue
2501_942389551 天前
小米寥寥几家车企设计汽车顶棚
华为·编辑器·时序数据库·harmonyos