前段时间看到懂车帝app有几个效果非常好的汽车展示方面的模块,再加上前年在公司写过一个关于汽车售后业务选择汽车部位的组件,就开始着手把这几个组件编写成一个组件库发布出去供大家使用,当然提供按需引入方式,使用的依然是vuecli3工具其中的vue-cli-service build --target lib做为打包工具,下面会讲如何把打包成独立的js 然后供大家可以选择按需引入的方式减少文件体积,由于保密协议的原因,可能这个项目暂时不能开源,以后会开源,但是我尽量把每个组件的关键思路提供出来。图片资源部分引用的是懂车帝和乐车的,如有侵权请联系我删除。
一、技术栈
vuecli3
二、效果演示
三、创建项目
通过vuecli3创建一个初始化项目,具体可以查看官网。cli.vuejs.org/zh/guide/cr…
四、调整项目结构
我们需要一个文件夹存放组件,一个文件夹存放示例,按照以下方式对目录进行改造。
- examples // 原 src 目录,改成 examples 用作示例展示
- packages // 新增 packages 用于编写存放组件,如下图为我的项目结构
五、配置项目以支持新的目录结构
src目录更名为examples,导致项目无法运行
-
注:vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。
-
重新配置入口,修改配置中的 pages 选项
-
新版 Vue CLI 支持使用 vue.config.js 中的 pages 选项构建一个多页面的应用。
-
这里使用 pages 修改入口到 examples,设置css不单独提取。
module.exports = {
// 修改 src 目录 为 examples 目录
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
css: { extract: false }
}
六、编写组件库大框以及配置按需打包命令
以上我们已配置好对新目录架构的支持,接下来我们尝试编写组件库。我大概说下流程:
- 在 packages 目录下,所有的单个组件都以文件夹的形式存储,所以这里创建三个文件夹 分别是carMaintenanceModel,showCar360degrees,pano对应上面预览的三个组件
- 在 每个文件夹下创建 src 文件夹存储组件源码
- 在 每个文件夹下创建 index.js 文件对外提供对组件的引用,其中一个例子
- 创建组件的统一入口文件/packages/index.js,导出所有组件
- 这样如果所有的组件都写完,就可以打包了,这里说下打包都命令,如果想要把所有组件单独打包出来需要做一些额外的配置:
- 根目录创建build/webpack.component.js
- 根目录创建components.json文件
- package.json里配置打包命令 以lib模式 组件名为carmodelcomponents 输出目录lib 至此整体大的框架已经出来了,打包出的js可以看下面的图,把其余没用的文件删掉,剩余三个单独的组件js以及一个整体的.umd.min.js的文件
七、选择汽车部位的大概思路
- 首先这个肯定不是3d模型,是由四张背景图片控制display: none;来显示当前对应的图片。
- 应用map area 来获取触碰的部位,这个可以做到获取到很细化的具体部位。
- 定义一个map
- 然后四个不同汽车模型方位的图所展示的部位图形都不一样,但是不管哪张图里点击之后获取的部位肯定都是一样的,这样比如点击前保险杠,通过获取了classname名,来控制所有带前保险杠图的前保险杠由隐藏变为显示或显示变隐藏,并通过_this.$emit("onChange", [_this.result, _this.arr]);把部位数组以及对应的中文名数组发给父组件。
- 这基本就是这个组件的思路,但是里面的代码很多,很细,这里没办法说全,以后有机会开源大伙自己看。
八、360度汽车展示的大概思路
- 360度汽车展示大家刚看到的时候是不是感觉也是一个3d模型,其实并不是,一句话就说明白,就是由大概36张不同位置的图片控制其opacity: 0 o r 1;。图片的多少自己控制,多了展示的细腻,对性能略损耗,少了体验效果不好,丰俭由人,自行根据实际项目做取舍,当然懂车帝是36张,我使用了懂车帝的图片资源,方便省事,侵权请联系我删除。
- 需要注意的就是图片没加载全时候要给一个图片加载的百分比以及虚化样式,代码下面发出来,这个组件自己业余写的,不涉及保密协议。
<template>
<div class="showCar360degrees">
<div class="image-container" >
<div class="cpt-wg-360" @touchmove = touchmove @touchstart = touchstart :style="{ filter: filterblur }">
<div class="wg-item" v-for="(item,index) in imgs" :key="item" >
<img
v-if="index == 0"
style="opacity: 1;"
class="wg-img"
:src=item
/>
<img
v-else
style="opacity: 0;"
class="wg-img"
:src=item
/>
</div>
</div>
<span v-show = percentageflag class="loading-tip">{{percentage}}%</span>
</div>
</div>
</template>
<script>
export default {
name: "showCar360degrees",
data() {
return {
eventtouches:{},
percentage:0,
percentageflag:true,
filterblur: 'blur(2px)'
};
},
props: {
//图片数组
imgs: Array,
//速度调节
speed:{
type: Number,
default: 20
},
},
mounted(){
this.calculatepercentage()
},
methods: {
calculatepercentage(){
let imgele = document.getElementsByClassName('wg-img')
if(imgele.length>0){
let num = 100 / imgele.length
num = Math.ceil(num)
for(let i = 0;i<imgele.length;i++){
imgele[i].onload = ()=>{
this.percentage += num
if(this.percentage >= 100){
this.percentage = 100
setTimeout(() => {
this.percentageflag = false
this.filterblur = 'blur(0px)'
}, 100);
}
}
}
}
},
touchstart(e){
this.eventtouches = e;
},
touchmove(e){
if((e.timeStamp - this.eventtouches.timeStamp)>this.speed){
if((e.touches[0].clientX - this.eventtouches.touches[0].clientX)>0 ){
this.right()
}else {
this.left()
}
this.eventtouches = e;
}
},
left(){
let domlist = Array.from( document.getElementsByClassName('wg-img'));
for(let i= 0; i < domlist.length; i++){
if(domlist[i].style.opacity == '1'){
domlist[i].setAttribute('style', 'opacity: 0;');
let j = i == 0 ? domlist.length-1: i-1
domlist[j].setAttribute('style', 'opacity: 1;');
return
}
}
},
right(){
let domlist = Array.from( document.getElementsByClassName('wg-img'));
for(let i= 0; i < domlist.length; i++){
if(domlist[i].style.opacity == '1'){
domlist[i].setAttribute('style', 'opacity: 0;');
let j = i == domlist.length-1 ? 0: i+1
domlist[j].setAttribute('style', 'opacity: 1;');
return
}
}
}
},
};
</script>
<style scoped>
.image-container {
display: flex;
justify-content: center;
position: relative;
width: 100%;
height: 160px;
background-image: linear-gradient(0deg, hsla(0, 0%, 100%, 0), #ccc);
}
.cpt-wg-360 {
width: 100%;
height: 100%;
position: relative;
min-height: 160px;
overflow: hidden;
}
.cpt-wg-360 .wg-item {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 100%;
padding: 0 9.6%;
box-sizing: border-box;
margin-top: -15px;
}
.cpt-wg-360 .wg-img {
width: 100%;
}
.image-container .loading-tip {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid #ffe100;
background: rgba(0,0,0,.5);
color: #fff;
font-size: 10px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
display: flex;
align-items: center;
justify-content: center;
}
img {
border-style: none;
}
</style>
九、全景图的大概思路
- 全景图我这边就是用three.js实现的一个基础版本,原理也比较简单,但是建议先去看下three.js的教程,了解下各种概念,比如场景,几何体,材质,相机,渲染器等等吧
- 简单理解的原理就是先创建一个球体,然后往球体内壁贴上全景图(也是懂车帝的全景图,省事,真香),把相机(其实就是眼睛)放在球体里,把网格添加到场景里,把场景和相机添加到渲染器中,最后时时渲染,滑动时改变相机观察角度。具体代码下面发出来,业余时间编写,不涉及保密协议。
<template>
<div class="pano">
<div id="container" ></div>
</div>
</template>
<script>
/* eslint-disable */
import * as THREE from "three";
export default {
name: "pano",
data() {
return {
camera: null,
scene: null,
renderer: null,
mesh: null,
onMouseDownMouseX : 0,
onMouseDownMouseY : 0,
lon : 180,
onMouseDownLon : 0,
lat : 0,
onMouseDownLat : 0,
phi : 0,
theta : 0,
isUserInteracting:false
};
},
props: {
imgurl:{
type:String,
default:''
}
},
mounted() {
this.init();
this.animate();
this.addEvent();
},
methods: {
addEvent(){
this.$el.addEventListener('touchstart', this.start, false)
this.$el.addEventListener('touchmove', this.move, false)
this.$el.addEventListener('touchend', this.end, false)
this.$el.addEventListener('mousedown', this.start, false)
this.$el.addEventListener('mousemove', this.move, false)
this.$el.addEventListener('mouseup', this.end, false)
},
init: function() {
let container = document.getElementById("container");
let opt={
fov:90,
width:container.clientWidth,
height:container.clientHeight,
}
//创建相机
this.camera = new THREE.PerspectiveCamera(
opt.fov, opt.width / opt.height, 1, 10000
);
this.camera.target = new THREE.Vector3(0, 0, 0); //指定看向哪个点
//创建几何体
let geometry = new THREE.SphereBufferGeometry(60, 60, 60);
geometry.scale(-1, 1, 1); // 贴图朝里
// 加载贴图
let texture = new THREE.TextureLoader().load(
// ""
this.imgurl
);
//创建
let material = new THREE.MeshBasicMaterial({
map: texture
});
this.mesh = new THREE.Mesh(geometry, material);
this.scene = new THREE.Scene();
this.scene.add(this.mesh);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(opt.width, opt.height);
container.appendChild(this.renderer.domElement);
},
start(event){
this.isUserInteracting = true;
var clientX = event.clientX || event.touches[0].clientX; //获取点击开始时的屏幕位置
var clientY = event.clientY || event.touches[0].clientY;
this.onMouseDownMouseX = clientX;
this.onMouseDownMouseY = clientY;
this.onMouseDownLon = this.lon; //经度
this.onMouseDownLat = this.lat; //纬度
},
move(event){ //算出经度纬度移动的距离
if(this.isUserInteracting ){
var clientX = event.clientX || event.touches[0].clientX;
var clientY = event.clientY || event.touches[0].clientY;
this.lon = (this.onMouseDownMouseX - clientX ) * 0.2 + this.onMouseDownLon;
this.lat = ( clientY - this.onMouseDownMouseY ) * 0.2 + this.onMouseDownLat;
}
},
end(event){
this.isUserInteracting = false;
},
animate: function() {
requestAnimationFrame(this.animate);
this.lon += 0.1
this.lat = Math.max(-85, Math.min(85, this.lat));
this.phi = THREE.Math.degToRad(90 - this.lat);
this.theta = THREE.Math.degToRad(this.lon);
this.camera.target.x = Math.sin(this.phi) * Math.cos(this.theta);
this.camera.target.y = Math.cos(this.phi);
this.camera.target.z = Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.camera.target);
this.renderer.render(this.scene, this.camera);
}
}
};
</script>
<style scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>
十、组件库的使用
- yarn add carmodelcomponents 现在版本是1.0版本 后续我有时间可能会把全景图那个完善更多功能
- 下面贴出代码 两种引入方式 自行选择
<template>
<div id="app">
<carMaintenanceModel :carpart = carpart v-on:onChange=carChange></carMaintenanceModel>
<showCar360degrees :speed = 20 :imgs = imgs ></showCar360degrees>
<pano imgurl = 'https://p9-dcd.byteimg.com/img/mosaic-legacy/bef400005f4eca28e99e~tplv-resize:4000:0.image'></pano>
</div>
</template>
<script>
import Vue from 'vue'
//全部引用
// import carmodelcomponents from 'carmodelcomponents'
// Vue.use(carmodelcomponents)
//按需引用
import carMaintenanceModel from 'carmodelcomponents/lib/carMaintenanceModel.js'
import showCar360degrees from 'carmodelcomponents/lib/showCar360degrees.js'
import pano from 'carmodelcomponents/lib/pano.js'
Vue.use(carMaintenanceModel)
Vue.use(showCar360degrees)
Vue.use(pano)
export default {
name: 'App',
components: {
},
data(){
return{
imgs:[
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef30000e03696639164~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef30000e0358a61e148~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef10000e2c28d75f043~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef10000e2c10123ffe2~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef4000065de9346a35d~tplv-resize:640:0.png",
"https://p9-dcd.byteimg.com/img/mosaic-legacy/bef20000e5258c0eed4e~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/beef0000e308f1415db3~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef10000e2c510c0b6c0~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef20000e52661db22a3~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef4000065ea77e140ae~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef10000e2c6437dd4d3~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef4000065e9893b1aec~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef4000065e6ff33e833~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef4000065e81a11bb83~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/9b1300016b2ee29ca41e~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef4000065e7cbeb7f66~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef4000065e5ccb3bda9~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef10000e2c460481ef9~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef4000065e46f39caa7~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef30000e038c8917999~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef4000065e321519d3a~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef4000065e234fc8580~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/beef0000e307938c2601~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef00000e2f05c5b3d90~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef10000e2c313c5c2b8~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/9b1300016b2d7cb46459~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/9b1300016b2b3c80acdf~tplv-resize:640:0.png",
"https://p9-dcd.byteimg.com/img/mosaic-legacy/bef20000e5248ce4558b~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef4000065e129d49497~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/bef4000065e060e466a2~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/9b1300016b2cf2fcd008~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef4000065df1aabda54~tplv-resize:640:0.png",
"https://p1-dcd.byteimg.com/img/mosaic-legacy/bef20000e5222d9c040b~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef20000e523c6b69e37~tplv-resize:640:0.png",
"https://p3-dcd.byteimg.com/img/mosaic-legacy/bef30000e037e7ee5961~tplv-resize:640:0.png",
"https://p6-dcd.byteimg.com/img/mosaic-legacy/beef0000e30646247719~tplv-resize:640:0.png"
],
carpart: ['component-1', 'component-2'],
}
},
methods:{
carChange(e){
console.log(e);
},
}
}
</script>
<style>
body{
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
十一、总结
至此这篇文章就结束了 如果还有什么问题评论区留言,我会更改,有好的需求也可以提,如果需求合理并且在我能力范围内,我会加上去。希望看完别忘记点赞,全布手敲。 后面就是npm发包了,如有需要可以关注我上一篇文章 => juejin.cn/post/684490…