官网项目完成总结
Posted 接着奏乐接着舞。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了官网项目完成总结相关的知识,希望对你有一定的参考价值。
目录
2.1 在static目录下新建js文件 page:flexble
5.nuxt 使用VueAwesomeSwiper插件,展示轮播及缩略图
6.IntersectionObserver API 使用教程
前言:
开发完成的这个项目是基于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>
4.seo优化
需求:
实现:
其实就是设置nuxt.config.js里面的config中设置全局head对象,也可在每个页面的head中设置
在页面里面:
<template>
<h1> title </h1>
</template>
<script>
export default
data()
return
title: 'Home page'
,
head()
return
title: this.title,
meta: [
hid: 'description',
name: 'description',
content: 'Home page description'
]
</script>
nuxt官网地址:
Nuxt - Meta Tags and SEOhttps://nuxtjs.org/docs/features/meta-tags-seo
5.nuxt 使用VueAwesomeSwiper插件,展示轮播及缩略图
nuxt 使用VueAwesomeSwiper插件,展示轮播及缩略图 - 小白&小菜 - 博客园参考地址:https://github.com/surmon-china/vue-awesome-swiper vue-cli 脚手架使用swiper的步骤:参考地址:https://www.cnblhttps://www.cnblogs.com/duanzhenzhen/p/12362129.html
6.IntersectionObserver API 使用教程
7.IE11的CSS兼容性问题
8.pc端网页屏幕自适应适配方案(rem)
9.后台返回图形验证码 前端如何处理
10.nuxt中文官网
11.好用的日期处理插件day.js
下载安装全局安装后
在页面中就可随意使用
$dayjs(item.StartTime).format("YYYY/MM/DD")
以上是关于官网项目完成总结的主要内容,如果未能解决你的问题,请参考以下文章
Vue3官网-高级指南(十七)响应式计算`computed`和侦听`watchEffect`(onTrackonTriggeronInvalidate副作用的刷新时机`watch` pre)(代码片段