开篇
因为这两天手里的项目算是整完啦,所以有点空闲来搞搞好玩儿的东西。本身这个跟上一篇闲来无事,弄个“纯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图中的几个场景,大致发现了以下几个需求:
- 可能是文本或者数字,数字的话需要处理小数位两位小数
- 文字颜色不一样
- 文字有的会发光(阴影效果),有的没有
- 文字大小也有多中尺寸
所以大致设置了以下 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: "" } } }
在上面的需求上又增加了separator 和bolder配置,用来确定数字是否需要分隔符和文本部分的文字字重(既然都抽离成组件了,配置多一点没毛病吧~)。
然后,就是处理模板所用到的计算属性部分了。
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标题和下面的占比比例显示两个部分,封面的图是多个组合在一起实现的。
首先一样是 分析下需求:
- 底部的 bar 的背景色不确定
- bar 的颜色也不确定,也可能是渐变
- 数值和最大值都不确定
- 文字大小、文本颜色也不确定
- 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 样式,也方便后面进行样式排查吧。当然这些都是我的个人感受,也不知道这样做对浏览器的渲染性能有没有影响,不过目前看起来感觉还不错~