vue - 动态统计
vue - 动态统计
intro
- 需求
更具不同配置显示不同统计效果和数据
组件
宽高
组件宽高自行通过盒子实现,项目使用动态布局组件,可从首页导航
列
滚动
尽量避免滚动或自行调整滚动样式
动态背景
难点
不同宽度组件间通过拉伸宽度达到协调效果
通过计算透明度主题色
实现思路
- 伪类长方形旋转 (无法实现)
- 不同宽度,旋转相同角度效果不同
- 尝试用三角形 (无法实现)
- border-width 无法设为宽度百分比,不同宽度无法实现拉伸效果
- transform (无法实现)
- 旋转
- 倾斜
- 伸缩
- canvas 生成图片,做图片拉伸 (可实现,计算线条交点算法繁琐)
- 传入图片 (可实现,配置繁琐)
- 需配置传入背景图片
- 无法改变颜色
- iconfont 或 svg图片实现 (可实现,最佳!!!)
- 可通过改变字体颜色改变颜色
canvas 实现
需计算交点
let can = document.createElement('canvas');
let height = 100;
can.width = 300;
can.height = height;
can.style = 'height:auto';
let cxt = can.getContext('2d');
cxt.translate(0, height);
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.9);
cxt.beginPath();
[
[0, 0],
[0, -height * 0.5],
[60, -20],
[300 * 1, -height * 0.8],
[300 * 1, 0],
].forEach((v, i) => {
// console.log(v);
cxt[i == 0 ? 'moveTo' : 'lineTo'](...v);
});
cxt.closePath();
cxt.fill();
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.8);
cxt.beginPath();
cxt.moveTo(300 * 0.7, 0);
cxt.lineTo(300, -height * 0.6);
cxt.lineTo(300, -height * 0.4);
cxt.closePath();
cxt.fill();
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.8);
cxt.beginPath();
cxt.moveTo(0, 0);
cxt.lineTo(60, -20);
cxt.lineTo(100, 0);
cxt.closePath();
cxt.fill();
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.8);
cxt.beginPath();
cxt.moveTo(90, 0);
cxt.lineTo(120, 0);
cxt.lineTo(300, -height * 0.7);
cxt.lineTo(130, -38);
cxt.closePath();
cxt.fill();
let img = document.createElement('img');
Object.assign(img.style, {
width: '100%',
height: '100%',
display: 'inline-block',
position: 'relative',
zIndex: 0,
});
// img.style = 'width:100%;height:100%;display:inline-block;position:relative;z-index:0;'; // strict 模式下不允许分配到只读属性
img.src = can.toDataURL('image/png');
this.$el.firstChild.appendChild(img);
计算透明度主题色
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16);
let green = parseInt(color.slice(2, 4), 16);
let blue = parseInt(color.slice(4, 6), 16);
if (tint === 0) {
// when primary color is in its rgb space
return [red, green, blue].join(',');
} else {
red += Math.round(tint * (255 - red));
green += Math.round(tint * (255 - green));
blue += Math.round(tint * (255 - blue));
red = red.toString(16);
green = green.toString(16);
blue = blue.toString(16);
return `#${red}${green}${blue}`;
}
};
代码及说明
<template>
<!-- 动态统计组件 -->
<!-- 说明
1. 配置
1. 数据
1. 层级2级,二维数组,children内不能再嵌套子集
2. 列布局
3. 主题色
4. 高亮数据
2. 溢出滚动
3. 列布局
1. 暂不支持列布局含子集
2. 格式化数据支持列布局
4. 动态背景
-->
<div class="case_calc_item" :style="{ borderColor: data.color }">
<i class="background"></i>
<div class="case_calc_item_title" :style="{ backgroundColor: data.color }">
{{ data.stage }}
</div>
<div class="case_calc_list">
<div class="case_calc_list_content">
<div
class="case_calc_list_item"
:class="{ highlight: v.point }"
v-for="(v, i) in list"
:key="i"
>
<div
class="case_calc_parent"
:style="v.point ? { color: data.color } : ''"
>
<!-- 列布局 start:要求 1、连续列配置 -->
<div v-if="v.cols" class="case_calc_parent case_calc_parent_col">
<div
class="case_calc_parent_col_item"
v-for="(m, n) in v.cols"
:key="n"
>
<div class="case_calc_value">{{ m.amount }}</div>
<div class="case_calc_label">{{ m.label }}</div>
</div>
</div>
<!-- 列布局 end -->
<template v-else>
<div class="case_calc_value">{{ v.amount }}</div>
<div class="case_calc_label">{{ v.label }}</div>
</template>
</div>
<div
v-if="v.children && v.children.length"
class="case_calc_children"
>
<div
class="case_calc_children_item"
v-for="(m, n) in v.children"
:key="n"
:class="{ highlight: m.point }"
:style="m.point ? { color: data.color } : ''"
>
<div class="children_value">{{ m.amount }}</div>
<div class="children_label">{{ m.label }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: () => {
return {};
},
},
},
data() {
return {};
},
computed: {
list: function () {
return this.formatList();
},
},
mounted() {
this.createBgImg();
},
methods: {
createBgImg() {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16);
let green = parseInt(color.slice(2, 4), 16);
let blue = parseInt(color.slice(4, 6), 16);
if (tint === 0) {
// when primary color is in its rgb space
return [red, green, blue].join(',');
} else {
red += Math.round(tint * (255 - red));
green += Math.round(tint * (255 - green));
blue += Math.round(tint * (255 - blue));
red = red.toString(16);
green = green.toString(16);
blue = blue.toString(16);
return `#${red}${green}${blue}`;
}
};
let can = document.createElement('canvas');
let height = 100;
can.width = 300;
can.height = height;
can.style = 'height:auto';
let cxt = can.getContext('2d');
cxt.translate(0, height);
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.9);
cxt.beginPath();
[
[0, 0],
[0, -height * 0.5],
[60, -20],
[300 * 1, -height * 0.8],
[300 * 1, 0],
].forEach((v, i) => {
// console.log(v);
cxt[i == 0 ? 'moveTo' : 'lineTo'](...v);
});
cxt.closePath();
cxt.fill();
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.8);
cxt.beginPath();
cxt.moveTo(300 * 0.7, 0);
cxt.lineTo(300, -height * 0.6);
cxt.lineTo(300, -height * 0.4);
cxt.closePath();
cxt.fill();
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.8);
cxt.beginPath();
cxt.moveTo(0, 0);
cxt.lineTo(60, -20);
cxt.lineTo(100, 0);
cxt.closePath();
cxt.fill();
cxt.fillStyle = tintColor(this.data.color.replace('#', ''), 0.8);
cxt.beginPath();
cxt.moveTo(90, 0);
cxt.lineTo(120, 0);
cxt.lineTo(300, -height * 0.7);
cxt.lineTo(130, -38);
cxt.closePath();
cxt.fill();
let img = document.createElement('img');
Object.assign(img.style, {
width: '100%',
height: '100%',
display: 'inline-block',
position: 'relative',
zIndex: 0,
});
// img.style = 'width:100%;height:100%;display:inline-block;position:relative;z-index:0;'; // strict 模式下不允许分配到只读属性
img.src = can.toDataURL('image/png');
this.$el.firstChild.appendChild(img);
},
/**
* @method formatList
* @description 格式化列表数据
* 1. 列数组处理
*/
formatList() {
let list = [];
function pushColItem(item) {
// list 为空 或 最后一项不为列数组 push 新cols
!list.length || !list[list.length - 1].cols
? list.push({ cols: [] })
: '';
list[list.length - 1].cols.push(item);
}
this.data.list.forEach((v, i) => {
this.isColItem(v, i) ? pushColItem(v) : list.push(v);
});
return list;
},
/**
* @method isColItem
* @description 判断是否列布局,返回新的列数组
* 判断要素
* 1. v.col
* 2. i=0时,判断存在下一项且下一项 col == true
* 3. i!=0, 上一项col==true 或 存在下一项且下一项col==true
*/
isColItem(v, i) {
let list = this.data.list;
// if (v.col) {
// console.log(`${i} is col:${v.col}`)
// console.log(`${i} isColItem:${Boolean(v.col)&&(list[i - 1]&&list[i - 1].col||list[i + 1]&&list[i + 1].col)}`)
// console.log(`${i} exist prev && is col:${list[i - 1]&&list[i - 1].col}`)
// console.log(`${i} exist next && is col:${list[i + 1]&&list[i + 1].col}`)
// }
return (
Boolean(v.col) &&
((list[i - 1] && list[i - 1].col) || (list[i + 1] && list[i + 1].col))
);
},
},
};
</script>
<style lang="scss" scoped>
$border-color: #dee3e7;
.case_calc_item {
position: relative;
box-sizing: border-box;
border: 1px solid #508aff;
background-color: #fff;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
border-radius: 0 0 4px 4px;
color: #333;
border-top-width: 5px;
padding: 0 10px;
position: relative;
> i.background {
overflow: hidden;
bottom: 0;
display: block;
position: absolute;
height: 81px;
width: 100%;
}
.case_calc_item_title {
display: inline-flex;
align-items: center;
padding: 0 30px;
height: 40px;
font-size: 16px;
background-color: #508aff;
color: #fff;
border-radius: 0 0 8px 8px;
flex-shrink: 0;
}
.case_calc_list {
position: relative;
z-index: 1;
width: 100%;
display: flex;
overflow: auto;
align-items: center;
flex-grow: 1;
.case_calc_list_content {
display: flex;
width: 100%;
flex-grow: 1;
align-items: stretch;
}
.case_calc_list_item {
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
&.highlight,
.highlight {
color: #508aff;
}
.case_calc_label {
font-size: 16px;
line-height: 24px;
}
.case_calc_value {
font-size: 28px;
}
.case_calc_parent {
&.case_calc_parent_col {
margin-bottom: initial;
}
}
.case_calc_parent_col {
display: flex;
flex-direction: column;
.case_calc_parent_col_item {
flex-grow: 1;
}
.case_calc_parent_col_item + .case_calc_parent_col_item {
border-top: 1px solid $border-color;
}
}
.case_calc_children {
margin-top: 16px;
padding-top: 12px;
display: flex;
flex-direction: initial;
border-top: 1px solid $border-color;
.case_calc_children_item {
width: 100%;
.children_value {
font-size: 20px;
line-height: 24px;
}
.children_label {
font-size: 16px;
}
}
}
}
.case_calc_list_item + .case_calc_list_item,
.case_calc_children_item + .case_calc_children_item {
border-left: 1px solid $border-color;
}
}
}
</style>
数据结构
calcList: [
{
showHeader: false,
calcHeight: '32%',
width: 0.5,
data: {
color: '#508AFF',
stage: '立案准备阶段',
list: [
{
label: '待提交审批',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
label: '待审批',
amount: 4215,
},
{
label: '待申请',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
label: '已撤销',
amount: 800,
},
],
},
},
{
showHeader: false,
calcHeight: '32%',
width: 0.25,
data: {
color: '#508AFF',
stage: '立案阶段',
list: [
{
label: '待立案',
amount: 6165,
children: [
{
label: '待提交立案审批',
amount: 4215,
},
{
label: '待审批',
amount: 1950,
},
],
},
{
col: true,
label: '待交诉讼费',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
col: true,
label: '已交诉讼费',
amount: 2800,
},
{
col: true,
label: '已撤回',
amount: 800,
},
{
label: '待立案',
amount: 6165,
children: [
{
label: '待提交立案审批',
amount: 4215,
},
{
label: '待审批',
amount: 1950,
},
],
},
{
col: true,
label: '待交诉讼费',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
col: true,
label: '已交诉讼费',
amount: 2800,
},
{
col: true,
label: '已撤回',
amount: 800,
},
],
},
},
{
showHeader: false,
calcHeight: '32%',
width: 0.25,
data: {
color: '#F26222',
stage: '庭审准备阶段',
list: [
{
label: '待撤诉处理',
amount: 2150,
},
{
label: '待排期',
amount: 4215,
},
{
label: '待开庭',
amount: 2000,
},
],
},
},
{
showHeader: false,
calcHeight: '32%',
width: 0.25,
data: {
color: '#705AE8',
stage: '庭审阶段',
list: [
{
label: '开庭中',
amount: 8045,
children: [
{
label: '被告到庭',
amount: 4645,
},
{
label: '被告不到庭',
amount: 3400,
},
],
},
],
},
},
{
showHeader: false,
calcHeight: '32%',
width: 0.25,
data: {
color: '#F26222',
stage: '裁判阶段',
list: [
{
label: '当庭宣判',
amount: 960,
point: true, // 重点,图上主题色显示
},
{
label: '非当庭宣判',
amount: 4760,
},
],
},
},
{
showHeader: false,
calcHeight: '32%',
width: 0.5,
data: {
color: '#E23E6A',
stage: '结案阶段',
list: [
{
label: '待结案',
amount: 2760,
point: true, // 重点,图上主题色显示
},
{
label: '已结案',
amount: 8430,
children: [
{
label: '待归档',
amount: 1200,
},
{
label: '已归档',
amount: 2000,
point: true, // 重点,图上主题色显示
},
],
},
{
label: '已生效',
amount: 12645,
children: [
{
label: '待申请执行',
amount: 2500,
point: true, // 重点,图上主题色显示
},
{
label: '已申请执行',
amount: 2500,
point: true, // 重点,图上主题色显示
},
],
},
{
label: '已归集材料',
amount: 2760,
point: true, // 重点,图上主题色显示
},
],
},
},
{
showHeader: false,
calcHeight: '32%',
width: 1,
data: {
color: '#508AFF',
stage: '送达汇总',
list: [
{
label: '已分案-立案文书',
amount: 12215,
children: [
{
label: '待送达',
amount: 2000,
},
{
label: '待交公告费',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
label: '送达中',
amount: 2000,
},
{
label: '已送达',
amount: 2000,
},
{
label: '送达退回',
amount: 2000,
},
],
},
{
label: '待开庭-开庭文书',
amount: 11815,
children: [
{
label: '待送达',
amount: 2000,
},
{
label: '待交公告费',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
label: '送达中',
amount: 2000,
},
{
label: '已送达',
amount: 2000,
},
{
label: '送达退回',
amount: 2000,
},
],
},
{
label: '待结案-裁判文书',
amount: 12645,
children: [
{
label: '待送达',
amount: 2000,
},
{
label: '待交公告费',
amount: 4215,
point: true, // 重点,图上主题色显示
},
{
label: '送达中',
amount: 2000,
},
{
label: '已送达',
amount: 2000,
},
{
label: '送达退回',
amount: 2000,
},
],
},
],
},
},
],
Lee2