仿写讯飞AI生成PPT大纲组件

效果

别的不说先上效果
在这里插入图片描述

在这里插入图片描述

难点

  1. 树的实现 :需要递归自身,有丶难度。但是对于各位应该是有手就彳亍
  2. 双亲节点样式 :可以观察到双亲节点在连接线左侧是有内容的,叶子节点则没有。
  3. 连接线:可以观察到双亲节点是实心圆点,叶子节点是空心圆圈且连接线有缩进。
  4. 确定层级:缩进是根据层级确定的,如何确定层级。
  5. 确定最后的叶子节点:最后一个叶子节点的连接线是没有往下的,这里需要做判断。
  6. 插槽透传:因为需要递归,一些插槽的透传也是必不可少的。
  7. 重新渲染:有时数据结构变了,但是组件的并没有重新渲染导致与预期不一致。

实现

在这里插入图片描述

确定层级

在组件props中 保留一个默认层级level = 0,当有子节点时把:level = "level + 1" 传入子组件中即可完成标识。例如第一层的level = 0 ,第二层的level = 1。后续还可以根据level计算缩进线宽度。

连接线

1.第一个节点没有上边线,这个很好判断。
2.中间的节点有上边线,下边线,缩进线。
3.最后一个节点没有下边线,这个不太好判断。
4.点还是圆圈,这个很好判断。
5.缩进线的宽度计算公式:level * 每一节缩进线的宽度,这里默认一节缩进线宽度为16
例如:第一层的双亲节点他的缩进线宽度为:0 * 16 = 0
例如:第二层的叶子节点他的缩进线宽度为:1 * 16 =16

重新渲染

在组件上加key,改变key即可强制重新渲染。
在这里插入图片描述

确定最后的叶子节点

这个在组件内不好判断呐,那我干脆让使用者来判断。让他来做这件事,那在编程中怎么去做事呢?当然是函数辣,让用户传入函数,根据函数调用返回的结果去决定是否是最后的叶子节点。

在这里插入图片描述
我把数据都给你,我让你自己去判断。

插槽透传

循环$slots即可
在这里插入图片描述上面这样写ts会报错,但是其实是没问题,其它项目也是这么写的都没问题。但是为了不报红我还是用下面的写法在这里插入图片描述

源码

<template>
  <div v-bind="$attrs" class="tree-wrap">
    <template v-for="(chapter, chapterIdx) in treeData" :key="chapter.id">
      <div class="tree-node">
        <div class="node-desc" :style="descStyle">
          <slot
            v-if="$slots.desc"
            name="desc"
            :chapter="chapter"
            :chapterIdx="chapterIdx"
            :level="level"
          ></slot>
          <span v-else>{{ nodeDescText(level, chapterIdx) }}</span>
        </div>
        <div class="node-dot-line" :style="nodeDotLineStyle">
          <div v-if="shouldShowTopVertical(chapterIdx)" class="line-top"></div>
          <div
            :class="[shouldShowHorizontal(level, chapter) ? 'circle' : 'dot']"
            :style="nodeDotCircleStyle"
          ></div>
          <div v-if="isRenderLineBottom(level, chapter, chapterIdx)" class="line-bottom"></div>
          <div
            v-if="shouldShowHorizontal(level, chapter)"
            class="line-horizontal"
            :style="horizontalLineStyle"
          ></div>
        </div>
        <div class="node-info" :style="nodeInfoStyle">
          <slot
            v-if="$slots.title"
            name="title"
            :chapter="chapter"
            :chapterIdx="chapterIdx"
            :level="level"
          ></slot>
          <span v-else :class="{ 'font-bold': level === 0 }">{{ chapter.chapterTitle }}</span>
        </div>
        <div v-if="$slots.extra" class="extra" :style="extraStyle">
          <slot name="extra" :chapter="chapter" :chapterIdx="chapterIdx" :level="level"></slot>
        </div>
      </div>
      <PptOutline
        v-if="chapter && chapter.chapterContents"
        :level="level + 1"
        :tree-data="chapter.chapterContents"
        :key="chapter.id"
        :is-render-line-bottom="isRenderLineBottom"
      >
        <template v-for="(_slotFn, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
          <slot v-if="slotName === 'desc'" name="desc" v-bind="slotProps"></slot>
          <slot v-if="slotName === 'title'" name="title" v-bind="slotProps"></slot>
          <slot v-if="slotName === 'extra'" name="extra" v-bind="slotProps"></slot>
        </template>
      </PptOutline>
    </template>
  </div>
</template>

<script setup lang="ts">
import PptOutline from '@/components/PptOutline.vue'

const props = withDefaults(
  defineProps<{
    level?: number
    treeData: ChaptersItem[]
    isRenderLineBottom?: (level: number, chapter: ChaptersItem, chapterIdx: number) => boolean
  }>(),
  {
    level: 0,
    isRenderLineBottom: () => true
  }
)
const slots = useSlots()
const dotSize = 7
const descWidth = 50
const extraWidth = 100
const subNodeLineLeft = 16
const descStyle = {
  width: `${descWidth}px`
}
const extraStyle = {
  width: `${extraWidth}px`
}
// 横向连接线长度
const horizontalLineWidth = computed(() => props.level * subNodeLineLeft)
const horizontalLineStyle = computed(() => ({
  width: `${horizontalLineWidth.value}px`
}))
// 节点点样式
const nodeDotCircleStyle = computed(() => {
  if (props.level === 0) {
    return {
      left: `${horizontalLineWidth.value}px`
    }
  } else {
    return {
      left: `${horizontalLineWidth.value + dotSize / 2}px`
    }
  }
})
// 节点点线样式
const nodeDotLineWidth = computed(() => 3 * dotSize + horizontalLineWidth.value)
const nodeDotLineStyle = computed(() => ({
  width: `${nodeDotLineWidth.value}px`
}))
// 节点信息样式
const nodeInfoStyle = computed(() => {
  if (slots.extra) {
    return {
      width: `calc(100% - ${descWidth + extraWidth + nodeDotLineWidth.value}px)`
    }
  } else {
    return {
      width: `calc(100% - ${descWidth + nodeDotLineWidth.value}px)`
    }
  }
})
// 是否展示横向连接线
const shouldShowHorizontal = (level: number, chapter: ChaptersItem): boolean =>
  level !== 0 && Boolean(chapter)
// 是否展示上半部分纵向连接线
const shouldShowTopVertical = (chapterIdx: number): boolean => chapterIdx !== 0 || props.level > 0
// 展示节点描述信息
const nodeDescText = (level: number, chapterIdx: number): string =>
  level === 0 ? `章节${chapterIdx + 1}` : ''
</script>

<style lang="less" scoped>
.tree-wrap {
  width: 100%;
  @titleColor: #161724;
  @titleFontSize: 14px;
  @descColor: #8e90a5;
  @descFontSize: 12px;
  @dotLineColor: #bfc7d6;
  @hoverBgColor: #f6f6f6;
  @hoverBorderRadius: 6px;
  @dotSize: 7px;
  @dotLineWidth: 1px;
  @subNodeLineleft: 16px;
  @dotTop: 15px;
  @nodeMinHeight: 36px;

  /**每一个节点-start */
  .tree-node {
    display: flex;
    padding: 0 4px 0 20px;
    min-height: @nodeMinHeight;
    line-height: @nodeMinHeight;
    &:hover {
      background-color: @hoverBgColor;
      border-radius: @hoverBorderRadius;
    }

    /**节点描述:例如章节1 */
    .node-desc {
      padding-right: 10px;
      color: @descColor;
      font-size: @descFontSize;
      word-wrap: break-word;
      white-space: pre-line;
    }
    /**节点描述-end */

    /**节点连接线-start */
    .node-dot-line {
      position: relative;
      width: 15px;
      height: inherit;
      .line-top {
        position: absolute;
        top: 0;
        left: calc(@dotSize / 2);
        width: @dotLineWidth;
        height: calc(@dotTop + @dotSize / 2);
        background-color: @dotLineColor;
      }
      .dot {
        position: absolute;
        top: @dotTop;
        width: @dotSize;
        height: @dotSize;
        border-radius: 50%;
        background-color: @dotLineColor;
      }
      .circle {
        position: relative;
        top: @dotTop;
        width: @dotSize;
        height: @dotSize;
        border-radius: 50%;
        border: @dotLineWidth solid @dotLineColor;
      }
      .line-bottom {
        position: absolute;
        bottom: 0;
        left: calc(@dotSize / 2);
        width: @dotLineWidth;
        height: calc(100% - @dotTop - @dotSize / 2);
        background-color: @dotLineColor;
      }
      .line-horizontal {
        position: relative;
        top: calc(@dotTop - @dotSize / 2);
        left: calc(@dotSize / 2);
        width: @subNodeLineleft;
        height: @dotLineWidth;
        background-color: @dotLineColor;
      }
    }
    /**节点连接线-end */

    /**节点信息:例如标题 */
    .node-info {
      min-width: 400px;
      height: fit-content;
      font-size: @titleFontSize;
      word-wrap: break-word;
      white-space: pre-line;
    }
    /**节点信息-end */

    /**节点额外信息:例如图标 */
    .extra {
      height: @nodeMinHeight;
      color: @descColor;
      text-align: right;
    }
    /**节点额外信息-end */
  }
  /**每一个节点-end */
}
.font-bold {
  font-weight: bold;
}
</style>

// 二级标题(章节)
declare interface ChaptersItem {
  id: number
  chapterTitle: string | null
  fileUrl: string | null
  fileType: number
  chartFlag: string | null
  searchFlag: string | null
  chapterContents: ChaptersItem[] | null
  [key: string]: any
}
// 大纲
declare interface OutLine {
  id: number
  title: string | null
  subTitle: string | null
  fileUrl: string | null
  fileType: number
  chapters: ChaptersItem[] | null
  end: string | null
  fileId: string | null
  [key: string]: any
}
// 大纲返回内容
declare interface PptOutlineData {
  sid: string
  coverImgSrc: string
  title: string
  subTitle: string
  outline: OutLine
  [key: string]: any
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 安装element-ui: `npm install element-ui --save` 2. 引入element-ui和样式文件 ```javascript import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) ``` 3. 创建Carousel组件 ```vue <template> <el-carousel :interval="interval"> <el-carousel-item v-for="(item, index) in list" :key="index"> <div class="card"> <div class="card-img"> <img :src="item.imgUrl" alt=""> </div> <div class="card-info"> <h3>{{ item.title }}</h3> <p>{{ item.desc }}</p> <a :href="item.link" target="_blank">了解更多</a> </div> </div> </el-carousel-item> </el-carousel> </template> <script> export default { name: 'MyCarousel', props: { list: { type: Array, default: () => [] }, interval: { type: Number, default: 3000 } } } </script> <style scoped> .card { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 200px; width: 300px; background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .card-img { height: 120px; width: 100%; overflow: hidden; border-radius: 4px 4px 0 0; } .card-img img { height: 100%; width: 100%; object-fit: cover; } .card-info { padding: 16px; text-align: center; } .card-info h3 { font-size: 18px; margin-bottom: 10px; } .card-info p { font-size: 14px; margin-bottom: 10px; color: #999; } .card-info a { display: inline-block; font-size: 14px; color: #409eff; } </style> ``` 4. 在父组件中使用Carousel组件 ```vue <template> <my-carousel :list="list" /> </template> <script> import MyCarousel from './MyCarousel.vue' export default { components: { MyCarousel }, data() { return { list: [ { imgUrl: 'https://picsum.photos/300/200/?image=10', title: 'Card Title 1', desc: 'Card Description 1', link: 'https://www.example.com' }, { imgUrl: 'https://picsum.photos/300/200/?image=20', title: 'Card Title 2', desc: 'Card Description 2', link: 'https://www.example.com' }, { imgUrl: 'https://picsum.photos/300/200/?image=30', title: 'Card Title 3', desc: 'Card Description 3', link: 'https://www.example.com' }, { imgUrl: 'https://picsum.photos/300/200/?image=40', title: 'Card Title 4', desc: 'Card Description 4', link: 'https://www.example.com' } ] } } } </script> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值