vue3使用vite+setup+ts写一个初级前端项目

Vue3vite+setup+ts项目总结

项目说明
项目概述

本项目纯粹是为了学习和巩固刚学的vue3组合式api和语法糖,再加上网上流行vite+ts+vue,所以便有了这个历时四天的vue3前端项目。不同于别人写的vue后台管理系统项目,这个是纯前端,没有一丝后端。

在这个项目里,你可以了解vue3与vue2不同之处,也能对vue3的常用知识点进行学以致用。项目中遇到的问题和解决办法也归纳到了使用技巧中,另外也可以作为一个vue3初级项目进行二次开发。

本项目的实现一部分基于我的毕设作品,一部分则是搜集网上优秀创作者分享的知识。在这个资源共享的时代,我也希望自己能够将学到的东西系统的整理起来然后分享给大家,这也就是开源项目的意义所在,哪怕这个项目毫无商业价值。

关于vue后台管理系统,这里推荐一个vite-vue3-template是一个管理后台系统中前端解决方案,后续我会同步分享给大家

基于vite+vue3语法糖setup+ts 所以本项目代码全部基于steup语法糖写法 具体格式如下:

<script lang='ts' setup>
import { useRoute, useRouter } from "vue-router"
import { reactive, toRefs, ref, computed, watch, onMounted } from 'vue'
// 接收路由传递过来的参数
const route = useRoute()
const router = useRouter()
onMounted(() => {
  console.log(hid.value)
})
</script>
为什么使用setup语法糖

因为相比组合式api的写法更加的简洁,不需要return定义的属性、方法,也不需要再注册组件,直接引入即可

setup是Vue3.0后推出的语法糖,并且在Vue3.2版本进行了大更新,像写普通JS一样写vue组件,对于开发者更加友好了

按需引入computed、watch、directive等选项,一个业务逻辑可以集中编写在一起,让代码更加简洁便于浏览。

项目首页预览

在这里插入图片描述

创建项目
使用vite创建vue3项目

首先,使用vite创建vue3项目(推荐安装最新版)

Vite 需要 Node.js 版本 >= 12.0.0

npm init vite@latest vue项目名称 -- --template vue

图示:

在这里插入图片描述
在这里插入图片描述

切换到当前目录、安装依赖、运行

cd 项目名
npm install
npm run dev
使用vite的注意事项

注意:使用ts,所有的js文件都需要使用ts后缀

配置vite.config.ts

支持alias别名@

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
	resolve: {
    	alias: {
			'@': path.resolve(__dirname, 'src'),
		}
	},
	plugins: [vue()]
})

作用是@直接指向src,对于多级src自己目录可以直接替换…/,确保引入的文件路径不会因为文件本身位置的改变而受到影响

import Header from '../components/header.vue'
=>
import Header from '@/components/header.vue'

如果你的path出现了红色波浪线,则还需要安装 @types/node

npm install --save @types/node

引入vue-router路由的写法变化

路径配置 .vue 不可省略,但ts文件可以不要后缀,并且component推荐使用异步加载的写法

// 导入路由对象
import { createRouter,createWebHistory } from 'vue-router'
// 路径配置 .vue 不可省略
const routes = [
	{
		path: '/',
		name: 'index',
     	 //在vite中使用如下引入组件的写法
		component: () => import('@/components/index.vue') //.vue不可省略
	}
]
解决axios跨域请求

server与plugins同级

server: {
    proxy: {
      // 字符串简写写法
      // '/myApi': 'http://apis.juhe.cn/',
      // 选项写法
      "/myApi": {
        target: "http://apis.juhe.cn/",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/myApi/, ""),
      },
      // 引入多个api
      "/api": {
        target: "https://v2.alapi.cn",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },

解释一下哈:假设我想访问聚合数据的天气预报的api接口:

`http://apis.juhe.cn/simpleWeather/query?city=${city}&key=cffe158caf3fe63aa2959767a503bbfe`

那么这里的‘/myApi’代表全局 axios 默认值,即http://apis.juhe.cn/,我们处理跨域请求就是对接口的默认值进行允许跨域的配置,而不是针对整个接口地址

配置完成后,我们使用下面的方式就可以成功get到我们的数据了,针对不同的api我们指定对应的axios 默认值就可以啦

requestData() {
  axios.defaults.baseURL = '/myApi'
  //对输入的城市中文进行编码处理
  let city = encodeURI(this.city)
  let api = `/simpleWeather/query?city=${city}&key=cffe158caf3fe63aa2959767a503bbfe`
  this.axios.get(api).then((response) => {
    this.weatherData = response.data
    console.log(response.data)
  })
}

题外话:如果你想使用聚合数据的api接口,需要注册以及实名认证获取自己的key,上面的key就是我自己注册的

安装依赖
本项目中用到的依赖

package.json中"dependencies" {} 里面的内容

  "axios": "^0.27.2",
  "element-plus": "^2.2.17",
  "jsonwebtoken": "^8.5.1",
  "jwt-decode": "^3.1.2",
  "mockjs": "^1.1.0",
  "moment": "^2.29.4",
  "pinia": "^2.0.22",
  "pinia-plugin-persist": "^1.0.0",
  "swiper": "^8.4.2",
  "v-viewer": "^3.0.10",
  "vue": "^3.2.37",
  "vue-axios": "^3.4.1",
  "vue-easy-lightbox": "^1.8.2",
  "vue-router": "^4.1.5",
  "vuex": "^4.0.2"
axios vue-axios

拦截请求和响应 转换请求和响应数据 取消请求 自动转换JSON数据

npm install axios vue-axios --save
element-plus

vue3推荐的Ui组件库

npm install element-plus --save
jsonwebtoken jwt-decode

一个用于生成token,一个用于解析token

注意:本项目没有使用到,ts报错找不到jwt,应该是需要配置.d.ts文件

mockjs

模拟生成数据,常用于项目上线的数据测试,但本项目没用到过,不过保留了mock.js文件供大家参考

npm install  mockjs --save
moment

用户时间格式的处理转换,比如注册状态的时间需要转换成标准格式

npm install moment --save
pinia vuex pinia-plugin-persist

用于状态管理,vue3中我们更推荐使用pinia,但是本项目中使用vuex足够了,因为pinia的写法我还不是很熟悉

pinia-plugin-persist用于持久化存储,自行百度

npm install pinia pinia-plugin-persist --save
npm install vuex --save
swiper

在vue3中使用swiper轮播可以满足特定的轮播需求,尽管elementplus的轮播比swiper配置更简单,ui更好看,但功能性不如swiper。不过目前网上的资料并不多

npm install swiper@latest --save
v-viewer vue-easy-lightbox

图片预览插件 v-viewer适配vue3,而vue-photo-preview目前好像不适配vue3,因为我使用时会报错

npm install v-viewer@next --save
npm insall vue-easy-lightbox --save
vue-router

vue路由管理,其中的route和router的使用场景想必大家都you心得体会了

npm install vue-router@4
route常用于获取对应的name,path,params,query等,传参一个跳转的路由对象
router常用于路由跳转VueRouter的一个对象
//路由跳转
router.push({path:/path’})
router.replace({path:/path’})
//获取传过来的key
route.query.key
// 获取当前路由path
router.currentRoute.value.path

补充:params传参合query传参的区别

params参数在地址栏中不会显示,query会显示

网页刷新后params参数会不存在

全局注册

main.ts 部分全局注册不起作用,局部再次引入即可

import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
const app = createApp(App);
// 引入路由
import router from "./router/index";
// 导入vue-axios模块
import VueAxios from "vue-axios";
import axios from "axios";
// 全局跨域请求基本配置
// axios.defaults.baseURL = '/api';
//导入Element Plus
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
//导入所有图标并进行全局注册
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
// 导入Pinia状态管理器
// import store from './pinia/index'
// 引入vuex
import store from "./store";
// 图片预览器
import "viewerjs/dist/viewer.css";
import VueViewer from "v-viewer";
app.use(VueViewer, {
  defaultOptions: {},
});
// 时间处理
import moment from 'moment'
// 下面的vue3全局注册方法在ts中不起作用,需要进一步配置
app.config.globalProperties.$moment = moment
app.use(VueAxios, axios)
app.use(router);
app.use(ElementPlus);
app.use(store);
app.mount("#app");
//全局导入过滤器  vue3不支持filters过滤器了,下面方法也不起效果,直接用计算属性算了
//在ts中,目前axios全局注册后不起作用,局部重新引入即可
使用技巧
分离css和js

当vue文件中的css和js代码过多时,我们可以导入到外部css和ts文件中

//css的引入方式
@import "@/assets/css/index.css" 
//js的引入方式
import Header from "@/assets/js/header"
js和ts的区别

ts是js的超集,主要是定义类型

ts报错类型“string | null”的参数不能赋给类型“string”的参数。 不能将类型“null”分配给类型“string”
JSON.parse(localStorage.getItem(key)|| '0')
TypeScript + vue3.0设置全局对象或者属性,出现ts类型错误

以本项目vue3语法糖setup+ts为moment注册全局属性为例:app.config.globalProperties.$moment = moment,

在组件中使用$moment会有ts报错

const time = $moment().format('MMMM Do YYYY, h:mm:ss a')
//找不到名称“$moment”。你是否指的是“moment”?ts

添加下列代码可解决:

方案1:src/@types/moment.d.ts。 若没有types文件夹则可以创建

import moment from 'moment'
declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $moment: typeof moment
  }
}
import { getCurrentInstance } from 'vue'
const app = getCurrentInstance()
const time = app?.proxy?.$moment().format('MMMM Do YYYY, h:mm:ss a')

方案2:重新导入该依赖也可以,区别就是一个全局一个局部,在不注重ts的类型约束下,不使用ts也行

import moment from 'moment'
const time = moment().format("YYYY-MM-DD HH:mm:ss")
判断对象数组是否包含某个对象

有这么一个需求:创建一个loginData数组来模拟后台数据库中的用户登录账号和密码信息,用户信息为对象类型

const data = {
    name: name.value,
    password: password.value
  }
const loginData = [
  {},{},{}
  ...
]

当用户输入的用户信息与该数组里面的某个对象匹配时,则登录成功,否则提示登录失败

这意味着我们需要判断一个对象数组中是否包含某个对象,并且对象要全等

// 判断用户登录的数据是否与模拟数据库匹配
function findObj(arr: [], obj: {}) {
  for (let i = 0; i < arr.length; i++) {
    if (JSON.stringify(arr[i]) == JSON.stringify(obj)) {
      return i
    }
  }
  return -1
}
const flag = findObj(arr, data)
if(flag != -1) {
  console.log('匹配成功,登录成功')
} else {
  console.log('匹配失败,登录失败')
}
如何配置swiper

下面的链接或许可以帮到你,说说我的方法

访问https://swiperjs.com/demos#centered,然后按下图操作

在这里插入图片描述

css变量注入 vue3.2新增
<template>
  <span>Jerry</span>  
</template>
 
<script setup>
  import { reactive } from 'vue'
 
  const state = reactive({
    color: 'red'
  })
</script>
  
<style scoped>
  span {
    // 使用v-bind绑定state中的变量
    color: v-bind('state.color');
  }  
</style>

使用上面的方式可以更加方便的操控元素样式了,都不需要指定标签,但是使用了变量注入的代码不能分离到外部css,否则会获取不到的

在我的项目中,可以用来设置主题和header导航栏的样式

使用css变量注入实现header导航栏的样式切换

1.默认进入首页(path: ‘/index’)时header背景显示透明色transition,文字颜色显示白色white

在这里插入图片描述

2.鼠标移入时,背景显示白色,文字颜色显示#606266;鼠标移出还原

3.滚动条移动时,背景显示白色,文字颜色显示#606266

4.切换到非首页的路由path时,样式固定为背景显示白色,文字颜色显示#606266

<el-menu :default-active="activeIndex" class="el-menu-demo"
      :class="{'menu_fixed': activeIndex.search('index') == -1}" mode="horizontal" :ellipsis="false"
      @select="handleSelect" @mouseover="ShowInput($event)" @mouseleave="HideInput($event)">
      ...
</el-menu>
import { useRouter } from 'vue-router'
// 调用路由方法及注册
const router = useRouter()
// 获取当前路由path
const activeIndex = ref(router.currentRoute.value.path)
// 定义css变量
const state = reactive({
  color: 'white',
  bgColor: 'rgba(0, 0, 0, .3)'
})
// 鼠标移入 
function ShowInput(event: any) {
  state.color = '#606266'
  state.bgColor = 'white'
}
// 鼠标移出
function HideInput(event: any) {
  if (router.currentRoute.value.path.search('index') != -1) {
    state.color = 'white'
    state.bgColor = 'rgba(0, 0, 0, .3)'
  } else {
  }
}
/* 首页-header导航栏固定样式 */
.el-menu-demo {
  height: 70px;
  /*这个颜色是控制非el-menu元素内部的外部元素颜色 */
  color: white;
  transition: 0.6s;
  background: v-bind('state.bgColor');
}
/* 首页-固定文字样式 */
.el-menu-demo .el-menu-item {
  color: v-bind('state.color');
}
/* 非首页固定样式 */
.menu_fixed {
  background: white;
}
.menu_fixed .el-menu-item {
  color: #606266;
}

首先默认的class选择器是el-menu-demo和el-menu-demo .el-menu-item分别控制背景颜色和文字颜色(这里文字颜色指的是el-menu内部元素el-menu-item的文字颜色)

menu_fixed作为动态class根据当前路由是否为‘/index’来生效或不生效,并且class类针对相同属性优先选择后者,这意味着步骤4可以很轻易的实现

而针对步骤3,我们需要对滚动条进行监听

// 实时滚动条高度
const scrollTop = () => {
  let scroll = document.documentElement.scrollTop || document.body.scrollTop;
  if (scroll > 70) {
    state.color = '#606266'
    state.bgColor = 'white'
  } else {
    state.color = 'white'
    state.bgColor = 'rgba(0, 0, 0, .3)'
  }
}
onMounted(() => {
  // 监听滚动条位置
  window.addEventListener('scroll', scrollTop, true)
})

到此,header的样式切换就完成啦,如果不使用css变量注入,则需要写四个class类选择器去操控,总之css变量注入的功能很香

如何使用JS将两个数组合并为一个数组
var a=[1,2,3];
var b=[4,5,6];
a.push.apply(a,b);
console.log(a)
//=> a = [1,2,3,4,5,6]
关于 js Promise 中如何取到 [[PromiseValue]] 值

关于这个问题在项目中遇到过,我封装了一个api去调用一个名人名言接口,结果给我返回了Promise {<pending>},原因是封装写法双重return导致的

在这里插入图片描述

api.ts

// 封装axios的外部api请求
import axios from "axios";
const AxiosApi = {
    // 这里双重return返回得到的是promise<pending>,需要then方法取出
    async FamousQuotes(id: any) {
    axios.defaults.baseURL = '/api'
    // 这里的typeid=1可以自定义更换 具体查询地址:https://www.alapi.cn/api/view/7   参数范围1-45
    let api = `/api/mingyan?token=wPsstR6XUhVezM8Y&format=json&typeid=${id}`;
    const text = await axios.get(api)
    .then((res: any) => {
      return res.data.data.content
    })
    .catch((err: any) => {
        console.log("双重return需要先定义外面的text,然后赋值在最外面return")
    })
    return text
  },
};
export default AxiosApi;

据悉,Promise 有三个阶段,处于 pending 阶段 既可以把获得数据直接放在 Promise.resolve(pdata); 然后就可以通过 then(re=>{}) 处理数据了

import AxiosApi from '@/tools/api'
onMounted(() => {
	Promise.resolve(AxiosApi.FamousQuotes(1)).then(function (result: any) {
    	console.log(result)
    	text.value = result
  	})
}
vue3+ts全局注册axios不起作用

类似上面的全局属性,方法见相关链接中的VUE3(十三)main.ts中全局引入axios

解决头部使用 position:fixed; 固定定位后遮住下方内容的问题

最优解:在设置了positon的盒子外部再设置一个相同高度的div即可,本项目中的header就是固定定位,轮播图则是绝对定位

vue3使用计算属性代替过滤器,实现对显示文字字数的限制

vue3移除了filters过滤器,所以该选择computed计算属性来实现

//template
<pre>{{ ellipsisText(text,44) }}</pre>
//ts
const text = ref('')
const ellipsisText = computed(() => {
  return function (val: any, len: any) {
    return val.length > len ? val.slice(0, len) + "..." : val
  }
})

上述例子表明对text的内容进行字数限制超过44个字符后用…来代替

CSS3背景图片background属性简写/连写
background:#F00 url('img/images.png') no-repeat fixed center / cover

主要是注意background-size的写法,需要用/ cover

参考链接:https://cloud.tencent.com/developer/article/1538122

Vue3中的watch监听
const a = ref('')
const b = ref('')
//
const c = reactive({
  name: 'lzm', age: 22,
  more: {
  	phone: '153xxxx9723'
}
})
//
const props = defineProps({
  name: '',
})
//
watch( ,(newValue,oldValue) => {
  
})
监听第一个参数注意
监听单个ref数据a注意不是a.value
监听多个ref数据[a,b]多个用数组格式
监听reactive中的单个属性() => c.name注意第一个参数是一个箭头函数
监听reactive中的多个属性[() => c.name, () =>c.more.phone]
同时监听ref和reactive[a, () =>c.name]

补充

监听第一个参数props本身是一个Proxy(响应式)对象
监听propsprops监听整个对象时不需要使用箭头函数格式
监听props.name() => props.name监听reactive中的单个属性

同理路由也是如此,route.query.key是路由对象route里面的属性

//监听路由query中key的变化
watch(() => route.query.key, (val: any) => {

}
css渐变写法 从左到右颜色渐变

使用渐变色可以让我们的页面颜色更加的舒适,本项目的首页旅游景点轮播图的背景颜色就是渐变色

background:linear-gradient(to right,#fff,#000)
background: linear-gradient(90deg,#fff,#000)
process.env.BASE_URL

process.env是node的一个系统变量,在vue cli中,会对这个process.env做自己的操作,默认情况,再不添加任何自定义环境变量文件的情况下,process.env只有两个属性:

{
    "NODE_ENV": "development",
    "BASE_URL": "/"
}

不管是在development还是production或者其他模式,默认的BASE_URL都是/

我的理解:意味着process.env.BASE_URL默认指向的是public文件夹,与publicPath: "./"差不多

因为本项目内容的图片基本都放在public文件夹,直接使用绝对路径img/1.png就能直接引用

public和assets文件夹的区别:

public中的文件,是不会经过编译的,打包后会生成dist文件夹,public中的文件只是复制一遍,存放静态资源。图片存放在public里,由于不会被压缩所以会很占内存,以本项目为例,图片资源占了99%,代码文件反而只有几百KB

assets里面主要存放js和css,打包后会被压缩,存放动态资源,存放图片需要动态引入

assets文件夹更适合放置会经常变动的资源,public文件夹更适合放置不会变动的资源

尝试直接打印process.env.BASE_URL,结果报错

Uncaught (in promise) ReferenceError: process is not defined

vite里面引入图片

假设图片放在public/img文件夹中,直接按下面方式引入即可

引入 public 中的资源永远应该使用根绝对路径 —— 举个例子,public/icon.png 应该在源码中被引用为 /icon.png

<img src="img/2.jpg" alt="" />
<img src="/img/2.jpg" alt="" />

引入assets里的图片,尝试直接相对路径引入

<!--- 直接引入img、svg可以--->
<img src="@/assets/img/2.jpg" alt="" />
<img src="@/assets/logo.svg" alt="" />
<!--- 但是el-image 不行 --->

尝试使用require引入报错:Uncaught (in promise) ReferenceError: require is not defined

原因:因为 require 是属于 Webpack 的方法,而Vite其不支持require,所以我们需要去寻找 Vite 静态资源处理的方法

这意味着使用require引入模块也不行,比如引入生成token的jsonwebtoken需要采用下面的引入方式

const jwt = require(‘jsonwebtoken’)

但是这在vite中会报错

正确引用assets里面图片资源的方式

// 1	vite3目前好像支持直接引入assets里面的图片了
<img src="@/assets/img/2.jpg" alt="" />	//需要配置@别名
<img src="../assets/img/2.jpg" alt="" />
// 2
<img :src="images" alt="" />
const images = new URL('../assets/img/2.jpg', import.meta.url).href

el-image支持直接引入public的图片,但不支持assets里的图片

直接引入<el-image src="@/assets/img/2.jpg"></el-image>

浏览器会报错:GET http://127.0.0.1:5173/assets/img/1.jpg 404 (Not Found)

watch监听当前路由

当前路由:router.currentRoute.value.path

当前路由参数:route.query.key(key为query传递的参数) || route.params.key

watch监听事件默认首次不会触发,加上{immediate: true,deep: true} 可触发

import { useRouter } from 'vue-router'
const router = useRouter()
watch(() => router.currentRoute.value.path,(toPath) => {
	//要执行的方法
},
{immediate: true,deep: true}
)
// {immediate: true,deep: true} 要加上,不加的首页不会触发要执行的方法
搜索功能的实现

template模块结合了elementplus的autoComplete自动提示框

<el-autocomplete v-model="searchKey" :fetch-suggestions="querySearch" popper-class="my-autocomplete"
   placeholder="Search You want to search" :trigger-on-focus="false" :popper-append-to-body="false"
   @select="handleSelectInput" @keyup.enter.native="handleKeyup">
    <template #suffix>
        <el-icon class="el-input__icon" @click="handleIconClick">
            <Search />
        </el-icon>
     </template>
     <template #default="{ item }">
         <div class="value">{{ item.value }}</div>
              <span class="link">{{ item.link }}</span>
     </template>
</el-autocomplete>

script部分

import { reactive, toRefs, ref, computed, watch, onMounted,defineProps } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus';
// 声明props
const props = defineProps({
    pathIndex: {
        type: String,
        default: ''
    }
})
// 调用路由方法及注册
const router = useRouter()
// 获取当前路由path
// console.log(router.currentRoute.value.path)
//autoComplete业务
interface LinkItem {
    value: string
    en_value: string
}
const links = ref<LinkItem[]>([])
const searchKey = ref('')
// 获取到输入值,传递给searchKey接收
const querySearch = (queryString: string, cb: any) => {
    searchKey.value = queryString
    const results = queryString
        ? links.value.filter(createFilter(queryString))
        : links.value
    cb(results)
}
const createFilter = (queryString: any) => {
    return (restaurant: any) => {
        return (
            restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
        )
    }
}
// 提示数据列表
const loadAll = () => {
    return [
        { value: '张家界', en_value: 'Zhangjiajie' },
        { value: '民宿', en_value: 'Mudi Stream' },
        { value: '牧笛溪', en_value: 'Hotel B&B' },
    ]
}
// 封装跳转逻辑处理
function HandleEvent() {
    if (searchKey.value != '') {
        if (router.currentRoute.value.path != '/search') {
            ElMessage({
                message: "跳转成功!",
                duration: 1000,
                type: 'success'
            })
        }
        router.push({
            path: '/search',
            query: { key: searchKey.value }
        })
        //传递成功后将关键词初始化
        searchKey.value = ''
    } else {
        ElMessage({
            message: "还没有输入关键词哦!请重新输入",
            duration: 1000,
            type: 'warning'
        })
    }
}
// 获取用户选择的关键词
const handleSelectInput = (item: LinkItem) => {
    searchKey.value = item.value
    HandleEvent()
}
// 点击事件 获取用户输入的关键词
const handleIconClick = (ev: Event) => {
    HandleEvent()
}
// 键盘事件 enter触发 获取用户输入的关键词
const handleKeyup = () => {//这一步根据用户按enter键时对应的触发
    HandleEvent()
}
// watch监听路由变化默认清空存在的关键词    使用router.currentRoute.value.path也可以
watch(() => props.pathIndex, (newVale, oldValue) => {
    // console.log(newVale, oldValue)
    if (searchKey.value.length > 0) {
        searchKey.value = ''
    }
})
onMounted(() => {
    links.value = loadAll()
})

上面的代码看似复杂,其实一分解就一目了然了

首先获取用户输入的关键词,在此状态过程中,我们需要监控用户输入的关键词是否为空,不为空才提示跳转成功,然后使用路由router.push()方法跳转到搜索列表(path:’/search’),否则提示用户输入关键词

而触发路由跳转有两种方式,一是点击el-icon图标跳转,而是监听键盘事件enter跳转

获取关键词可直接通过autocomplete业务提供的方法去接收,这里我们定义响应式ref变量searchKey去完成这项光荣的任务

用户输入后或输入状态中跳转其他路由时,我们将其关键词重置为空,来提升用户体验

涉及知识点:子组件使用props接收父组件传递过来的数据、watch监听事件的使用、路由跳转及传参、使用elementplus提供的autocomplete和ElMessage以及el-icon图标等

上述代码只是传递关键词给搜索列表,重头戏在于搜索列表search/index.vue如何返回与关键词匹配的数据,这里我们使用search()方法

script部分代码

// 后台接收路由传递过来的参数
const route = useRoute()
const router = useRouter()
const searchKey = ref<any>(route.query.key)
const resultFlag: any = ref(false)
const siteData: any = Mock.getSiteData()
const hotelData: any = Mock.getHotelData()
// 数组合并
siteData.push.apply(siteData, hotelData)
const searchData = ref<any>([])
// 监听路由本身
watch(() => route.query.key, (val: any) => {
  // console.log(val)
  searchKey.value = val
  if (val != '') {
    searchData.value = siteData.filter((data: any) => {
      return data.title.toLowerCase().search(val) != -1
    })
    if (searchData.value.length > 0) {
      resultFlag.value = true
      console.log("成功返回搜索匹配后的数据:")
    } else {
      ElMessage({ message: "未匹配到符合条件的内容,请再试试吧!", duration: 1000, type: 'info' })
      searchData.value = []
    }
  } else {
    ElMessage({ message: "还没有输入关键词哦", duration: 1000, type: 'warning' })
    searchData.value = []
  }
})
// 初次进入状态
function filterData() {
  // route.query.key
  if (searchKey.value != '') {
    searchData.value = siteData.filter((data: any) => {
      return data.title.toLowerCase().search(searchKey.value) != -1
    })
    if (searchData.value.length > 0) {
      resultFlag.value = true
      console.log("成功返回搜索匹配后的数据:")
    } else {
      ElMessage({ message: "未匹配到符合条件的内容,请再试试吧!", duration: 1000, type: 'info' })
      searchData.value = []
    }
  } else {
    ElMessage({ message: "还没有输入关键词哦", duration: 1000, type: 'warning' })
  }
}
// 处理字数超出限制
const ellipsisText = computed(() => {
  return function (val: any, len: any) {
    return val.length > len ? val.slice(0, len) + "..." : val
  }
})
// 处理路由跳转
function skipPath(id: any, type: any) {
  // 分类索引内容直接写成路由path更直接,都不需要判断了  
  //简写写法
  router.push(type+'?id='+id)
}
onMounted(() => {
  filterData()
  // console.log(route.query.key,searchKey.value)
})

继续解剖代码,首先是跳转成功后,我们将后台数据组合成为一个新数组,然后与关键词进行匹配,最后返回条件匹配过滤后的数组——数据

这里分了两种情况,一是从其他路由页面搜索返回关键词跳转到/search,二是本地路由直接搜索返回关键词,这时是关键词的变化,但本质上都是关键词变化,所以需要监控当前路由的关键词来实时更新搜索内容

并且我们可以发现,上述代码存在冗余,可进一步优化,比如结合watch监听当前路由中的{immediate: true,deep: true}实现首次触发,将filterData()方法的代码块等效替换

开写项目
项目结构

额,这目录结构自己写肯定不现实吧!尝试搜索vscode如何导出目录结构,1秒搞定!

步骤如下:

  1. vscode安装插件,project-tree
  2. 安装之后按ctrl+shift+p,并输入Project Tree回车
  3. 点击要生成目录的项目,回车
  4. 将项目目录生成并存储到README.md中
```
vite+vue+setup+ts
├─ .gitignore
├─ index.html
├─ package-lock.json
├─ package.json		//存放项目的依赖配置
├─ public		//静态资源文件夹  存放页面图标和不支持 JavaScript 情况时的页面。
├─ README en.md
├─ README.md		//项目说明文件
├─ src		//存放 vue 项目的源代码
│  ├─ @types
│  │  └─ moment.d.ts
│  ├─ App.vue
│  ├─ assets		//资源文件,比如存放 css,图片等资源
│  │  ├─ css
│  │  │  ├─ footer.css
│  │  │  ├─ header.css
│  │  │  ├─ index.css
│  │  │  ├─ list.css
│  │  │  └─ login.css
│  │  ├─ ts
│  ├─ components		//组件文件夹,用来存放 vue 的公共组件(注册于全局,在整个项目中通过关键词便可直接输出)
│  │  ├─ autoSearch.vue		//搜索功能组件
│  │  ├─ mainHeader.vue		//头部导航栏组件	header
│  │  ├─ style.css		//swiper样式文件
│  │  ├─ swiper.vue		//swiper轮播图组件
│  │  ├─ upload-plus.vue		//图片上传组件
│  │  └─ uplodimg.vue		图片上传组件
│  ├─ main.ts		//是项目的入口文件,作用是初始化 vue 实例,并引入所需要的插件
│  ├─ mock		//模拟生成数据
│  │  ├─ index.ts		//本项目数据源文件
│  │  └─ Mock.js	//模拟生成数据,暂未使用
│  ├─ router		//用来存放 index.js,这个 js 用来配置路由
│  │  ├─ index.ts
│  │  ├─ routes.ts
│  │  └─ web-routes.ts
│  ├─ store		//vuex状态管理
│  │  ├─ index.ts
│  │  ├─ storage.ts
│  │  └─ user.ts
│  ├─ style.css		//全局css
│  ├─ tools		//用来存放工具类 js
│  │  ├─ api.ts		//封装axios访问每日一言接口
│  │  ├─ text.ts
│  │  └─ upload.ts
│  ├─ views		//用来放主体页面,虽然和组件文件夹都是 vue 文件,但 views 下的 vue 文件是可以用来充当路由 view 的
│  │  ├─ about.vue		//关于我们,该页面设置了登录访问权限
│  │  ├─ Backup.vue		//置顶功能组件
│  │  ├─ footer.vue		//底部footer文件
│  │  ├─ Home.vue		//主页
│  │  ├─ hotel
│  │  │  └─ index.vue
│  │  ├─ index	//首页内容区
│  │  │  ├─ aboutus.vue
│  │  │  ├─ lpimg.vue
│  │  │  ├─ merchant.vue
│  │  │  ├─ newslist.vue
│  │  │  ├─ routelist.vue
│  │  │  ├─ sitelist.vue
│  │  │  ├─ tourlist.vue
│  │  │  └─ travelphoto.vue
│  │  ├─ Index.vue		//首页载体
│  │  ├─ login		//登录
│  │  │  ├─ login-elform.vue
│  │  │  ├─ login.vue
│  │  │  └─ register.vue
│  │  ├─ search		//搜索结果返回列表
│  │  │  └─ index.vue
│  │  ├─ site		//景点
│  │  │  ├─ index.vue		//景点列表
│  │  │  └─ webdetail.vue		//景点详情
│  │  └─ test.vue		//测试文件
│  └─ vite-env.d.ts
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts		//vue 的配置文件

```

仔细发现,部分组件的存放位置是有问题的

功能实现
首页模块

首页布局模仿响应式页面、header导航栏切换样式、切换主题背景、页面置顶功能(为什么不使用elementplus自带的,给忘了)、调用每日一言接口、下载本地md文件功能

搜索模块

搜索功能的简单实现(匹配包含搜索,不是模糊搜索)

路由模块

路由跳转功能、路由前置守卫登录状态页面拦截

状态管理

保存登录信息和注销功能

非首页模块

景点、酒店列表、景点详情

登录模块

模拟登录判断、el-form表单规则

其他模块

测试图片插件、时间格式处理

项目缺陷

1.没有引入评论模块,但是需要数据库的支持,真没法子

2.页面缺乏响应式,后续学习响应式布局

3.使用ts只是纯粹进行基本类型定义规范约束,其他未涉及功能实现

具体实现 & 项目下载

纯粹的复制粘贴过于麻烦,直接分享资源才是王道。
有需要的小伙伴点击此处下载 密码:6rv1
本文档pdf下载 密码:6zw9

登录账号信息:

用户名:lzm 密码:123456

相关链接

说明:以下链接是本项目开发所参考的相关文章或官方文档,感谢这些作者的知识分享!!!

官方文档

Vue3官网:https://v3.cn.vuejs.org/
Vite官网:https://cn.vitejs.dev/
VueRouter官网:https://next.router.vuejs.org/zh/

axios中文文档:https://www.axios-http.cn/docs/intro

vite3中文文档:https://vitejs.cn/vite3-cn/guide/assets.html#importing-asset-as-url

Typed.js: https://mattboldt.com/demos/typed-js/

csdn、简书、掘金

vite.config.js如何配置多个跨域 https://blog.csdn.net/weixin_52691965/article/details/120888332

使用Vite构建Vue3项目,对路由Vue Router 4.x的设置 https://blog.csdn.net/xjtarzan/article/details/119736309

v-viewer:vue3图片查看器 https://blog.csdn.net/ymzhaobth/article/details/122127852

ts报错类型“string | null”的参数不能赋给类型“string”的参数。 不能将类型“null”分配给类型“string”

TypeScript + vue3.0设置全局对象或者属性,出现ts类型错误 https://blog.csdn.net/zlguaizhang/article/details/123404276

Vue3 ts setup getCurrentInstance 使用时遇到的问题及解决

[Vue3] 如何注册和使用全局方法(setup 语法糖中) https://fishpi.cn/article/1662111299980?p=1&m=0

vue3时间转换插件-Moment.js的使用 https://blog.csdn.net/qq_43548590/article/details/121853437

判断对象数组是否包含某个对象 https://blog.csdn.net/qq_44869043/article/details/105981164

Vue3推荐的替代Vuex的新一代状态管理工具:Pinia 配置教程 https://blog.csdn.net/xjtarzan/article/details/123665620

Swiper Vue.js Components https://swiperjs.com/vue#swiper-props

Vue3/Vite中使用Swiper8基础入门教程 https://blog.csdn.net/weixin_59250190/article/details/125990065

vue3 setup语法糖请注意在script标签里加setup,不用return https://blog.csdn.net/qq_54753561/article/details/121044074

如何使用JS将两个数组合并为一个数组 https://blog.csdn.net/qq_43237365/article/details/101553667

分享一个Navicat Premium绿色版,无需破解 http://www.itmind.net/18710.html

关于 js Promise 中如何取到 [[PromiseValue]] 值 https://blog.csdn.net/weixin_44732337/article/details/103297283

VUE3(十三)main.ts中全局引入axios https://blog.csdn.net/qq_39708228/article/details/114662431

解决头部使用 position:fixed; 固定定位后遮住下方内容的问题 https://blog.csdn.net/lolhuxiaotian/article/details/122229393

vue3使用计算属性代替过滤器,实现对显示文字字数的限制 https://blog.csdn.net/weixin_39225682/article/details/119178581

CSS3背景图片background属性简写/连写 https://cloud.tencent.com/developer/article/1538122

Vue3中的watch监听 https://blog.csdn.net/qq_40323256/article/details/127134151

Vue3.2单文件组件setup的语法总结 https://www.jianshu.com/p/8c351aa6f373

让 vite 支持 require https://blog.csdn.net/qq_43806488/article/details/126616481 未亲测

Vue3中Vuex的使用 https://blog.csdn.net/qq_45934504/article/details/123462736

手把手教你vue3引入vue-router路由 https://blog.csdn.net/weixin_45966674/article/details/122260952

如何利用Vue3和element-plus实现图片上传组件 https://www.yisu.com/zixun/690747.html

vscode自动生成项目目录结构 https://blog.csdn.net/liuliuliuliumin123/article/details/113959649

网站工具

在线屏幕颜色提取器取色器拾色器工具
[typed.js]-JavaScript打字动画库

聚合数据-api接口

在线excel转json工具

ALAPI-api接口

最后,觉得有用顺手给我一个赞,欢迎评论区讨论哈!

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高中不复,大学纷飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值