边界
- 非纯技术型团队,承接完成市场项目即可挣钱
- 有一定量的自定义效果(有UI介入设计)
- 有一定量的定制功能,会指定适配的屏幕分辨率
- 页面不需要特别酷炫
- 项目数量有一定规模,每个项目都存在大量重复开发
方案选择
从市场需求、团队整体开发能力、实际产出效率、稳定性等方面考量,选择哪一种方式完成一个大屏项目花费的时间、人力成本更少,更易落地。
-
模板化项目
优点:完整的初始项目,立马可以部署
缺点:在模板项目代码基础上修改,适配新的UI图,代码冗余。修改的时间成本可能很高,并且代码质量不高
-
可视化拖拽
优点:运营或非技术人员可以自主生成页面
缺点:样式过于生硬,想要二次开发难度较大,自研拖拽系统需要成本。使用其他平台的可视化生成系统,价格不菲
-
项目预设
优点:模板化项目的一种,但只保留最小预设和约定,具体业务代码重新开发。
缺点:几乎每个页面都需要从0开发
经过几个项目实践下来,因为团队在开发大屏方面都是很有经验的小伙伴,重新开发也不慢,代码质量比起修改已有代码会更高,没有过多的冗余代码,并且可以完全适配所有定制内容,所以选择第三种,那么具体就看第三种怎么样能够做到更高效。
具体预设内容
采用VUE技术栈,直接vue-cli(@vue/cli 4.5.12)生成初始项目。
1. 布局
基本布局一般不会变,留出主要功能模块卡槽,包括顶部导航,底部菜单,每个项目按区块定制即可。
各个板块的尺寸根据项目要求调整。
App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<style lang="less">
#app {
height: 100%;
color: #2c3e50;
text-align: center;
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
layout.vue
<template>
<div class="page-wrap">
<div class="head">
<!-- 顶部导航 -->
<TopNavigator />
</div>
<div class="center-wrap">
<!-- 中间地图部分 -->
<CenterMap />
<!-- 主体内容页面 -->
<router-view></router-view>
</div>
<div class="foot">
<!-- 底部菜单 -->
<BottomMenu />
</div>
</div>
</template>
<script>
import TopNavigator from './topNavigator'
import CenterMap from './centerMap'
import BottomMenu from './bottomMenu'
export default {
components: { TopNavigator, CenterMap, BottomMenu }
}
</script>
<style lang="less" scoped>
.page-wrap {
position: relative;
height: 100%;
.head {
position: fixed;
z-index: 1;
height: 1rem;
width: 100%;
background-color: fade(#eee, 50%);
}
.foot {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 19.4rem;
height: 2.1rem;
z-index: 0;
background-color: fade(#eee, 50%);
}
.center-wrap {
position: relative;
height: 100%;
overflow-y: auto;
}
}
</style>
主体内容页
<template>
<div class="playground">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
</template>
<script>
export default {}
</script>
<style lang="less" scoped>
.playground {
position: relative;
height: 0;
.left,
.right {
position: absolute;
padding: 0.32rem 0.34rem 0.29rem 0.48rem;
top: 1rem;
width: 5rem;
height: 8rem;
border: 1px solid #3196fa88;
border-radius: 0.24rem;
background-image: linear-gradient(180deg, rgba(49, 150, 250, 0.25), rgba(49, 150, 250, 0));
}
.left {
left: 0.48rem;
border-left: none;
}
.right {
right: 0.48rem;
border-right: none;
}
}
</style>
2. css
@import './variable.less'; // 变量声明
@import './reset.less'; // 重置默认样式
@import './element-ui/index.less'; // 重写element-ui默认样式
@import './common.less'; // 通用样式
@import './tool.less'; // 原子类
@import './animation.less'; // 动画类
common.less
html {
font-size: 100px;
}
// 内容板块背景图
.bg1 {
background: url('~@/assets/img/bg1.png') no-repeat top left;
background-size: 100% 100%;
}
variable.less
这里需要跟UI协商好切图的风格,颜色、字体等,达成一致,完成定义。
@baseUnit: 0.04rem; // 最小单位
@defaultFontSize: 0.14rem; // 默认字体大小
// color
@primary: #3196fa;
@border: #e8e8e8;
@blue1: #1890ff;
@blue2: #b0d0ee;
@blue3: #99d0f2;
@blue4: #5bebff;
@red: #ff4d4f;
@red1: #f56c6c;
@yellow: #facc14;
@yellow1: #ffae00;
@gray: rgba(0, 0, 0, 0.45);
tool.less
其中包括margin、padding、字体、颜色、width、line-height、border、布局等原子样式。
// 生成padding、margin工具类
// 比如 .mt1 = "margin-top: 0.04rem"、mb2 = "margin-bottom: 0.08rem"
.loopPaddingMargin(@counter) when (@counter > 0) {
.p@{counter} {
padding: (@baseUnit * @counter);
}
.pt@{counter} {
padding-top: (@baseUnit * @counter);
}
.pr@{counter} {
padding-right: (@baseUnit * @counter);
}
.pb@{counter} {
padding-bottom: (@baseUnit * @counter);
}
.pl@{counter} {
padding-left: (@baseUnit * @counter);
}
.m@{counter} {
margin: (@baseUnit * @counter);
}
.mt@{counter} {
margin-top: (@baseUnit * @counter);
}
.mr@{counter} {
margin-right: (@baseUnit * @counter);
}
.mb@{counter} {
margin-bottom: (@baseUnit * @counter);
}
.ml@{counter} {
margin-left: (@baseUnit * @counter);
}
.loopPaddingMargin((@counter - 1)); // 递归调用
}
.loopPaddingMargin(20);
// 生成fontSize工具类,比如 .f14
.loogFontSize(@counter) when (@counter > 10) {
.f@{counter} {
font-size: (0.01rem * @counter);
}
.loogFontSize((@counter - 1));
}
.loogFontSize(60);
.b { font-weight: bold; } .n { font-weight: normal; }
// 字体颜色, 背景色(less >= 3.7)
@colorList: {
primary: @primary;
blue1: @blue1;
blue2: @blue2;
blue3: @blue3;
blue4: @blue4;
red: @red;
red1: @red1;
yellow: @yellow;
yellow1: @yellow1;
gray: @gray;
};
each(@colorList, {
.bg-@{key} {
background: @value !important;
}
.c-@{key} {
color: @value !important;
}
});
// 生成width宽度工具类,比如 .w200
.loogWidth(@counter) when (@counter > 0) {
.w@{counter} {
width: (0.01rem * @counter) !important;
}
.loogWidth((@counter - 4));
}
.loogWidth(600);
.pct100 {
width: 100% !important;
}
// line-height
.loogLineHight(@counter) when (@counter > 0) {
.lh@{counter} {
line-height: (0.01rem * @counter) !important;
}
.loogLineHight((@counter - 1));
}
.loogLineHight(40);
// border
.bdr {
border: 1px solid @border;
}
.bdr-l {
border-left: 1px solid @border;
}
.bdr-r {
border-right: 1px solid @border;
}
.bdr-t {
border-top: 1px solid @border;
}
.bdr-b {
border-bottom: 1px solid @border;
}
// 布局
.tl {
text-align: left;
}
.tr {
text-align: right;
}
.tc {
text-align: center;
}
.dib {
display: inline-block;
}
.flex-box {
display: flex;
justify-content: space-between;
align-items: center;
}
// other
.finger {
cursor: pointer;
}
.ell {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ell-2row {
text-overflow: -o-ellipsis-lastline;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
animation.less
.fade-in {
.setKeyframe(fadeIn);
}
.fade-in-up {
.setKeyframe(fadeInUp);
}
.setKeyframe(@keyframeName) {
animation-name: @keyframeName;
animation-duration: 1s;
animation-iteration-count: 1;
animation-delay: 0s;
animation-fill-mode: 'both';
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
margin-top: 0.3rem;
}
to {
opacity: 1;
margin-top: 0;
}
}
element-ui/index.less
.ele-table1 {
background: transparent;
border-bottom: none;
&.el-table {
/deep/ table {
border-collapse: separate;
border-spacing: 0px 0.08rem;
// 表头不需要border-spacing
&.el-table__header {
border-spacing: 0;
}
tr {
background: transparent;
&:hover {
td {
background: #2debe033;
}
}
&.el-table__row {
background: #2debe011;
}
th {
color: #fff;
font-size: 0.14rem;
font-weight: bold;
background: transparent;
padding: 0.08rem 0 0 0;
border: none;
}
td {
color: #ecf5ff;
background: #2debe011;
padding: 0.08rem 0;
font-size: 0.14rem;
border: none;
}
}
.cell {
line-height: 0.14rem;
}
}
}
// 底下始终有根线,去掉
&--border::after,
&--group::after,
&::before {
display: none;
}
}
3. echarts图表
为什么没有直接做组件,因为每个项目对图表的定制性其实很强,组件复用率不高。所以同样采取做一些基本预设的方式(通过Vue.extend方法混人)。
mixins
import { deepMerge } from '@/utils'
const defaultFontSize = 16
// 通用配置
const commonOptions = {
color: ['#fafb02', '#2debe0', '#2390ff', '#ff9936', '#b65fff'],
textStyle: {
color: '#fff',
fontSize: defaultFontSize,
},
grid: {
left: '10%',
right: '0%',
top: '5%',
bottom: '8%',
},
legend: {
textStyle: {
color: '#fff',
},
},
xAxis: {
axisLabel: {
color: '#eee',
fontSize: defaultFontSize,
},
axisLine: {
lineStyle: {
color: '#fff',
},
},
},
yAxis: {
axisLabel: {
color: '#eee',
fontSize: defaultFontSize,
},
},
}
export default {
data() {
return {}
},
methods: {
// 混入通用配置的函数
mixinEcharts(options) {
return deepMerge(commonOptions, options)
},
},
updated() {},
}
main.js
import EchartsMixin from '@/mixins/echarts'
Vue.mixin(EchartsMixin)
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app')
bar.vue
在组件中调用this.mixinEcharts(options)方法,混入通用配置
mixinOption() {
const barSeries = {
type: 'bar',
itemStyle = {
borderRadius: [15, 15, 0, 0]
}
}
const options = {
legend: { show: true },
dataset: {
dimensions: ['product', '2015', '2016', '2017', '2018', '2019'],
source: [
{ product: '棉花', '2015': 43.3, '2016': 85.8, '2017': 93.7, '2018': 93.7, '2019': 93.7 },
{ product: '玉米', '2015': 83.1, '2016': 73.4, '2017': 55.1, '2018': 63.7, '2019': 73.7 },
{ product: '稻谷', '2015': 86.4, '2016': 65.2, '2017': 82.5, '2018': 93.7, '2019': 23.7 },
{ product: '小麦', '2015': 72.4, '2016': 53.9, '2017': 39.1, '2018': 13.7, '2019': 83.7 },
{ product: '高粱', '2015': 72.4, '2016': 53.9, '2017': 39.1, '2018': 13.7, '2019': 83.7 }
]
},
xAxis: {
type: 'category',
},
series: [
barSeries,
barSeries,
barSeries,
barSeries,
barSeries,
{ type: 'line', color: ['#eee'] }
]
}
// 这里混入通用配置!!!
return this.mixinEcharts(options)
}
4. utils
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
function isArray(arr) {
return Array.isArray(arr)
}
// 合并两个对象
export const deepMerge = (target, ...arg) => {
return arg.reduce((old, cur) => {
return Object.keys(cur).reduce((subAttr, key) => {
const srcVal = cur[key]
// 如果相同属性值的 基本数据类型冲突并且后者是object类型,直接取后者的值
// 因为这种情况会报错,比如 { a1: 1 } 和 { a1: {x: 1, y: 2 } }
if (typeof subAttr[key] !== typeof srcVal[key] && typeof srcVal[key] === 'object') {
subAttr[key] = srcVal[key]
}
if (isObject(srcVal)) {
subAttr[key] = deepMerge(subAttr[key] ? subAttr[key] : {}, srcVal)
} else if (isArray(srcVal)) {
subAttr[key] = srcVal.map((item, idx) => {
if (isObject(item)) {
const curAccVal = subAttr[key] ? subAttr[key] : []
return deepMerge(curAccVal[idx] ? curAccVal[idx] : {}, item)
} else {
return item
}
})
} else {
subAttr[key] = srcVal
}
return subAttr
}, old)
}, target)
}
// 根据适配屏幕分辨率情况转换px值
export const parsePx = (src) => {
...
}
5. 后续 todo
地图接口封装,不需要关心地图厂商底层实现
cli直接生成项目初始模板
浙江大华技术股份有限公司-软研-智慧城市产品研发部招聘高级前端,欢迎来撩,有意向可发送简历到chen_zhen@dahuatech.com,长期有效