Vue3+Nuxt3 从0到1搭建官网项目,SEO搜索、中英文切换、图片懒加载

Vue2+Nuxt2 从 0 到1 搭建官网~

想开发一个官网,并且支持SEO搜索,当然离不开我们的 NuxtNuxt2 我们刚刚可以熟练运用,现在有出现了Nuxt3,那通过本篇文章让我们一起了解一下。

安装 Nuxt3,创建项目

安装nuxt3, 需要node v18.10.0以上,大家记得查看自己的node版本。

升级node,可以参考使用nvm 切换不同node版本~

// node  v18.10.0
// npx nuxi@latest init <project-name>

npx nuxi@latest init nuxt3-demo

cd nuxt3-demo

初始化的 package.json

这是项目刚创建后的package.json文件

{
  "name": "nuxt3-demo",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "3.8.2",
    "vue": "3.3.10",
    "vue-router": "4.2.5"
  }
}


项目结构

├── app.vue // 主文件
├── assets // 静态资源
├── components  // 公共vue组件
├── composables // 将你的Vue组合式函数自动导入到你的应用程序中
├── error.vue  // 路由匹配不到时
├── i18n.config.ts  // 语言切换配置文件
├── lang  // 语言JSON
├── nuxt.config.ts  // nuxt 配置文件
├── package.json  
├── pages   //  pages文件夹下面的页面名,默认为 路由地址
├── plugins  // 公共插件
├── public   // 提供网站的静态资源
├── server  
├── tsconfig.json 
└── yarn.lock // 包含了应用程序的所有依赖项和脚本

初始化项目

我们首先创建一个首页,将项目运行起来,这样一会儿讲到SEO、语音切换时,方便查看效果

pages 文件下创建index.vue

<template>
  <div class="home-wrap">
    Nuxt3--这是我们的首页
  </div>
</template>

<script>
export default {

}
</script>

<style lang="scss" scoped>
.home-wrap{
  height: 500px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
}
</style>

我们发现页面出现如下错误,提示我们需要引入 sass
在这里插入图片描述
在这里插入图片描述)

引入sass

// 如果下载失败。记得比对package.json 中依赖的版本号
// node_modules 和 yarn.lock 记得也删除一下
yarn add string-width@7.1.0 sass@1.69.5 sass-loader@13.3.2  --save

修改 app.vue 文件

<template>
  <div>
    <NuxtPage />
  </div>
</template>

查看效果

在这里插入图片描述

配置公共的css、meta

在我们的项目中,UI风格肯定是有规范(统一)的,因此我们可以将 css 重置文件公共的css文件,以及Meta提前引入

assets下的css

在assets 文件夹下,我们可以创建css(样式)、img(图片)、fonts(字体库)等文件夹

reset.scss 重置文件

因为UI主体区域为1200px,因此body 我们设置了最大宽度是1350px,这样主体区域两侧有一点占位空间,使内容不至于紧贴设备边界

/* CSS reset */
// /assets/css/reset.scss

html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
	margin:0;
	padding:0;
}

body{
	font-family:"思源黑体","Microsoft YaHei","微软雅黑",Arial,sans-serif;
  font-size: 14px;
  color: #333;
  min-width: 1350px;
}
*{
  -webkit-text-size-adjust: none;
}
*,*:after,*:before{
	box-sizing:border-box;
	margin: 0;
	padding:0;
}
table {
	border-collapse:separate;
	border-spacing:0;
}
fieldset,img {
	border:0;
}
img{
	display: block;
  -webkit-user-drag: none;
}
img[src=""],img:not([src]){
	opacity: 0;
	border:none;
	visibility: hidden;
	max-width: none;
}
address,caption,cite,code,dfn,th,var {
	font-style:normal;
	font-weight:normal;
}
ol,ul ,li{
	list-style:none;
}
caption,th {
	text-align:left;
}
h1,h2,h3,h4,h5,h6 {
	font-size:100%;
	font-weight:normal;
}
abbr,acronym { border:0;
}
a{
	text-decoration:none;
}

/* 解决兼容而加的样式 */
a, img {
    -webkit-touch-callout: none; /*禁止长按链接与图片弹出菜单*/}
a,button,input{-webkit-tap-highlight-color:rgba(255,0,0,0);}
img{
  display: block;
}
button,input,optgroup,select,textarea {
	outline:none;
    /*-webkit-appearance:none; /*去掉webkit默认的表单样式*/}

a,button,input,optgroup,select,textarea {
	-webkit-tap-highlight-color:rgba(0,0,0,0); /*去掉a、input和button点击时的蓝色外边框和灰色半透明背景*/
}

input:-ms-input-placeholder{color:#b3b7c0;}
input::-webkit-input-placeholder{color:#b3b7c0;}
input:-moz-placeholder{color:#b3b7c0;}
input::-moz-placeholder{color:#b3b7c0;}

common.scss

/* CSS common */
// 这里大家可以写一些公共的css样式,我这里主要是举例  
// /assets/css/common.scss

.g-border{position: relative;}
.g-border1{position: relative;}
.g-border:after{content:'';position: absolute;bottom:0;width:100%;height:1px;background:#e8e8e8;overflow: hidden;left:0;transform:translate(0%,0) scale(1,0.5);}
.g-border1::before{content:'';position: absolute;top:0;width:100%;height:1px;background:#e8e8e8;overflow: hidden;left:0;transform:translate(0%,0) scale(1,0.5);}

.g-border-on::before,.g-border-on:after{
  background: #dcdcdc!important;
}

.g-text-ove2{display: -webkit-box;-webkit-box-orient: vertical; -webkit-line-clamp: 2;overflow: hidden;}
.g-text-ove1{overflow: hidden;text-overflow:ellipsis;white-space: nowrap;}


配置nuxt.config.ts

// 熟悉我的小伙伴可能注意到,我非常喜欢在项目中使用 address,哈哈哈
yarn add address@2.0.1 --save
// https://nuxt.com/docs/api/configuration/nuxt-config
const address = require('address')
const localhost = address.ip() || 'localhost'

export default defineNuxtConfig({
  ssr:true,
  app:{
    head: {
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'viewport', name: 'viewport', content:"width=1350,  user-scalable=no,viewport-fit=cover"},
        { hid: 'description', name: 'description', content: 'CSDN 作者:拿回忆下酒,介绍Vue3+Nuxt3 从0到1搭建官网项目(SEO搜索、中英文切换、图片懒加载)的dome' },
        { hid: 'keywords', name: 'keywords', content: 'Vue3,Nuxt3,CSDN 拿回忆下酒' },
        { name: 'format-detection', content: 'telephone=no' }
      ],
      link:[
       {
        rel:'icon',
        type:'image/x-icon',
        href:'/favicon.ico'
       }
      ]
    },
  },
  css: [
    '@/assets/css/reset.scss',
    // 公共class
    '@/assets/css/common.scss'
  ],
  devtools: { 
    enabled: true,
    ssr:false
  },
  devServer:{
    host: localhost,
    port:8303
  }
})

现在的package.json

防止小伙伴下错版本号,咱们确定一下 现在的依赖包的版本号

大家也可以复制一下内容,将node_modulesyarn.lock 删除,重新执行 yarn install

{
  "name": "nuxt3-demo",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "nuxt dev --open",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "@nuxt/devtools": "latest",
    "@nuxtjs/i18n": "8.0.0",
    "address": "2.0.1",
    "nuxt": "3.8.2",
    "sass": "1.69.5",
    "sass-loader": "13.3.2",
    "string-width": "7.1.0",
    "vue": "3.3.10",
    "vue-router": "4.2.5"
  }
}

查看效果

我们可以看到 IP、端口、meta、css 已经都改变了
在这里插入图片描述

创建新页面

创建新页面,演示目录结构路由跳转多页面TDK配置等等

pages目录结构

我们在pages文件夹下,新建 首页(index)、关于我们(about)、订单(product/order)、管家(product/steward)等页面,用来演示 目录结构路由对应关系

// pages 目录结构

├── nuxt.config.ts
├── package.json
├── pages
│   ├── abouut.vue
│   ├── index.vue
│   └── product
│       ├── order.vue
│       └── steward.vue

components 创建组件

components/ 目录是你放置所有 Vue 组件的地方。我们可以把公共组件放到这里

Header 组件

<template>
  <section class="c-head-wrap" >
    <div class="content-box">
      <div  class="head-box">
        <section class="main g-width-box g-cen-y">
          <div class="logo-box" >
            演示logo
            <!-- <img class="img" src="@/assets/img/logo-top.png" /> -->
          </div>
          <div 
            class="nav-btn-box "
          >
            <div class="g-dis">
              <div
                v-for="(m, i) in navArr"
                :key="i"
                class="btn"
                ref="btnId"
                @click="routerFn(m)"
                :class="[isPageFn(m,i)]"
              >
                <h6>{{ m.name }}</h6>

                <div v-show="m.children" class="children-box">
                  <div 
                    class="item" 
                    v-for="(n,ind) in m.children" 
                    :key="i+'-'+ind" 
                    @click.stop="routerFn(n)"
                  >
                  {{ n.name }}
                  </div>
                </div>
              </div>
            </div>
            <div class="border" :style="isStyle"></div>
          </div>
         
        </section>
      </div>
    </div>
  </section>
</template>
<script setup>
import { ref } from "vue";
const router = useRouter()
const route = useRoute()

const navArr = ref([
  { path: "/",name:'首页' },
  { 
    path: "/product", 
    name:'产品业务',
    children:[
      { path: "/product/order", name:'订单'},
      { path: "/product/steward", name:'管家'}
    ]
  },
  { path: "/about", name:'关于我们' }
]);


let btnId = ref(null)
let isStyle = ref('')

const routerFn = (e) => {
  if(e.children) return 

  router.push(e.path)
};

const isPageFn =(e,ind)=>{
  let async = false
  if(route.path == e.path){
    async = true
  } else if(e.children){
    async = !!e.children.filter(m => m.path == route.path).length
  }

  return async ? 'on':''
}

</script>
<style lang="scss" scoped>
.c-head-wrap{
  min-width: 1350px;
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 22;
}



.content-box{
  width: 100%;
  height: 70px;
  background: transparent;
  border-bottom: 1px solid #E0E4E8;
}
.main{
  height: 70px;
}
.logo-box{
  height: 100%;
  display: flex;
  align-items: center;
  margin-right: 102px;
  font-size: 20px;
  font-weight: 500;
}
.nav-btn-box{
  flex: 1;
  position: relative;
  .border{
    width: 72px;
    height: 2px;
    background: #0044FF;
    position: absolute;
    bottom: 0;
    transition: left 0.28s;
    opacity: 0;
  }
  
  .btn{
    font-weight: 500;
    font-size: 18px;
    color: #4F587F;
    line-height: 70px;
    padding: 0 18px;
    cursor: pointer;
    position: relative;
    font-weight: 400;
    z-index: 2;
    &.on{
      color: #0044FF;
      position: relative;
    }

    &:hover{
      .children-box{
        display: block;
      }
    }
    
  }
}


.children-box{
  display: none;
  position: absolute;
  background-image: linear-gradient(197deg, rgba(246,246,246,1) , rgba(246,246,246,.65));
  backdrop-filter: blur(5px);
  box-shadow: 0 20px 40px 0 rgba(74,91,130,.16);
  border: 2px solid #fff;
  
  min-width: 100%;
  left: 50%;
  top: 70px;
  line-height: 50px;
  text-align: center;
  white-space: nowrap;
  transform: translate(-50%,0);
  font-size: 16px;
  font-weight: 400;
  color: #4F587F;
  z-index: 2;
 
  .item{
    border-bottom: 1px solid #E0E4E8;
    padding: 0 14px;

    &:last-child{
      border-bottom: 0;
    }
    &.on,&:hover{
      color: #0044FF;
    }
  }
}

</style>

Footer 组件

<template>
  <div class="footer-box" >
     <div class="box g-width-box">
      <div class="left">
        <div class="icon">演示logo</div>
        <div class="phone">咨询电话:+86-136xxxx8899</div>
        <div class="btn-box g-cen-y">
          <span>版权 © 演示xxxx科技有限公司</span>
          <i></i>
          <a href="https://beian.miit.gov.cn/" target="_blank" class="btn">ICP备2010xxxx88号</a>
          <i></i>
          <a href="https://beian.mps.gov.cn/#/query/webSearch?code=1010xxxxx3434" target="_blank" class="btn sprite-btn">
            <img class="sprite" src="assets/img/sprite.png"/>
            <span>京公网安备1010xxxxx3434号</span>
          </a>
        </div>
      </div>
      <div class="right">
      
      </div>
     </div>
    </div>
</template>

<style lang="scss" scoped>
.footer-box{
  height: 200px;
  background: #242933;
  
  .box{
    
    display: flex;
    justify-content:space-between;
    padding-top: 40px;
    .left{
      font-size: 14px;
      
      .icon{
        height: 32px;
        display: block;
        margin-bottom: 40px;
        color:#fff;
        font-size: 30px;
      }
      .phone{
        line-height: 22px;
        color: #FFFFFF;
      }
      .btn-box{
        line-height: 24px;
        color:#878FB4 ;
        &>i{
          height: 14px;
          width: 2px;
          background: #878FB4 ;
          margin: 0 10px;
        }
        .btn{
          color:#878FB4 ;
          font-size: 14px;
          cursor: pointer;
          &:hover{
            color: #fff;
          }
        }
        .sprite-btn{
          display: flex;
          align-items: center;
        }
        .sprite{
          width: 16px;
          height: 17px;
          margin-right: 5px;
        }
      }
    }
    .right{
      display: flex;
      flex-direction:column; 
      align-items: center;
      
      .qrcode1{
        width: 100px;
        height: 100px;

      }
      .txt{
        line-height: 32px;
        font-size: 12rpx;
        color: #E9EDFF;
        text-align: center;
      }
    }
  }
}
</style>

引入组件

官网一般header(头部)、footer(底部)都是复用的,因此我们在app.vue文件中引用它们

// app.vue
<template>
  <div>
    <Header />
    <NuxtPage />
    <Footer />
  </div>
</template>

组件效果

在这里插入图片描述

多语音系统

现在好多公司的官网都是多语言,因此我们一起了解一下 i18n

引入i18n

yarn add @nuxtjs/i18n@8.0.0 --save

新建lang文件夹

这是一个自创建的文件夹,用来存放 语言切换 需要用到的数据,我们来了解一下它的目录结构

├── nuxt.config.ts
├── i18n.config.ts
├── lang
│   ├── cn
│   │   ├── about.js
│   │   ├── home.js
│   │   ├── index.js // 入口文件
│   │   ├── order.js
│   │   └── steward.js
│   └── en
│       ├── about.js
│       ├── home.js
│       ├── index.js  // 入口文件
│       ├── order.js
│       └── steward.js

入口文件(index.js )

index.js 的代码是一致的,主要作用是 将同一种语言下的文件合并到一起, 我以其中一个为例

// 获取同一目录下 的 js 文件
const files = import.meta.globEager('./*.js');

const modules = Object.keys(files).reduce((prev, cur) => { 
   // 获取js 文件的 文件名 并转  大写 (这里很重要!!!!)
   // 这里  大写的文件名,是日后 dom 中日后替换的关键字
  let key = cur.split('/')[1].split('.')[0].toUpperCase()
  // 将内容合并在一起,为了方便理解,下面有合并后的JSON图
  return {...prev,...{[key]:files[cur]?.default}}
}, {});



export default {...modules};

中英js文件 对比

每个js文件,JSON结构都是一致的,我们以home、about 为例对比一下

// cn/home.js
export default {
    'b1': {
        'title': '我们的首页',
        'txt': '这是一段尝试,json结构可以任意定义,只要保持统一规范即可'
    }
}

// en/home.js
export default {
    'b1': {
        'title': 'Our homepage',
        'txt': 'China\'s leading fintech platform for automobiles empowering supply chain ecosystem partners. '
    }
}


***********************
// cn/about.js
export default {
    'b1': {
        'title': '关于我们'
    }
}

// en/about.js
export default {
    'b1': {
        'title': 'About Us'
    }
}


合并后的JSON图

在这里插入图片描述

i18n.config.ts

import en from './lang/en/index.js'
import cn from './lang/cn/index.js'

export default defineI18nConfig(() => ({
    legacy: false,  // 是否兼容之前
    fallbackLocale: 'cn',  // 区配不到的语言就用en
    messages: {
        en,
        cn
    }
}))

nuxt.config.ts

export default defineNuxtConfig({
	...

  modules:[
    '@nuxtjs/i18n'
  ],
  i18n: {
    strategy: 'prefix_and_default', // 添加路由前缀的方式
    locales: ["en", "cn"], //配置语种
    defaultLocale: 'cn', // 默认语种
    vueI18n: './i18n.config.ts', // 通过vueI18n配置
  },




...

})

修改index.vue 文件

我们以index.vue 文件为例,主要看 $t(HOME.b1.title) 部分,HOME 便是当时 lang中的文件名

<template>
  <div class="home-wrap">
    Nuxt3--{{ $t(`HOME.b1.title`) }}
  </div>
</template>

<script setup>
export default {

}
</script>

<style lang="scss" scoped>
.home-wrap{
  min-height: calc(100vh - 200px);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
}
</style>

查看效果

中英文切换,主要是通过不同的路由进行区分的,大家注意看浏览器URL地址

中文效果

在这里插入图片描述

英文效果

在这里插入图片描述

动态切换语言

我们虽然实现了语言替换,但是官网中并没有实现语言动态切换的功能,因此我们需要进一步完善,首先我们需要引入pinia

yarn add @pinia/nuxt@0.5.1 pinia@2.0.23 --save

nuxt.config.ts

// 添加  @pinia/nuxt

export default defineNuxtConfig({
	...

  modules:[
    '@pinia/nuxt', 
    '@nuxtjs/i18n'
  ]

...

})

head文件

lang 文件夹下增加 head.js 文件,用来替换 头部的语言

// lang/cn/head.js
export default {
    'home':'首页',
    'product':'产品业务',
    'order':'订单',
    'steward':'管家',
    'aboutUs':'关于我们'
}

// lang/en/head.js
export default {
    'home':'Home',
    'product':'Product',
    'order':'Order',
    'steward':'Steward',
    'aboutUs':'About Us'
}

composables

使用composables/目录将你的Vue组合式函数自动导入到你的应用程序中。


├── components
├── composables
│   ├── global.ts
│   └── store.ts

store.ts

export const useStateStore = defineStore('nuxtStore', () => {
  const locale = ref('')
  let localeName = ref('')

  const localeArr = ref([
    {id:'cn',name:'中文'},
    {id:'en',name:'English'}
  ])
 
  const setState = (name: any) => {

    let arr = localeArr.value.filter(m=>m.id ==name)
    let obj = localeArr.value[0]

    if(arr.length>0){
      obj = arr[0]
    }

    locale.value = obj.id
    localeName.value = obj.name
    
  }
 

 
 
  return {
    locale,
    localeArr,
    localeName,
    setState
  }
})

global.ts

export default function () {
  const routerFn = (fullPath: any) => {
    const route = useRoute()
    const localeRoute = useLocaleRoute()
    const store = useStateStore()
    // 你的公共逻辑
    let arr  = ['cn','en']

    let a = arr.filter((m) => fullPath.includes(m))

    if(a.length){
      let paths = fullPath.split(a[0])
      fullPath = '/'+store.locale +paths[1]
    } else {
      fullPath = '/'+store.locale +fullPath
    }
  
     // 用于把当前页面生成对应的语言前缀的路由,例如:/zh/,/zh/about
     const routePath = localeRoute({ path:fullPath, query: { ...route.query } })
  
    if (routePath) {
      return navigateTo(fullPath)  // 路由跳转
    }
  };
 
  return {
    routerFn
  };
}

header.vue

我们需要重新改造一下header文件,右上角增加了语言切换按钮,整个导航栏 增加了滚动超出页面后悬浮,按钮切换高亮动画效果等

<template>
  <section class="c-head-wrap" :class="[{'on':scrollY >=70},store.isIPhone?'m':'pc']">
    <div class="content-box">
      <div class="back-box"></div>
      <div  class="head-box">
        <section class="main g-width-box g-cen-y">
          <div class="logo-box" >
            <img class="img" src="@/assets/img/logo-top.png" />
          </div>
          <div 
            class="nav-btn-box "
          >
            <div class="g-dis">
              <div
                v-for="(m, i) in navArr"
                :key="i"
                class="btn"
                ref="btnId"
                @click="routerFn(m)"
                :class="[isPageFn(m,i),{'hover':isChildren == m.path}]"
              >
                <h6>{{ $t(`HEAD.${m.name}`) }}</h6>
                <div v-show="m.children && async" class="children-box">
                  <div 
                    class="item" 
                    v-for="(n,ind) in m.children" 
                    :key="i+'-'+ind" 
                    :class="[isPath == n.path && 'on']"
                    @click.stop="routerFn(n)"
                  >
                  {{ $t(`HEAD.${n.name}`) }}
                  </div>
                </div>
              </div>
            </div>
            <div class="border" :style="isStyle"></div>
          </div>
          <div 
            class="locale-box g-cen-y" 
            @click="localeFn"
            :class="[{'hover':isChildren == 'locale'}]"
          >
            <i class="icon1"></i>
            <span>{{ store.localeName}}</span>
            <i class="icon2"></i>

            <div  class="children-box" v-show="async ">
                <div 
                  class="item" 
                  v-for="n in store.localeArr" 
                  :key="n.id" 
                  :class="[store.locale == n.id && 'on']"
                  @click.stop="change(n.id)"
                >
                  {{ n.name }}
                </div>
              </div>
          </div>
        </section>
      </div>
    </div>
  </section>
</template>
<script setup>
import { ref } from "vue";
import  useGlobal from '@/composables/global.ts'
const route = useRoute()
const global = useGlobal()
const store = useStateStore()


defineProps({
  scrollY: {
    type: [String,Number],
    default: 0
  }
});


let async =ref(true)

const navArr = ref([
  { path: "/",name:'home' },
  { 
    path: "/product", 
    name:'product',
    children:[
      { path: "/product/order", name:'order'},
      { path: "/product/steward", name:'steward'}
    ]
  },
  { path: "/about", name:'aboutUs' }
]);


let btnId = ref(null)
let isStyle = ref('')

let isChildren =  ref('')
let isPath = computed(() => {
  let name = 'cn'
  if(route.path.split('/')[1]){
    name = route.path.split('/')[1]
  }

  if(name==404){
    name = 'cn'
  }

  store.setState(name)

  return route.path.split(store.locale)[1] || '/'
})

const routerFn = (e) => {
  if(e.children) {
    if(store.isIPhone){
      isChildren.value = e.path
    }
    return
  }
  let fullPath =  route.path.split( store.locale )[1]
  // 去重  路径无变化,不需要跳转
  if(fullPath === e.path)return

  global.routerFn(e.path)
  asyncFn()

};

const localeFn =()=>{
  if(store.isIPhone){
    isChildren.value = 'locale'
  }
}

const isPageFn =(e,ind)=>{
  let async = false
  if(isPath.value == e.path){
    async = true
  } else if(e.children){
    async = !!e.children.filter(m => m.path == isPath.value).length
  }

  let arr = btnId.value
  if(async && arr){
    let w = 0;
    for(let i=0;i<ind;i++){
      w += arr[i].offsetWidth
    }

    isStyle.value = `left:${w}px;opacity:1;width:${arr[ind].offsetWidth}px`
  }

  return async ? 'on':''
}

// 切换中英文
const change = (e)=>{
  store.setState(e)

  let fullPath = route.fullPath
  global.routerFn(fullPath)
  asyncFn()
}


const asyncFn = ()=>{
  async.value = false;
  isChildren.value = ''

  setTimeout(()=>{
    async.value = true;
  },100)
}
</script>
<style lang="scss" scoped>
.c-head-wrap{
  min-width: 1350px;
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 22;
  &.on{
    height: 0;
    animation: boxShow .5s forwards ;
    position: sticky;
    .content-box{
      box-shadow: 0 20px 40px 0 rgba(74,91,130,.16);
      position: relative;
    }
    .head-box{
      position: relative;
      z-index: 2;
    }
    
    .back-box{
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 70px;
      display: block;
      background: rgba(255,255,255,.9);
      backdrop-filter: blur(3px);
    }
  }

  &.pc{
    .nav-btn-box .btn:hover,.locale-box:hover{
      .children-box{
        display: block;
      }
    }
  }

  &.m{
    .nav-btn-box .btn.hover,.locale-box.hover{
      .children-box{
        display: block;
      }
    }
  }

}



.content-box{
  width: 100%;
  height: 70px;
  background: transparent;
  border-bottom: 1px solid #E0E4E8;
  .back-box{
    display: none;
  }
}
.main{
  height: 70px;
}
.logo-box{
  height: 100%;
  display: flex;
  align-items: center;
  margin-right: 102px;
  .img{
    width: 300px;
    height: 32px;
    object-fit: cover;
  }
}
.nav-btn-box{
  flex: 1;
  position: relative;
  .border{
    width: 72px;
    height: 2px;
    background: #0044FF;
    position: absolute;
    bottom: 0;
    transition: left 0.28s;
    opacity: 0;
  }
  
  .btn{
    font-weight: 500;
    font-size: 18px;
    color: #4F587F;
    line-height: 70px;
    padding: 0 18px;
    cursor: pointer;
    position: relative;
    font-weight: 400;
    z-index: 2;
    &.on{
      color: #0044FF;
      position: relative;
    }
    
  }
}
.locale-box{
  font-weight: 500;
  font-size: 18px;
  color: #1C1E20;
  height: 100%;
  display: flex;
  align-items: center;
  cursor: pointer;
  position: relative;

  
  .icon1{
    width: 22px;
    height: 22px;
    margin-right: 9px;
    background: url('@/assets/img/icon1.png') no-repeat center;
    background-size: cover;
  }

  .icon2{
    width: 24px;
    height: 24px;
    background: url('@/assets/img/icon2.png') no-repeat center;
    background-size: cover;
  }
}

.children-box{
  display: none;
  position: absolute;
  background-image: linear-gradient(197deg, rgba(246,246,246,1) , rgba(246,246,246,.65));
  backdrop-filter: blur(5px);
  box-shadow: 0 20px 40px 0 rgba(74,91,130,.16);
  border: 2px solid #fff;
  
  min-width: 100%;
  left: 50%;
  top: 70px;
  line-height: 50px;
  text-align: center;
  white-space: nowrap;
  transform: translate(-50%,0);
  font-size: 16px;
  font-weight: 400;
  color: #4F587F;
  z-index: 2;
 
  .item{
    border-bottom: 1px solid #E0E4E8;
    padding: 0 14px;

    &:last-child{
      border-bottom: 0;
    }
    &.on,&:hover{
      color: #0044FF;
    }
  }
}

@keyframes boxShow {
  0% {
    top: -70px;
  }
  100% {
    top:0px
  }
}
</style>

error.vue

和 app.vue 同一级,增加error.vue 文件

<template>
  <div />
</template>

<script>
export default {
  created () {
    this.$router.replace('/404')
  }
}
</script>

<style>

</style>

404.vue

和index.vue同一级,增加404.vue 文件,文件内容可以自行编辑。 我这里直接重置到了首页

<template>
  <div>
    
  </div>
</template>
<script setup>
import { onMounted } from "vue";
// const route = useRoute()


onMounted(()=>{
  navigateTo('/')
})

</script>

查看效果

内容有限,我们以首页为例,对比一下 中英文切换效果

中文效果

在这里插入图片描述

英文效果

在这里插入图片描述

配置每个页面的TDK

TDK指的是搜索引擎优化(SEO)中的页面Title、Description、Keywords

  • Title:
  1. 页面的主要内容,对搜索引擎而言至关重要。
  2. 简洁、准确地描述页面内容。
  3. 不超过70个汉字或160个英文字符。
  • Description:
  1. 简要描述页面内容,提供页面主要信息。

  2. 不超过200个汉字或~300个英文字符。

  • Keywords:
  1. 关键词应该是与页面内容相关的,用于提升搜索结果的相关性。

  2. 不超过1000个汉字或~1600个英文字符。

  3. 使用对搜索引擎友好的逗号、分号或空格分隔关键词。

plugins 文件夹

Nuxt拥有一个插件系统,可以在创建Vue应用程序时使用Vue插件和其他功能。

在plugins 文件夹下,增加global.ts

global.ts

//global.ts

export default defineNuxtPlugin((nuxtApp) => {
  return {
    provide: {
      setHead: (page: String) => {
          const store = useStateStore()

         let {title,keywords,description} = store.headJson[store.locale][page]
          return {
            title,
            meta:[
              {hid: 'keywords', name: 'keywords', content: keywords},
              {hid: 'description', name: 'description', content: description}
            ]
          }
        }
      }
    };
})



index.vue

在首页中引用 global.ts 中 创建的 $setHead 方法

<template>
  <div class="home-wrap">
    <div >
      Nuxt3--{{ $t(`HOME.b1.title`) }}
    </div>
    <div>
      {{ $t(`HOME.b1.txt`) }}
    </div>
  </div>
</template>

<script setup>
const nuxtApp = useNuxtApp();
useHead({...nuxtApp.$setHead('home')})

</script>

<style lang="scss" scoped>
...

...
</style>

演示效果

中文效果

在这里插入图片描述

英文效果

在这里插入图片描述

图片懒加载

官网项目一直有一个弊端,就是项目中 图片资源比较多,导致打开速度慢。因此我们需要对图片资源做一下优化。

常规的优化一般针对图片压缩、图片格式、图片标签属性 和 图片懒加载

我们这里主要讨论一下 ,nuxt项目中如何进行图片懒加载 处理

这里首先声明一下,市面上其实有很多懒加载插件,但是不知道是不是因为nuxt3 和 lazy相关插件版本有兼容的问题,一致配置的不太好使

因此这个懒加载是我自己写的一个 自定义指令(本来也不复杂,所以干脆自己写了)

懒加载指令

在 plugins文件夹下,增加index.ts 文件,申明 v-Mlazy 指令

// plugins/index.js
// 该指令 arg  用来 区分  img src  还是  标签  background 
import {useIntersectionObserver} from "@vueuse/core";

export default defineNuxtPlugin((nuxtApp) => {
  // 不再是 import Vue from 'vue'的写法了
  nuxtApp.vueApp.directive('Mlazy', {
    // 不用mounted,在mounted时用户权限集还是空的
    mounted(el,{
      arg, 
      value
    }) {


      if (typeof value != 'string') {
        return
      } else if(arg == 'show') {
        el.style.background = `url(${value}) no-repeat   center`
        el.style.backgroundSize = `cover`
        return
      } else if(arg == 'imgShow') {
        el.src = value
        return
      }

      

      const { stop } = useIntersectionObserver(
        el,
        ([{ isIntersecting }]) => {

          
          if (isIntersecting) {
            if(arg == 'img'){
              el.src = value
            } else if(!arg){
              
              el.style.background = `url(${value}) no-repeat   center`
              el.style.backgroundSize = `cover`
            }
            // 当组件卸载时,停止监听
            stop();
          }
        }
      )
    }
  })

})

页面中引用

nuxt3.0后 assets 里面的资源已经不允许动态拼接引入。

懒加载的地址,本地需要放到根目录下的 public文件下,网络图片不受限制

<template>
  <div class="home-wrap">
    <div >
      Nuxt3--{{ $t(`HOME.b1.title`) }}
    </div>
    <div >
      {{ $t(`HOME.b1.txt`) }}
    </div>

    <div>
      这张图是  img
      <img v-Mlazy:img="'/back.png'" />
    </div>
    <div v-Mlazy="'/back.png'">
      这张图是  背景图
    </div>
   
  </div>
</template>

<script setup>
const nuxtApp = useNuxtApp();
useHead({...nuxtApp.$setHead('home')})

</script>

<style lang="scss" scoped>
.home-wrap{
  min-height: calc(100vh - 200px);
  &>div{
    width: 1200px;
    margin: 0 auto;
    min-height: 200px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 50px;
    position: relative;
  }
  img{
    height: 200px;
    width: 100%;
    position: absolute;
    z-index: -1;
  }
}
</style>

演示效果

在这里插入图片描述

fonts 字体库

api请求

动画

文章正在努力完善中。。。。。

  • 10
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3和Nuxt中进行权限控制可以通过多种方式实现。一种常见的方法是使用路由守卫和中间件来控制页面的访问权限。 在Nuxt中,可以使用nuxt-auth模块来实现权限控制。该模块提供了身份验证、角色和权限管理等功能。您可以在Nuxt配置文件中进行相应的配置,指定需要进行权限控制的页面和相应的访问权限。 另一种方法是使用Vue的路由守卫。您可以在路由配置中定义全局的前置守卫和组件级别的守卫。在前置守卫中,您可以检查用户是否登录或具有相应的权限,并根据情况决定是否允许访问该页面。在组件级别的守卫中,您可以进一步细化对特定组件的权限控制。 例如,您可以在路由配置中定义一个全局的前置守卫,用于检查用户是否登录: ```javascript router.beforeEach((to, from, next) => { const isAuthenticated = // 检查用户是否登录 if (to.meta.requiresAuth && !isAuthenticated) { next('/login') // 如果需要登录但用户未登录,则重定向到登录页面 } else { next() // 允许访问该页面 } }) ``` 在需要进行权限控制的页面的路由配置中,您可以使用`meta`字段来指定该页面需要的权限: ```javascript { path: '/admin', component: AdminPage, meta: { requiresAuth: true, // 需要登录才能访问 requiresAdmin: true // 需要管理员权限才能访问 } } ``` 然后,您可以在组件内部的生命周期钩子函数中检查用户的权限,并根据情况控制页面的内容显示或重定向到其他页面。 总结起来,Vue3和Nuxt中的权限控制可以通过路由守卫和中间件来实现。您可以根据需要选择合适的方法,并在相应的地方进行配置和处理,以实现所需的权限控制功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Nuxt+Vue3+TS+Vite入门教程](https://blog.csdn.net/weixin_41535944/article/details/129794934)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值