前言:
开发完成的这个项目是基于nuxt.js框架和typescript语法的服务端渲染项目,具有良好的SEO体验。目前项目处于预发布阶段。开发用时约30天,测试约15天,共约26个页面,接口约50个,封装了一些公共组件,也用到一些三方组件,如seiper轮播图插件,加密插件、day.js插件。
兼容:项目兼容几乎所有浏览器,IE兼容到11
重点功能:SEO搜索引擎优化、直播功能、直播回顾功能、权限验证
1.总结(个人感受)
1.对于UI页面的精确度已经达到了惨绝人寰的程度,UI人员精确到1px就罢了,今天跑过来说调整一下这个地方,明天又跑来说这个地方再改下…(绝对是巨坑,最坑没有之一,浪费LZ大量的时间和精力,无力吐槽…)
2.nuxt.js框架的使用,是有坑的,比如说获取window对象,比如获取vue对象,还有个大坑就是,因为是服务端渲染。一般基地址就是两个,一个开发地址一个线上地址,但是在这里至少需要3个地址,一个开发的,一个客户端请求的,一个服务器请求的,要做判断区分。
3.项目中的首页大量使用seiper轮播图插件,高版本比如7兼容性不好,项目用的4,但是也存在问题,一个巨坑是缩略图轮播,thumbs老是报错(初始化正常,切换tab栏回去的时候报错,刷新页面就好),说找不到addclass,位置在init 7700多行,死活排查不出来,最后我是通过在错误页面强行使用this.$router.go(0)刷新页面。
4.关于三方插件,如时间格式转换的,推荐day.js插件,非常好用。再比如加密插件,使用的是crypto.js加密插件,使用也是非常简单,存的是时候加密一下,取出来的时候再解密一下
5.使用如elementUI组件库的时候,要修改它们样式,记得使用::v-deep
2.rem适配[1920*1080设计图]
2.1 在static目录下新建js文件 page:flexble
(function flexible (window, document) { var docEl = document.documentElement var dpr = window.devicePixelRatio || 1 // adjust body font size function setBodyFontSize () { if (document.body) { document.body.style.fontSize = (12 * dpr) + 'px' } else { document.addEventListener('DOMContentLoaded', setBodyFontSize) } } setBodyFontSize(); // set 1rem = viewWidth / 10 function setRemUnit () { // var rem = docEl.clientWidth / 10 // docEl.style.fontSize = rem + 'px' var width = docEl.getBoundingClientRect().width // 当屏幕超过1920px以后就不在随着屏幕的变大而变大了 if (width / dpr > 1920) { width = 1920 * dpr } // 当屏幕小于1300px以后就不再随着屏幕的变小而变小了 // if (width / dpr < 1300) { // width = 1300 * dpr // } var rem = width / 19.2 docEl.style.fontSize = rem + 'px' window.rem = rem } setRemUnit() // reset rem unit on page resize window.addEventListener('resize', setRemUnit) window.addEventListener('pageshow', function (e) { if (e.persisted) { setRemUnit() } }) // detect 0.5px supports if (dpr >= 2) { var fakeBody = document.createElement('body') var testElement = document.createElement('div') testElement.style.border = '.5px solid transparent' fakeBody.appendChild(testElement) docEl.appendChild(fakeBody) if (testElement.offsetHeight === 1) { docEl.classList.add('hairlines') } docEl.removeChild(fakeBody) } }(window, document))
2.2 在assets文件夹下新建css文件
@use "sass:math"; $base-width: 1920; $base-font_size: math.div(1920 , 19.2); @function rem($px){ @return math.div($px , $base-font_size) * 1rem; // @return $px * 1px } /** 字体 **/ $large-font_size: 16px ; $medium-font_size: 14px; $small-font_size: 12px; $title-size: rem(44); /** 颜色 **/ $primary-color: #C90000; $bottom-color: #D10000; $oss_url:'//oss-guanwang.yuceyingjia.com/'; $base-oss_url: '//oss-guanwang.yuceyingjia.com/newGWWeb/'; :export { ossUrl: $base-oss_url; }
2.3 在nuxt.config.js设置配置
// 引入全局scss 变量 styleResources: { // your settings here scss: ['./assets/css/variables.scss'], hoistUseStatements: true // Hoists the "@use" imports. Applies only to "sass", "scss" and "less". Default: false. },
script: [ { src: '/js/page-flexible.js'} ]
2.4 页面中使用
font-size:rem(15)
3. 登录注册页
登录、注册、忘记密码(重置)均在一个页面上,有密码校验、本地持久化,记住密码、短信验证码、加密、路由跳转等功能
封装成一个组件(使用cookie):
<template> <div> <div class="container1"> {{$route.query.zc}} <!-- 左侧注册区域 --> <div class="left"> <div class="zhuce"> <!-- logo title --> <div class="logo"><img src="~/assets/img/login/LOGO.png" alt="" /></div> <!-- 注册表单区域 --> <div class="formTable"> <el-form :rules="rules" ref="formData" :model="formData"> <el-form-item prop="mobile" v-show="wangji || !forGet"> <el-input v-model="formData.mobile" placeholder="请输入您的手机号" > </el-input> </el-form-item> <el-form-item prop="email" class="article" v-if="cLogin || forGet " v-show="wangji|| !forGet" > <el-input v-model="formData.email" maxlength="40" placeholder="手机验证码" class="input" > </el-input> <span class="span" @click="getCMS" v-if="cLogin "> <span v-show="isC == false">获取验证码</span> <span v-show="isC == true">{{ isActive ? "重新获取" : count + "s" }}</span></span > </el-form-item> <!-- <el-form-item prop="Yzm" v-if="cLogin || forGet" v-show="wangji|| !forGet"> --> <el-form-item prop="Yzm" v-if="imgUrl"> <el-input v-model="formData.Yzm" maxlength="40" placeholder="图形验证码" class="input" > </el-input> <img :src="imgUrl" alt="" class="img"/> </el-form-item> <el-form-item prop="miMa" v-if="!forGet"> <el-input v-model="formData.miMa" placeholder="您的账号密码" ></el-input> </el-form-item> <!-- 修改密码区域 forGet控制 --> <el-form-item prop="newPassword" v-if="!wangji && forGet"> <el-input type="password" placeholder="请输入新密码" v-model="formData.newPassword"></el-input> </el-form-item> <el-form-item prop="repPassword" v-if="!wangji&& forGet"> <el-input type="password" placeholder="请输入确认密码" v-model="formData.repPassword"></el-input> </el-form-item> <!-- 记住密码、忘记密码区域 --> <div class="middle" v-if="!cLogin"> <div class="left"> <el-checkbox v-model="checked">记住密码</el-checkbox> </div> <div class="right" v-if="!forGet"><span @click="missCode">忘记密码</span></div> </div> <!-- 提交表单按钮 --> <el-form-item align="center" class="div" v-if="!wangji"> <el-button size="mini" type="primary" :loading="loading" @click="submitForm('formData')" >{{buttonText}}</el-button > </el-form-item> <el-form-item align="center" class="div" v-else> <el-button size="mini" type="primary" :loading="loading" @click="next" >下一步</el-button > </el-form-item> <!-- 底部 还没有账号? 立即注册区域 --> <div class="bottom" v-if="!wangji" > <span class="left">{{cLogin ? '已有账号?':'还没有账号?'}}</span> <span class="right" @click="clickLogin">{{cLogin ? '立即登录':'立即注册'}}</span> </div> </el-form> </div> </div> </div> <!-- 右侧图片区域 --> <div class="right"> <img src="~/assets/img/login/bg.png" alt="" /> <div class="img"><img src="~/assets/img/login/login_slogin.png" alt=""></div> <div class="zhezhao"></div> <!-- <div class="textIMG"> <h3>让投资更简单更理性</h3> <p>Make investment simpler and</p> <p>more rational</p> </div> --> </div> </div> </div> </template> <script> import CryptoJS from "crypto-js";//加密 import { isvalidUsername, isvalidMobile, isvalidEmail, code, password } from '@/utils/validate' import axios from 'axios' // import { Alert } from 'element-ui' const TIME_COUNT = 60 export default { props: { loading: { // 是否点击确定按钮 type: Boolean, default: false } }, methods: { //提交表单 submitForm (formName) { this.$refs[formName].validate((valid) => { if (valid) { // 校验通过,提交数据 this.getIMGCode()//调图形验证码,默认为空 if(this.forGet){ // 重置密码 this.getReset() }else{ if(this.cLogin===false){ // 做登录 this.getLogin () }else{ // 做注册 this.getZhuCe () } } } else { // 验证不通过 return false } }) }, // 封装的倒计时效果 getCMS () { this.isC = true if (!this.timer) { this.count = TIME_COUNT this.isActive = false this.getCMSCode() //获取验证码(ip)----60秒内不能重复发送-- this.timer = setInterval(() => { if (this.count > 0 && this.count <= TIME_COUNT) { this.count-- } else { this.isActive = true clearInterval(this.timer) this.timer = null } }, 1000) } }, // 做登录 async getLogin () { let data1 = await axios.post("https://app-gw-test.365ycyj.com/UserApi/50000/UserLogin", { username: this.formData.mobile, password: this.formData.miMa, }) console.log(data1, 'login--------------11111111111111111') if(data1.data.Msg == 'success'){ this.$message({ message: '恭喜登录成功!', type: 'success' }); console.log(data1.data,'wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww'); // 如果记住密码的话 if(this.checked){ localStorage.setItem("rememberPsw",true) let cipherText = CryptoJS.AES.encrypt(this.formData.miMa, "secretkey123").toString(); this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token,userInfo:this.formData.mobile,pwd:this.formData.miMa}) this.$cookies.set("accessToken",data1.data.Data.Token,{maxAge:60*60*24*3}) //保存3天 this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*24*3}) //保存3天 this.$cookies.set("pwd",cipherText,{maxAge:60*60*24*3}) //保存3天 }else{ // 不记住密码,不保存密码 localStorage.setItem("rememberPsw",false) let cipherText = CryptoJS.AES.encrypt(this.formData.miMa, "secretkey123").toString(); this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token,userInfo:this.formData.mobile}) // this.$cookies.set("pwd",'') // // this.$cookies.set("accessToken",'') // this.$cookies.set("accessToken",data1.data.Data.Token,{maxAge:60*60*4}) //保存4小时 this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*4}) //保存4小时 this.$cookies.set("pwd",cipherText,{maxAge:60*60*4}) //保存4小时 // this.$cookies.set("accessToken",data1.data.Data.Token,{maxAge:60*60*4}) //保存4小时 // this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*4}) //保存4小时 } this.$router.push(this.$route.query.return_url || '/') //登录后跳转到原位置或者首页 // this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token2,userInfo:this.formData.mobile}) // this.$cookies.set("accessToken",data1.data.Data.Token2,{maxAge:60*60*24*3}) //保存3天 // this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*24*3}) //保存3天 // this.$router.go(0); //刷新页面 }else { this.$message.error(data1.data.Msg); // this.$router.push({path: '/', query:{id: 3}}); } }, // 做注册 async getZhuCe () { let data1 = await axios.post("https://app-gw-test.365ycyj.com/UserApi/50000/MReg", { tel: this.formData.mobile, pwd: this.formData.miMa, smscode:this.formData.email, app: '官网', issendmsg: '1', qudaotype: '2', clienttype: '0', mac: '', clientagent: 'pc_ycyj', qudaoma: '', }) console.log(data1, 'login--------------11111111111111111') if(data1.data.Msg == 'success'){ this.$message({ message: '恭喜您注册成功!请登录', type: 'success' }); this.cLogin = false //去登陆 // 如果是注册的话,那么登录后应该下载软件 this.isDownPC = true // this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token2,userInfo:this.formData.mobile}) // this.$cookies.set("accessToken",data1.data.Data.Token2,{maxAge:60*60*24*3}) //保存3天 // this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*24*3}) //保存3天 // this.formData.mobile = '' // 关闭弹框 // this.$router.go(0); //刷新页面 }else { this.$message.error(data1.data.Msg); } }, // 接口--获取短信验证码--ip限制--默认为空GetSMSCodeByWeb async getCMSCode(){ let data1 = await axios.post("https://app-gw-test.365ycyj.com/UserApi/50000/GetSMSCodeByWeb", { tel: this.formData.mobile || '15805494663', imgcode:this.formData.Yzm ||'', yingjiatype:'0', type:'0' }) console.log(data1,'ceshi 短信验证码……………………………………………………'); }, // 接口--获取图形验证码--ip限制 async getIMGCode(){ let data1 = await axios.post("https://app-gw-test.365ycyj.com/UserApi/50000/GetImgCodeByWeb") if(data1.Data){ this.imgUrl = data1.Data.ImgSrc } }, // 做忘记密码--密码重置 async getReset () { let data1 = await axios.post("https://app-gw-test.365ycyj.com/UserApi/50000/ResetPassword", { tel: this.formData.mobile, password: this.formData.newPassword, smscode:this.formData.email, clienttype: '0', }) console.log(data1, 'getReset--------------11111111111111111') if(data1.data.Msg == 'success'){ this.$message({ message: '恭喜您重置成功!', type: 'success' }); // 如果记住密码的话 if(this.checked){ this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token,userInfo:this.formData.mobile}) this.$cookies.set("accessToken",data1.data.Data.Token,{maxAge:60*60*24*3}) //保存3天 this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*24*3}) //保存3天 }else{ this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token,userInfo:this.formData.mobile}) this.$cookies.set("accessToken",data1.data.Data.Token,{maxAge:60*60*4}) //保存4小时 this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*4}) //保存4小时 } this.$router.push(this.$route.query.return_url || '/') //登录后跳转到原位置或者首页 // this.cLogin = false //去登陆 // this.$store.commit('UPDATE_ALL_STATE',{accessToken:data1.data.Data.Token2,userInfo:this.formData.mobile}) // this.$cookies.set("accessToken",data1.data.Data.Token2,{maxAge:60*60*24*3}) //保存3天 // this.$cookies.set("userInfo",this.formData.mobile,{maxAge:60*60*24*3}) //保存3天 // this.formData.mobile = '' // 关闭弹框 // this.$router.go(0); //刷新页面 }else { this.$message.error(data1.data.Msg); } }, // missCode+clickLogin做忘记密码、注册、登录切换效果 missCode(){ this.wangji = true this.forGet=!this.forGet this.cLogin = true }, clickLogin(){ this.cLogin=!this.cLogin this.forGet = false }, // 点击下一步 next(){ this.wangji = false } }, data () { var checkNickName = (rule, value, callback) => { if (!value) { callback(new Error('姓名不能为空')) } else if (value.length > 30) { callback(new Error('最多30个字符')) } else { callback() } } var checkMobile = (rule, value, callback) => { if (!value) { callback(new Error('手机号不能为空')) } else if (!isvalidMobile(value)) { callback(new Error('手机号不合法')) } else { callback() } } var checkCode = (rule, value, callback) => { if (!value) { callback(new Error('验证码不能为空')) } else if (!code(value)) { callback(new Error('验证码不合法')) } else { callback() } } var checkPass = (rule, value, callback) => { if (value === '') { callback(new Error('密码不能为空')) } else if (!password(value)) { callback(new Error('密码不合法')) } else { callback() } } var checkYZM = (rule, value, callback) => { if (!value) { callback(new Error('图形验证码不能为空')) } else if (!code(value)) { callback(new Error('图形验证码不合法')) } else { callback() } } // 校验新密码 const validatePassword = (rule, value, callback) => { // console.log('value', value) if(value.length < 6) { callback(new Error('新密码不能少于6位')) }else { callback() } }; // 校验确认密码是否一致 const validateRepPassword = (rule, value, callback) => { if(value !== this.formData.newPassword) { callback(new Error('两次输入的密码不一致')) }else { callback() } }; return { rules: { mobile: [{ required: true, validator: checkMobile, trigger: 'blur' }], email: [{ required: true, validator: checkCode, trigger: 'blur' }], miMa: [{ required: true, validator: checkPass, trigger: 'blur' }], Yzm: [{ required: true, validator: checkYZM, trigger: 'blur' }], newPassword: [ { required: true, message: '新密码不能为空', trigger: 'blur' }, { validator: validatePassword, trigger: 'blur' } ], repPassword: [ { required: true, message: '确认密码不能为空', trigger: 'blur' }, { validator: validateRepPassword, trigger: ['change', 'blur'] } ] }, ip: '1.1.1.1', area: '北京市', brower: 'chrome', os: 'windows7', // mobile:this.formData.mobile, imgUrl: "", formData: { mobile:this.$cookies.get('userInfo')? CryptoJS.AES.decrypt(this.$cookies.get('userInfo'), "secretkey123").toString(CryptoJS.enc.Utf8):'', email: '', miMa:this.$cookies.get('pwd')? CryptoJS.AES.decrypt(this.$cookies.get('pwd'), "secretkey123").toString(CryptoJS.enc.Utf8):'',//解密 code: "", Yzm: '', newPassword:'', repPassword:'' }, isActive: false, count: 0, timer: null, isC: false, cLogin:false,//切换登录和注册 checked:false,//是否记住密码 forGet:false,//是否点击忘记密码 wangji:false,//忘记密码密码 isDownPC:false,//是不是下载pc软件 borHeight:1000,//浏览器可视区域的高度 } }, async created() { // 如果是点击注册,那么页面显示注册 if(this.$route.query.zc == 1){ this.cLogin = true } // 这里测试同一ip超过5次哪个 ,有数据就会显示图形验证码 // const data1 = await this.$api.UserApi.GetImageVeriFicationCode(); // this.imgUrl = data1.ImgSrc // this.getIMGCode() //这个是获取图形验证码、ip限制的 }, mounted () { if(localStorage.getItem("rememberPsw") == 'true'){ this.checked = true }else if(localStorage.getItem("rememberPsw") == 'false'){ this.checked = false } }, // 登录按钮的文字显示 computed: { buttonText() { if(this.forGet === true){ return "确定" }else{ if(this.cLogin){ return "快速注册领取3天VIP" }else if(!this.cLogin){ return "立即登录" } } } } } </script> <style lang="scss" scoped> .span { position: absolute; top: 16.2%; right: 8%; color: #c90000; font-family: PingFang SC; font-size: rem(18); } // 修改提交按钮的样式 .el-button{ outline: 0 none; border: 0 none; width: rem(600); height: rem(60); background: #C90000; margin-top: rem(50); font-size: rem(30); font-family: PingFang SC; font-weight: 600; color: #FFFFFF; } // 修改输入框的颜色 ::v-deep .el-input__inner{ background-color: #f6f6f6; } // 修改输入框的边框样式 ::v-deep .el-input__inner{ border: 1px solid rgba(255,255,255,0); border-bottom: 2px solid #f6f6f6; font-size: rem(20); color: #1E1E1E; margin: rem(20) 0; border-radius: 0; } // 修改校验错误时的样式 ::v-deep .el-form-item.is-error .el-input__inner, .el-form-item.is-error .el-input__inner:focus, .el-form-item.is-error .el-textarea__inner, .el-form-item.is-error .el-textarea__inner:focus, .el-message-box__input input.invalid, .el-message-box__input input.invalid:focus { border-color: #f6f6f6; border-bottom: 2px solid #C90000; } // 修改校验错误时弹出字体 ::v-deep .el-form-item__error { color: #C90000; font-size: rem(16); } // 修改选中记住密码样式 ::v-deep .el-checkbox__input.is-checked .el-checkbox__inner, .el-checkbox__input.is-indeterminate .el-checkbox__inner { background-color: #C90000; border-color: #C90000; } ::v-deep .el-checkbox__input.is-checked+.el-checkbox__label { width: rem(94); height: rem(23); font-size: rem(16); font-family: PingFang SC; font-weight: 400; color: #1E1E1E; } ::v-deep .el-checkbox{ width: rem(94); height: rem(23); font-size: rem(16); font-family: PingFang SC; font-weight: 400; color: #1E1E1E; } ::v-deep .el-checkbox__label { font-size: rem(16); } .container1 { height:100%; width:100%; display: flex; box-sizing: border-box; .left { flex: 3; overflow: hidden; width: 70%; background-color: #f6f6f6; .logo { width: rem(296); height: rem(70); margin: rem(90) rem(422) rem(60); } .formTable{ width: rem(600); height: 100%; margin: 0 rem(270); .middle{ display: flex; justify-content: space-between; .right{ text-align: right; width: 93px; height: rem(23); font-size: rem(16); font-family: PingFang SC; font-weight: 400; color: #C90000; } .left{ width: rem(94); height: rem(23); font-size: rem(16); font-family: PingFang SC; font-weight: 400; color: #1E1E1E; } } .bottom{ width: rem(238); height: rem(23); margin:rem(30) auto; line-height: rem(23); text-align: center; .left{ color: #1F1F1F; font-size: rem(14); } .right:hover{ cursor: pointer; } .right{ color: #C90000; font-size: rem(16); font-family: PingFang SC; font-weight: 700; } } } } .right { position: relative; width: 100%; flex: 2; overflow: hidden; height: 100%; .textIMG{ position: absolute; top: 20%; left: 0; h3{ margin-left: rem(30); font-size: rem(26); font-family: PingFang SC; font-weight: 600; color: #C90000; } p{ margin-left: rem(30); margin-top: rem(10); font-size: rem(20); font-family: D-DIN; font-weight: bold; color: #3B3B3B; } } .img{ position: absolute; top: rem(200); left: rem(60); width: rem(448); height: rem(138); z-index: 2; img{ width: 100%; height: 100%; } } .zhezhao{ position: absolute; height: 100%; top: 0; left: 0; width: rem(140); background-color: #f2f2f2; opacity: 0.7; z-index: 1; } img { width: 100%; height: 100%; } } .img { overflow: hidden; width: rem(200); height: rem(50); position: absolute; bottom: 23%; right: 0%; } } </style>