本项目纯粹是为了学习和巩固刚学的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>
因为相比组合式api的写法更加的简洁,不需要return定义的属性、方法,也不需要再注册组件,直接引入即可
setup是Vue3.0后推出的语法糖,并且在Vue3.2版本进行了大更新,像写普通JS一样写vue组件,对于开发者更加友好了
按需引入computed、watch、directive等选项,一个业务逻辑可以集中编写在一起,让代码更加简洁便于浏览。
首先,使用vite创建vue3项目(推荐安装最新版)
Vite 需要 Node.js 版本 >= 12.0.0
npm init vite@latest vue项目名称 -- --template vue
图示:
切换到当前目录、安装依赖、运行
cd 项目名
npm install
npm run dev
注意:使用ts,所有的js文件都需要使用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 不可省略,但ts文件可以不要后缀,并且component推荐使用异步加载的写法
// 导入路由对象
import { createRouter,createWebHistory } from 'vue-router'
// 路径配置 .vue 不可省略
const routes = [
{
path: '/',
name: 'index',
//在vite中使用如下引入组件的写法
component: () => import('@/components/index.vue') //.vue不可省略
}
]
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"
拦截请求和响应 转换请求和响应数据 取消请求 自动转换JSON数据
npm install axios vue-axios --save
vue3推荐的Ui组件库
npm install element-plus --save
一个用于生成token,一个用于解析token
注意:本项目没有使用到,ts报错找不到jwt,应该是需要配置.d.ts文件
模拟生成数据,常用于项目上线的数据测试,但本项目没用到过,不过保留了mock.js文件供大家参考
npm install mockjs --save
用户时间格式的处理转换,比如注册状态的时间需要转换成标准格式
npm install moment --save
用于状态管理,vue3中我们更推荐使用pinia,但是本项目中使用vuex足够了,因为pinia的写法我还不是很熟悉
pinia-plugin-persist用于持久化存储,自行百度
npm install pinia pinia-plugin-persist --save npm install vuex --save
在vue3中使用swiper轮播可以满足特定的轮播需求,尽管elementplus的轮播比swiper配置更简单,ui更好看,但功能性不如swiper。不过目前网上的资料并不多
npm install swiper@latest --save
图片预览插件 v-viewer适配vue3,而vue-photo-preview目前好像不适配vue3,因为我使用时会报错
npm install v-viewer@next --save npm insall vue-easy-lightbox --save
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全局注册后不起作用,局部重新引入即可
当vue文件中的css和js代码过多时,我们可以导入到外部css和ts文件中
//css的引入方式
@import "@/assets/css/index.css"
//js的引入方式
import Header from "@/assets/js/header"
ts是js的超集,主要是定义类型
JSON.parse(localStorage.getItem(key)|| '0')
以本项目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('匹配失败,登录失败')
}
下面的链接或许可以帮到你,说说我的方法
访问https://swiperjs.com/demos#centered,然后按下图操作
Jerry
使用上面的方式可以更加方便的操控元素样式了,都不需要指定标签,但是使用了变量注入的代码不能分离到外部css,否则会获取不到的
在我的项目中,可以用来设置主题和header导航栏的样式
使用css变量注入实现header导航栏的样式切换
1.默认进入首页(path: ‘/index’)时header背景显示透明色transition,文字颜色显示白色white
2.鼠标移入时,背景显示白色,文字颜色显示#606266;鼠标移出还原
3.滚动条移动时,背景显示白色,文字颜色显示#606266
4.切换到非首页的路由path时,样式固定为背景显示白色,文字颜色显示#606266
... 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变量注入的功能很香
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]
关于这个问题在项目中遇到过,我封装了一个api去调用一个名人名言接口,结果给我返回了Promise {
,原因是封装写法双重return导致的
api.ts
// 封装axios的外部api请求
import axios from "axios";
const AxiosApi = {
// 这里双重return返回得到的是promise,需要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(十三)main.ts中全局引入axios
最优解:在设置了positon的盒子外部再设置一个相同高度的div即可,本项目中的header就是固定定位,轮播图则是绝对定位
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个字符后用…来代替
background:#F00 url('img/images.png') no-repeat fixed center / cover
主要是注意background-size的写法,需要用/ cover
参考链接:https://cloud.tencent.com/developer/article/1538122
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(响应式)对象 |
---|---|---|
监听props | props | 监听整个对象时不需要使用箭头函数格式 |
监听props.name | () => props.name | 同监听reactive中的单个属性 |
同理路由也是如此,route.query.key是路由对象route里面的属性
//监听路由query中key的变化
watch(() => route.query.key, (val: any) => {
})
使用渐变色可以让我们的页面颜色更加的舒适,本项目的首页旅游景点轮播图的背景颜色就是渐变色
background:linear-gradient(to right,#fff,#000)
background: linear-gradient(90deg,#fff,#000)
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
假设图片放在public/img文件夹中,直接按下面方式引入即可
引入
public
中的资源永远应该使用根绝对路径 —— 举个例子,public/icon.png
应该在源码中被引用为/icon.png
<img src="img/2.jpg" alt="" />
<img src="/img/2.jpg" alt="" />
引入assets里的图片,尝试直接相对路径引入
<img src="@/assets/img/2.jpg" alt="" />
<img src="@/assets/logo.svg" alt="" />
尝试使用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里的图片
直接引入
浏览器会报错:
GET http://127.0.0.1:5173/assets/img/1.jpg 404 (Not Found)
当前路由: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自动提示框
{{ item.value }}
{{ item.link }}
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秒搞定!步骤如下:
- vscode安装插件,project-tree
- 安装之后按ctrl+shift+p,并输入Project Tree回车
- 点击要生成目录的项目,回车
- 将项目目录生成并存储到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/
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接口
最后,觉得有用顺手给我一个赞,欢迎评论区讨论哈!