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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步