闲来无事,配合CSS变量搞个Vue数据显示组件吧~

简介: 闲来无事,配合CSS变量搞个Vue数据显示组件吧~

开篇


因为这两天手里的项目算是整完啦,所以有点空闲来搞搞好玩儿的东西。本身这个跟上一篇闲来无事,弄个“纯CSS”的伪3D柱状图吧~算是姊妹篇,因为都是从UI图上抠下来的😝。


最后的效果图就长这样儿~


网络异常,图片无法展示
|


忽略掉 demo 里面的文本和单位哈,因为右边的数值显示 部分的样式在其他地方也有相似的效果,所以拆分成了两个组件:HighlightText 高亮文本组件 和 WeightedLineBar 权重占比图


也为了练习一下 CSS 变量,所以有一部分动态样式 使用了Vue 动态属性配合 CSS 变量来实现的。 如果有兴趣的同学可以接着往下看,代码和逻辑都很简单,对稍微厉害一点的同学可能都没有太大帮助哈,实在抱歉!


HighlightText 高亮文本


因为组件的主要作用就是 调整文本的样式,所以组件内部只有一个span标签,通过 计算属性 动态设置标签的样式和类名。


<template>
  <span
    :class="['highlight-text', { 'with-unit': !!unit, lighting: lighting }]"
    v-bind="{ 'data-attr-unit': !!text && unit }"
    :style="computedStyle"
    >{{ computedText }}</span
  >
</template>


当然,好像是因为 html 的解析问题,如果span之类的 标签与文本不在同一行或者没有紧邻的情况下,文本两边会出现空白字符占位。所以这里格式化成了这个样子。


而其中的 自定义属性 data-attr-unit,则是为了方便后面显示单位。


分析我们UI图中的几个场景,大致发现了以下几个需求:


  1. 可能是文本或者数字,数字的话需要处理小数位两位小数


  1. 文字颜色不一样


  1. 文字有的会发光(阴影效果),有的没有


  1. 文字大小也有多中尺寸


所以大致设置了以下 props 配置项:


export default {
  name: "HighlightText",
  props: {
    text: {
      type: [String, Number],
      default: ""
    },
    color: {
      type: String,
      default: "#010101" // #BCE3FF
    },
    size: {
      type: [String, Number],
      default: 12
    },
    separator: {
      type: Boolean,
      default: true
    },
    bolder: {
      type: Boolean,
      default: true
    },
    lighting: {
      type: Boolean,
      default: true
    },
    unit: {
      type: String,
      default: ""
    }
  }
}


在上面的需求上又增加了separatorbolder配置,用来确定数字是否需要分隔符和文本部分的文字字重(既然都抽离成组件了,配置多一点没毛病吧~)。


然后,就是处理模板所用到的计算属性部分了。


export default {
  computed: {
    computedText() {
      if (typeof this.text === "number" && this.separator) {
        return this.text.toLocaleString();
      }
      return this.text;
    },
    computedStyle() {
      let fontSize = this.size;
      let fontWeight = "normal";
      const color = this.color;
      if (typeof fontSize === "number") {
        fontSize += "px";
      }
      if (this.bolder) {
        fontWeight = "bold";
      }
      return { fontSize, fontWeight, color, lineHeight: fontSize };
    }
  }
};


数字分隔符的话,本身 Number 提供了一个toLocalString() 的方法,用来转成 带逗号分隔的字符串;当然为了方便,这里对样式部分就只是做了一点组装然后绑定成行内样式的形式给了span 标签。


然后就是 CSS 部分。嗯~~~这部分太简单,就直接放代码吧。只是注意unit单位为了节省标签,采用的是自定义标签属性的形式结合伪类来实现的。


.highlight-text {
  font-family: Akrobat-Black, Akrobat, sans-serif;
  user-select: none;
  word-break: break-word;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  &.lighting {
    text-shadow: 0 0 6px;
  }
  &.with-unit {
    &::after {
      content: attr(data-attr-unit);
      display: inline-block;
      transform: scale(0.4);
      transform-origin: bottom left;
    }
  }
}


最终我们可以通过这样的代码得到一些理想的效果:


<highlight-text text="这是高亮文本" size="24px" unit="unit" /><br />
  <highlight-text :text="8972183671.1762" size="32px" unit="%" /><br />
  <highlight-text text="这是高亮文本2" size="32px" color="#ea28bc" /><br />
  <highlight-text text="这是高亮文本3,没有单位" size="48px" color="#8b5fda" /><br />
  <highlight-text text="这是高亮文本4" size="64px" color="#ea28bc" unit="%" :lighting="false" /><br />


网络异常,图片无法展示
|


当然,后面也可以增加其他配置,比如单位与文本的间隔、高亮颜色、立体文本?等


WeightedLineBar 权重占比图


这个其实本身只有上面的title标题和下面的占比比例显示两个部分,封面的图是多个组合在一起实现的。


首先一样是 分析下需求


  1. 底部的 bar 的背景色不确定


  1. bar 的颜色也不确定,也可能是渐变


  1. 数值和最大值都不确定


  1. 文字大小、文本颜色也不确定


  1. bar 的高度也不确定


所以整理一下,初步确定的 Props 配置大概有以下项目:


export default {
  props: {
    data: {
      type: Number,
      default: 0
    },
    max: {
      type: Number,
      default: 1
    },
    barHeight: {
      type: Number,
      default: 16
    },
    barPadding: {
      type: Number,
      default: 2
    },
    icon: {
      type: String,
      default: "1"
    },
    title: {
      type: String,
      default: ""
    },
    unit: {
      type: String
    },
    highlight: {
      type: Boolean,
      default: true
    },
    fontColor: {
      type: String,
      default: "#ffffff"
    },
    fontSize: {
      type: [String, Number],
      default: "20px"
    },
    color: {
      type: [String, Array],
      default: "#92e1fe"
    },
    bgColor: {
      type: String,
      default: "#33414c"
    }
  }
  }
};


然后就是大致设计一下html 模板和 CSS 基础样式,首先模板部分如下


<template>
  <div class="weighted-line-bar">
    <div class="bar__header" :style="computedHeaderStyle">
      <slot name="icon"></slot>
      <div v-if="!$slots.icon" class="bar__header-icon">{{ icon }}</div>
      <div class="bar__header-title">{{ title }}</div>
      <div v-if="!highlight" class="bar__header-data" :data-attr-unit="unit">{{ data }}</div>
      <highlight-text v-else :text="data" :unit="unit" :color="fontColor" :size="fontSize" />
    </div>
    <div class="bar__content" :style="computedBoxStyle">
      <div :class="computedBarClass" :style="computedBarStyle"></div>
    </div>
    <slot name="footer" />
  </div>
</template>


为了方便布局,外层采用的是垂直分布的 flex 布局,内部分为 两层:header 和 content,底部留了一个slot 插槽,用来显示扩展内容。


然后header 部分的 Icon 图标位置,也提供了一个自定义插槽,没有的话则可以传递参数使用默认样式显示header 一样是 flex 布局,中间的 title 标题部分使用 flex: 1 占据除 icon 和 data 两个部分之外的剩余区域。


也是之前在别人的代码中看到可以通过行内样式设置 CSS 变量,在外部 style 中使用,所以脑子一热准备用computed 配合 v-bind:style 来实现动态定义 CSS 变量,看能不能实现。


最终的computed 计算属性部分和 style 样式部分因为联系比较紧密,就不拆分代码了,完整的代码如下:


<template>
  <div class="weighted-line-bar">
    <div class="bar__header" :style="computedHeaderStyle">
      <slot name="icon"></slot>
      <div v-if="!$slots.icon" class="bar__header-icon">{{ icon }}</div>
      <div class="bar__header-title">{{ title }}</div>
      <div v-if="!highlight" class="bar__header-data" :data-attr-unit="unit">{{ data }}</div>
      <highlight-text v-else :text="data" :unit="unit" :color="fontColor" :size="fontSize" />
    </div>
    <div class="bar__content" :style="computedBoxStyle">
      <div :class="computedBarClass" :style="computedBarStyle"></div>
    </div>
    <slot name="footer" />
  </div>
</template>
<script>
import { getRawType } from "../../utils/tools";
import HighlightText from "@/components/HighlightText";
export default {
  name: "WeightedLineBar",
  components: { HighlightText },
  props: {
    data: {
      type: Number,
      default: 0
    },
    max: {
      type: Number,
      default: 1
    },
    barHeight: {
      type: Number,
      default: 16
    },
    barPadding: {
      type: Number,
      default: 2
    },
    icon: {
      type: String,
      default: "1"
    },
    title: {
      type: String,
      default: ""
    },
    unit: {
      type: String
    },
    highlight: {
      type: Boolean,
      default: true
    },
    fontColor: {
      type: String,
      default: "#ffffff"
    },
    fontSize: {
      type: [String, Number],
      default: "20px"
    },
    color: {
      type: [String, Array],
      default: "#92e1fe"
    },
    bgColor: {
      type: String,
      default: "#33414c"
    }
  },
  computed: {
    computedBoxStyle() {
      let styles = {};
      styles["--box-color"] = this.bgColor;
      styles["--box-height"] = `${this.barPadding * 2 + this.barHeight}px`;
      styles["--bar-width"] = `${(this.data / this.max) * 100}%`;
      styles["--bar-padding"] = `${this.barPadding}px`;
      styles["--bar-height"] = `${this.barHeight}px`;
      return styles;
    },
    computedHeaderStyle() {
      let styles = {};
      styles["--font-color"] = this.fontColor;
      if (typeof this.fontSize === "number") {
        styles["--font-size"] = this.fontSize + "px";
      } else {
        styles["--font-size"] = this.fontSize;
      }
      return styles;
    },
    computedBarClass() {
      let classes = "bar__content-inner";
      if (
        getRawType(this.color) === "array" ||
        this.color.startsWith("radial") ||
        this.color.startsWith("linear") ||
        this.color.startsWith("repeating")
      ) {
        classes += " gradient-bg";
      }
      return classes;
    },
    computedBarStyle() {
      let styles = {};
      if (this.computedBarClass === "bar__content-inner") {
        styles["--bar-color"] = this.color;
      } else {
        if (getRawType(this.color) === "array") {
          styles["backgroundImage"] = `linear-gradient(to right, ${this.color.join(",")})`;
          styles["--bar-color"] = this.color[0];
        } else {
          styles["backgroundImage"] = this.color;
          styles["--bar-color"] = "#92e1fe";
        }
      }
      return styles;
    }
  }
};
</script>
<style scoped lang="scss">
.weighted-line-bar {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-rows: 1fr 1fr;
  grid-row-gap: 12px;
}
.bar__header {
  width: 100%;
  height: 100%;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-between;
  align-items: center;
}
.bar__header-icon,
.bar__header-title,
.bar__header-data {
  color: var(--font-color);
  font-size: var(--font-size);
  overflow: hidden;
  word-break: break-word;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bar__header-icon {
  border: 2px solid var(--font-color);
  padding: 0 4px 0 0;
}
.bar__header-title {
  flex: 1;
  text-align: left;
  padding-left: 1em;
}
.bar__header-data {
  padding-left: 1em;
  font-weight: bold;
  &::after {
    content: attr(data-attr-unit);
    display: inline-block;
    transform: scale(0.4);
    transform-origin: bottom left;
  }
}
.bar__content {
  width: 100%;
  height: var(--box-height);
  border-radius: calc(var(--box-height) / 2);
  background-color: var(--box-color);
  position: relative;
  .bar__content-inner {
    position: absolute;
    left: calc(var(--bar-padding) - 1px);
    top: var(--bar-padding);
    width: var(--bar-width);
    height: var(--bar-height);
    border-radius: calc(var(--bar-height) / 2);
    background-color: var(--bar-color);
    box-shadow: 0 0 calc(var(--bar-padding) + 4px) 0 var(--bar-color);
    transition: all ease-in-out 0.2s;
  }
}
</style>


发现的小技巧


上面说了有用 computed 配合 v-bind:style 来实现动态定义 CSS 变量,然后在外部 style 中使用的方式,在我写完的第一眼发现看上去好像是没什么用,但是后面再改改又发现,还是有那么一点儿用的。


比如我们在 header 部分的外层 div 绑定的动态样式声明的 CSS 变量 --font-size--font-color,那么我们在这个 div 的 所有子元素中,都可以用这两个变量,这样也算是可以 减少我们去绑定多个动态样式 吧,而且我发现,这样写 也减少了行内样式的问题,可以直接在下面的 style 部分直接写完完整的 css 样式,也方便后面进行样式排查吧。当然这些都是我的个人感受,也不知道这样做对浏览器的渲染性能有没有影响,不过目前看起来感觉还不错~


目录
相关文章
|
5月前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
461 2
|
8月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
930 0
|
10月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
1200 4
|
8月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
8月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
637 8
|
8月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
388 1
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
625 161
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
1014 159
|
10月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
842 6
|
8月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
372 0