Vue 3.0 (CompostionAPI + VueRouter + Axios + Bootstrap + Sass) 文章管理项目

1. Vue 3.0 介绍和脚手架文档

2. Vue 3.0 快速入门指南

2.1 Vue 3.0 搭建脚手架项目
  • 创建项目
    • vue create 项目名
    • 根据个人情况设置配置
2.2 Vue 3.0 文件结构梳理

在这里插入图片描述

  • node_modules: 这个目录存放的是项目的所有依赖,即 npm install 命令下载下来的文件
  • public: 静态资源
  • index.html: 主页
  • src: 这个目录下存放项目的源码,即开发者写的代码放在这里
  • assets: 静态文件
  • components: 目录用来存放组件(一些可复用,非独立的页面)
  • App.vue: 是一个Vue根组件,也是项目的第一个Vue组件
  • main.js: 整个项目的入口文件
  • package.json: 定义了项目的所有依赖,包括开发时依赖和发布时依赖
  • .babel.config.js: 该文件是babel的配置文件
2.3 Vue 3.0 组件的组成
  • 组件组成
    • html 结构部分
    • script 逻辑部分
    • style 样式部分
<template>

</template>

<script>
export default {

}
</script>

<style>

</style>
2.4 Vue 3.0 CompositionAPI
<template>
	<div class="my-app">
		<h1>{{counter}}</h1>
		<button @click="increment(1)">increment</button>
		<button @click="increment(-1)">decrement</button>
	</div>
</template>

<script>
import { ref } from 'vue'
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
	setup(){
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
			counter.value += num;
		};

		return { counter, increment };
	}
}
</script>

<style>
</style>

3. Vue 3.0 组件

3.1 Vue 3.0 组件拆分
  • CounterView.vue
<template>
	<h1>1000</h1>
</template>

<script>
export default {};
</script>

<style>
</style>
  • App.vue
<template>
	<div class="my-app">
		<!-- <CounterView></CounterView> -->
		<!-- <counter-view></counter-view> -->
		<CounterView/>
		<button @click="increment(1)">increment</button>
		<button @click="increment(-1)">decrement</button>
	</div>
</template>

<script>
import { ref } from 'vue'
import CounterView from '@/components/CounterView.vue";
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
	// 组件注册
	components: {
		CounterView,
		// "zx-counter-view": CounterView,
	},
	setup(){
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
			counter.value += num;
		};

		return { counter, increment };
	}
}
</script>

<style>
</style>
3.2 Vue 3.0 属性传值
  • CounterView.vue
<template>
	<h1>{{counter}}</h1>
</template>

<script>
export default {
	// props: ['counter'],
	props: {
		counter: {
			type: Number, // 类型
			required: true, // 可以传也可以不传
			default: 500, // 默认值
		}
	}
};
</script>

<style>
</style>
  • App.vue
<template>
	<div class="my-app">
		<!-- <CounterView></CounterView> -->
		<!-- <counter-view></counter-view> -->
		<CounterView :counter="counter"/>
		<button @click="increment(1)">increment</button>
		<button @click="increment(-1)">decrement</button>
	</div>
</template>

<script>
import { ref } from 'vue'
import CounterView from '@/components/CounterView.vue";
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
	// 组件注册
	components: {
		CounterView,
		// "zx-counter-view": CounterView,
	},
	setup(){
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
			counter.value += num;
		};

		return { counter, increment };
	}
}
</script>

<style>
</style>
3.3 Vue 3.0 事件注册
  • CounterController.vue
<template>
	<button @click="increment(1)">increment</button>
	<button @click="increment(-1)">decrement</button>
</template>
<script>
export default {
	emits: ["onIncrement"],
	setup(props, context){
		// 增加方法
		const increment = (num) => {
			// 注册事件 emit
			// 传入事件名、值
			context.emit("onIncrement", num)
		};

		return { increment };
	}
}
</script>

<style>
</style>
  • App.vue
<template>
	<div class="my-app">
		<!-- <CounterView></CounterView> -->
		<!-- <counter-view></counter-view> -->
		<CounterView :counter="counter" />
		<CounterController @onIncrement="increment($event)"/>
	</div>
</template>

<script>
import { ref } from 'vue'
import CounterView from '@/components/CounterView.vue";
import CounterController from '@/components/CounterController.vue";
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
	// 组件注册
	components: {
		CounterView,
		// "zx-counter-view": CounterView,
	},
	setup(){
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
			counter.value += num;
		};

		return { counter, increment };
	}
}
</script>

<style>
</style>
3.4 Vue 3.0 调整项目引入 Bootstrap
  • 引入 Bootstrap
    • npm install bootstrap@4.5.2 --save
  • App.vue
<style>
	@import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
</style>
3.5 Vue 3.0 模拟数据和属性传值
  • 模拟列表内容数据,并将数据传入列表组件
  • ResourceHome.vue
<!-- 将数据传入列表组件 -->
<ResourceList :resources="resources" />
...
setup(){
    const data = reactive({
        resources: [
          {
            _id: "1",
            title: "2021 前端面试 | “HTML + CSS + JS”专题",
            description: "BAT面试1000题——数据结构(841~850题)",
            type: "video",
            link: ""
          },
          {
            _id: "2",
            title: "一篇搞定前端高频手撕算法题(36道)",
            description: "《JavaScript 20 年》中文在线版发布",
            type: "book",
            link: ""
          },
          {
            _id: "3",
            title: "32个手撕JS,彻底摆脱初级前端(面试高频)",
            description: "56 道高频 JavaScript 与 ES6+ 的面试题及答案",
            type: "video",
            link: ""
          },
          {
            _id: "4",
            title: "字节跳动2020届秋招提前批前端面经",
            description: "初入WEB前端的新手,掌握这些核心知识点,年薪冲破20W",
            type: "book",
            link: ""
          },
          {
            _id: "5",
            title: "前方预警!史上最全前端面试题来袭!(附答案)",
            description: "做一个数据可视化项目的难点在什么地方?",
            type: "video",
            link: ""
          },
          {
            _id: "6",
            title: "JavaScript数据类型详解",
            description: "nodejs的websocket的服务器端是如何实现的?",
            type: "book",
            link: ""
          },
        ]
    })
    return { 
      // 解包
      ...toRefs(data),
    }
}
  • ResourceList.vue
<template>
  <!-- {/* 数据列表 Starts */} -->
  <ul class="list-group mb-3">
    <li v-for="resource in resources" 
        :key="resource._id"
        class="list-group-item d-flex justify-content-between lh-condensed"
    >
      <div>
        <h6 class="my-0">{{ resource.title }}</h6>
        <small class="text-muted">{{ resource.description }}</small>
      </div>
      <span class="text-muted">{{ resource.type }}</span>
    </li>
  
  </ul>
  <!-- {/* 数据列表 Ends */} -->
</template>

<script>
export default {
  props: {
    resources: {
      type: Array,
      default: () => [],

    }
  }
}
</script>

<style>

</style>
3.6 Vue 3.0 方法和计算属性
  • 利用方法实现列表数据的统计
  • ResourceHome.vue
<span class="badge badge-secondary badge-pill">{{getResourcesLength()}}</span>
...
// 列表数量统计方法
const getResourcesLength = () => {
  return data.resources.length
}

// 导出数据
return { 
  getResourcesLength,
}
  • 利用计算属性实现列表数据的统计
  • ResourceHome.vue
<span class="badge badge-secondary badge-pill">{{getResourcesLength()}}</span>
...
import { computed } from 'vue';
...
// 列表数量统计(计算属性实现)
const getResourcesLength = computed(() => {
  return data.resources.length
})
3.7 Vue 3.0 实现视图切换
  • 使用v-if/v-else实现视图切换
<!-- {/* 更新数据 Starts */} -->
<div class="col-md-8 order-md-1">
  <h4 class="mb-3">数据<button @click="isDetailView = !isDetailView" class="btn btn-sm btn-success">切换</button></h4>
  <ResourceUpdate v-if="isDetailView" />
  <!-- 数据详情 -->
  <ResourceDetail v-else></ResourceDetail>
</div>
<!-- 更新数据 Ends  -->
...
import { ref } from 'vue';
...
// 3. 定义视图切换属性
const isDetailView = ref(true)

// 导出数据
return { 
  isDetailView
}

4. Vue 3.0 数据的交互

4.1 Vue 3.0 添加数据
  • ResourceHome.vue
<!-- 添加按钮 -->
<button @click="addResource" class="btn btn-sm btn-primary">
  添加数据
</button>
...
// 4. 添加数据事件
const addResource = () => {
   // debugger
   // 随机获取id
   const _id = "_" + Math.random().toString(36).slice(2)
   // 随机获取列表内容类型
   const type = ["book","blog","video"][Math.floor(Math.random() * 3)]
   // 新的列表内容
   const newResource = {
     _id,
     title:`${_id} title`,
     description:`${_id} description`,
     link:'',
     type,
   }
   // 添加到数据列表前列
   data.resources.unshift(newResource)
 }
4.2 Vue 3.0 调整样式
  • 修改切换按钮样式
  • ResourceHome.vue
<!-- {/* 更新数据 Starts */} -->
<div class="col-md-8 order-md-1">
  <h4 class="mb-3">数据
    <button 
      @click="isDetailView = !isDetailView" 
      :class="`btn btn-sm ${togglesBtnClass}` ">
      {{!isDetailView ? "更新" : "详情"}}
    </button>
  </h4>
  <ResourceUpdate v-if="isDetailView" />
  <!-- 数据详情 -->
  <ResourceDetail v-else></ResourceDetail>
</div>
<!-- 更新数据 Ends  -->
...
// 5. 切换按钮样式
const togglesBtnClass = computed(() => {
  return !isDetailView.value ? "btn-primary" : "btn-warning"
})
  • 修改数据列表固定高度
  • ResourceList.vue
<style scope lang="scss">
.resource-list {
  max-height: 350px;
  overflow-y: auto;
}

// css语法
// .resource-list {
//   max-height: 350px;
//   overflow-y: auto;
// }
</style>
4.3 Vue 3.0 选中数据显示详情
  • 选中ResourceList组件中的列表数据,将点击事件传入ResourceHome父组件
  • 从ResourceHome父组件传入ResourceDetail数据详情组件
  • ResourceList.vue
<template>
	<!-- {/* 数据列表 Starts */} -->
	<ul class="list-group mb-3 resource-list">
	  <li v-for="resource in resources" 
	      :key="resource._id"
	      :class="list-group-item d-flex justify-content-between lh-condensed resource-list-item"
	      @click="onItemClick(resource)"
	  >
	    <div>
	      <h6 class="my-0">{{ resource.title }}</h6>
	      <small class="text-muted">{{ resource.description }}</small>
	    </div>
	    <span class="text-muted">{{ resource.type }}</span>
	  </li>
	
	</ul>
	<!-- {/* 数据列表 Ends */} -->
</template>

<script>
import { computed } from 'vue'
export default {
  props: {
    resources: {
      type: Array,
      default: () => [],
    },
  },
  setup(props, context) {
    // methods
    // 1. 选中列表显示数据事件
    const onItemClick = (resource) => {
      // 注册事件
      // 事件名、参数
      context.emit("handleItemClick",resource)
    }
    
    return { onItemClick }
  }
}
</script>

<style>
</style>
  • ResourceHome.vue
<template>
	<!-- 数据详情 -->
    <ResourceDetail :resource="selectedResource" v-else></ResourceDetail>
</template>

<script>
  ...
  setup(){
      // =================================================================
      // data
      // 6. 定义选中的数据
      const selectedResource = ref(null);

      // =================================================================
      // computed
      // 7. 调用数据
      const activeResource = computed(() => {
        return selectedResource.value || 
        (getResourcesLength > 0 && data.resources[0]) ||
        null
      })
      // =================================================================
      // methods
      // 6. 选中列表显示数据事件
      const selectResource = (resource) => {
        // console.log(resource);
        selectedResource.value = resource
        // console.log(selectedResource.value);
      }
  }
};
</script>

<style>
</style>
  • ResourceDetail.vue
<template>
  <!-- {/* 数据详情 Starts */} -->
  <div class="card" v-if="!resource?._id">
    <div class="card-body">No Resource is selected :(</div>
  </div>
  <div class="card" v-else>
    <div class="card-header">{{ resource.title }}</div>
    <div class="card-body">
      <blockquote class="blockquote mb-0">
        <p>{{ resource.description }}</p>
        <footer class="text-muted mb-2">{{ resource.type }}</footer>
      </blockquote>
      <a href="#" class="btn btn-primary">编辑</a>
    </div>
  </div>
  <!-- {/* 数据详情 Ends */} -->
</template>

<script>
export default {
  props: {
    resource: {
      // type: Object,
      validator: (prop) => typeof prop === "object" || prop === null,
      required: true,
    },
  },
};
</script>

<style>
</style>
4.4 Vue 3.0 选中状态
  • 选中列表数据改变其样式
  • ResourceList.vue
<template>
	<!-- {/* 数据列表 Starts */} -->
	<ul class="list-group mb-3 resource-list">
	  <li v-for="resource in resources" 
	      :key="resource._id"
	      :class="`${activeItemClass(resource)} list-group-item d-flex justify-content-between lh-condensed resource-list-item`"
	      @click="onItemClick(resource)"
	  >
	    <div>
	      <h6 class="my-0">{{ resource.title }}</h6>
	      <small class="text-muted">{{ resource.description }}</small>
	    </div>
	    <span class="text-muted">{{ resource.type }}</span>
	  </li>
	
	</ul>
	<!-- {/* 数据列表 Ends */} -->
</template>

<script>
import { computed } from 'vue'
export default {
  props: {
    resources: {
      type: Array,
      default: () => [],

    },
    activeId: String,
  },
  setup(props, context) {
    // methods
    // 1. 选中列表显示数据事件
    const onItemClick = (resource) => {
      // 注册事件
      // 事件名、参数
      context.emit("handleItemClick",resource)
    }

    // computed
    // 2. 选中样式
    const activeItemClass = computed(() => {
      return (resource) => resource._id === props.activeId ? "is-active" : ""
    })
    
    return { onItemClick, activeItemClass }
  }

}
</script>

<style scope lang="scss">
.resource-list {
  max-height: 350px;
  overflow-y: auto;

  &-item {
    cursor: pointer;

    &:hover {
      background-color: #f3f3f3;
    }
  }

  .is-active {
    background-color: #f3f3f3;
  }
}

// css语法
// .resource-list {
//   max-height: 350px;
//   overflow-y: auto;
// }

// .resource-list-item {
//   cursor: pointer;
// }

// .resource-list:hover {
//   background-color: #f3f3f3;
// }
</style>
4.5 Vue 3.0 发送数据请求 axios
  • 安装 axios
    • npm install axis --save
  • ResourceHome.vue
// 生命周期钩子函数
onMounted(async () => {
	const resources = await fetchResources()
});
  • index.js
import axios from 'axios'

// 请求数据方法
export function fetchResource(){
    return axios.get("https://vue3-fjord-81553.herokuapp.com/api/resources")
}
4.6 Vue 3.0 代理解决跨域
4.6.1 跨域问题
  • blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
4.6.2 跨域
  • 只要不同源,即为跨域
  • 同源策略: 以下任意一个不同,即为跨域
    • 协议头 http https file
    • 域名 baidu.com www.taobao.com
    • 端口号 8080 441 21 22
  • 域名不同 = 跨域
    • https://www.baidu.com:8888
    • https://www.taobao.com:8888
  • 协议不同 = 跨域
    • http://www.baidu.com:21
    • https://www.baidu.com:21
  • 端口不同 = 跨域
    • http://www.baidu.com:21
    • http://www.baidu.com:22
  • 同源
    • http://www.baidu.com:21
    • http://www.baidu.com:21/map/api/citi
4.6.3 解决跨域
  • jsonp

  • 服务器代理

  • 后端允许跨域

  • vue.config.js

module.exports = {
  devServer: {
    // 代理
    proxy: {
      "^/api": {
        target: "https://vue3-fjord-81553.herokuapp.com",
        changeOrigin: true,
      },
    },
  },
};
4.7 Vue 3.0 数据赋值
  • 请求接口数据
  • index.js
import axios from 'axios'

// 请求数据方法
export function fetchResources(){
    return axios.get("/api/resources").then((res) => res.data)
}
  • 数据赋值
  • ResourceHome.vue
// 生命周期钩子函数
onMounted(async () => {
  const resources = await fetchResources()
  data.resources = resources
});
4.8 Vue 3.0 调整更新组件
  • ResourceUpdate.vue
<template>
  <form>
    <div class="mb-3">
      <label htmlFor="title">标题</label>
      <input
        v-model="uResource.title"
        type="text"
        class="form-control"
        id="title"
        placeholder="title...."
      />
    </div>
    <div class="mb-3">
      <label for="description">描述</label>
      <textarea
        v-model="uResource.description"
        class="form-control"
        id="description"
        placeholder="描述"
      ></textarea>
    </div>
    <div class="mb-3">
      <label htmlFor="type">类型<span class="text-muted">(可选)</span></label>
      <input v-model="uResource.type" type="text" class="form-control" id="type" placeholder="类型..."/>
    </div>
    <div class="mb-3">
      <label htmlFor="link">链接</label>
      <div class="input-group">
        <input
          v-model="uResource.link"
          type="text"
          class="form-control"
          id="link"
          placeholder="链接...."
        />
      </div>
    </div>
    <hr class="mb-4" />
    <button class="btn btn-primary btn-lg btn-block" type="submit">提交</button>
  </form>
</template>

<script>
import { ref } from 'vue'
export default {
  props: {
    resource: Object
  },
  setup(props, context) {
    const uResource = ref(props.resource)
    
    return { uResource }
  }
}
</script>

<style>
</style>
  • ResourceHome.vue
<ResourceUpdate :resource="activeResource" v-if="isDetailView" />
4.9 Vue 3.0 watch 监听数据实时切换
  • 利用 watch 监听数据是否变化
  • ResourceUpdate.vue
import { ref, watch } from 'vue'
export default {
  props: {
    resource: Object
  },
  setup(props, context) {
    const uResource = ref(props.resource)
    
    watch(
      () => props.resource,
      (resource, prevResource) => {
        uResource.value = resource
      }
    )
    
    return { uResource }
  }
}

5. Vue 3.0 数据的处理

5.1 Vue 3.0 调整类型选项
  • ResourceUpdate.vue
<div class="mb-3">
  <label htmlFor="type">类型</label>
  <select class="form-control" id="type" v-model="uResource.type">
    <option v-for="(resourceType, index) in types" 
            :key="index" 
            :value="resourceType">{{resourceType}}</option>
    
  </select>

</div>
...
// 类型选项
const types = ["blog","video","book"]
5.2 Vue 3.0 数据更新到服务器端
  • index.js
// 更新数据方法
// 传入对应id,及新内容
export function updateResource(id, resource){
    return axios
        .patch(`/api/resources/${id}`, resource)
        .then((res) => res.data)
}
  • ResourceUpdate.vue
<button 
      @click="handleUpdate" 
      class="btn btn-primary btn-lg btn-block" 
      type="button">提交
</button>
...
import { updateResource } from '@/actions'
...
// 提交事件(异步)
const handleUpdate = async () => {
  // 传入对应id和新内容
  const updatedResource = await updateResource(uResource.value._id, uResource.value)
}


// 将更新的数据传给父组件
context.emit("onUpdateResource", updateResource)
  • ResourceHome.vue
<ResourceUpdate 
    @onUpdateResource="handleUpdateResource($event)" 
     :resource="activeResource" 
     v-if="isDetailView" />
...
// 8. 获取更新数据
const handleUpdateResource = (newResource) => {
  // 拿到原先存储数据的数组下标
  const index = data.resources.findIndex(
    (resource) => resource._id === newResource._id
  )
  data.resources[index] = newResource

  selectResource(newResource)
}
5.3 Vue 3.0 弹窗提醒
  • ResourceUpdate.vue
<div v-if="alert?.success" class="alert alert-success">{{ alert.success }}</div>
<div v-if="alert?.error" class="alert alert-danger">{{ alert.error }}</div>
...
// 封装方法
// 初始化弹窗状态
const initAlert = () => {
  return { success: null, error: null }
}

// 弹窗状态
const setAlert = (type, message) => {
  data.alert = initAlert()
  data.alert[type] = message
}

// 提交事件(异步)
const handleUpdate = async () => {
	// 抛出异常
	try {
	  // 传入对应id和新内容
	  const updatedResource = await updateResource(
	    uResource.value._id, 
	    uResource.value
	  )
	
	  // 将更新的数据传给父组件
	  context.emit("onUpdateResource", updatedResource)
	  // 提交成功后弹窗信息
	  setAlert("success","Resource was updated")
	} catch (error) {
	  setAlert("error", error?.message)
	}
 
}
5.4 Vue 3.0 设置定时器取消提醒
  • ResourceUpdate.vue
setup(props, context) {
  // 接收数据
  const uResource = ref(props.resource)
  // 类型选项
  const types = ["blog","video","book"]

  // 弹窗信息
  const data = reactive({
    alert: { success: null, error: null },
    // 定义定时器
    timeoutId: null
  })
  
  // 监听数据切换
  watch(
    () => props.resource,
    (resource, prevResource) => {
      // 判断改变的值存不存在
      if(resource && (resource._id !== prevResource._id)){
        // 关闭定时器
        clearAlertTimeout()
        data.alert = initAlert()
      }

      uResource.value = resource
    }
  )

  // 封装方法
  // 初始化弹窗状态
  const initAlert = () => {
    return { success: null, error: null }
  }

  // 钩子函数
  // 离开组件之前调用
  onBeforeUnmount(() => {
    // 清除定时器方法
    clearAlertTimeout()
  })

  // 清除定时器方法
  const clearAlertTimeout = () => {
    data.timeoutId && clearTimeout(data.timeoutId)
  }

  // 弹窗状态
  const setAlert = (type, message) => {
    data.alert = initAlert()
    data.alert[type] = message

    // 设置定时器
    data.timeoutId = setTimeout(() => {
      data.alert = initAlert()
    },3000)
  }
}
5.5 Vue 3.0 删除数据
<template>
  <button @click="handleDeleteResource()" class="btn btn-sm btn-danger">
    删除
  </button>
</template>

<script>
import { deleteResource } from "@/actions";
export default {
  props: {
    activeId: String,
  },
  setup(props, context) {
    const handleDeleteResource = async () => {
      // 请求数据接口
      // 拿到要删除的内容
      const deleteData = await deleteResource(props.activeId);
      // 传递给父组件
      context.emit("onResourceDelete", deleteData);
      
    };
    return { handleDeleteResource };
  },
};
</script>

<style scoped>
</style>

6. Vue 3.0 路由和搜索功能

6.1 Vue 3.0 配置路由
  • 安装路由
    • yarn add vue-router@next
  • router.js
import { createRouter, createWebHistory } from "vue-router";
import ResourceHome from "@/views/ResourceHome.vue";
import ResourceNew from "@/views/ResourceNew.vue";

const routes = [
  { path: "/", component: ResourceHome },
  { path: "/new", component: ResourceNew}
];

const router = createRouter({
  history: createWebHistory(),
  //   routes: routes,
  routes,

});

export default router;
  • main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App);
// 挂载
app.use(router);
app.mount("#app");
6.2 Vue 3.0 子级路由
  • router.js
import { createRouter, createWebHistory } from "vue-router";
import ResourceHome from "@/views/ResourceHome.vue";
import ResourceNew from "@/views/ResourceNew.vue";
import ResourceRoutes from "@/views/ResourceRoutes.vue"

const routes = [
  // =================================================================
  // { path: "/", component: ResourceHome },
  // { path: "/new", component: ResourceNew}

  // =================================================================
  // 重定向、定义名字
  // http://localhost:8080/resources
  // { 
  //   path: "/", 
  //   name: "base", 
  //   redirect: { name: "resourceHomePage"} 
  // },
  // { 
  //   path: "/resources", 
  //   name: "resourceHomePage", 
  //   component: ResourceHome 
  // },
  // { 
  //   path: "/resources/new", 
  //   name: "resourceNewPage", 
  //   component: ResourceNew
  // }

  // =================================================================
  // 子路由
  { 
    path: "/", 
    name: "base", 
    redirect: { name: "resourceHomePage"} 
  },
  { 
    path: "/resources", 
    name: "resourceHomePage", 
    component: ResourceRoutes,
    children: [
      { 
        path: "",
        name: "resourceHome",
        component: ResourceHome,
      },
      { 
        path: "new",
        name: "resourceNewPage",
        component: ResourceNew,
      }
    ]
  }
];

const router = createRouter({
  history: createWebHistory(),
  //   routes: routes,
  routes,
});

export default router;
6.3 Vue 3.0 导航
  • Header.vue
<router-link class="btn btn-outline-primary mr-2" :to="{ name: 'resourceHomePage' }">首页</router-link>
<router-link class="btn btn-outline-primary" :to="{ name: 'resourceNewPage' }">添加</router-link>
  • router.js
import { createRouter, createWebHistory } from "vue-router";
import ResourceHome from "@/views/ResourceHome.vue";
import ResourceNew from "@/views/ResourceNew.vue";
import ResourceRoutes from "@/views/ResourceRoutes.vue"

const routes = [
  // 重定向、定义名字
  // http://localhost:8080/resources
  { 
    path: "/", 
    name: "base", 
    redirect: { name: "resourceHomePage"} 
  },
  { 
    path: "/resources", 
    name: "resourceHomePage", 
    component: ResourceHome 
  },
  { 
    path: "/resources/new", 
    name: "resourceNewPage", 
    component: ResourceNew
  }
];

const router = createRouter({
  history: createWebHistory(),
  //   routes: routes,
  routes,
  linkExactActiveClass: "active",
});

export default router;
6.4 Vue 3.0 重构更新组件
  • ResourceForm.vue
<template>
  <form>
    <div v-if="alert?.success" class="alert alert-success">
      {{ alert.success }}
    </div>
    <div v-if="alert?.error" class="alert alert-danger">{{ alert.error }}</div>
    <div class="mb-3">
      <label for="title">标题</label>
      <input
        v-model="uResource.title"
        type="text"
        class="form-control"
        id="title"
        placeholder="title...."
      />
    </div>

    <div class="mb-3">
      <label for="description">描述</label>
      <textarea
        v-model="uResource.description"
        class="form-control"
        id="description"
        placeholder="描述"
      ></textarea>
    </div>
    <div class="mb-3">
      <label for="type">类型</label>
      <select class="form-control" id="type" v-model="uResource.type">
        <option
          v-for="(resourceType, index) in types"
          :key="index"
          :value="resourceType"
        >
          {{ resourceType }}
        </option>
      </select>
    </div>
    <div class="mb-3">
      <label for="link">链接</label>
      <div class="input-group">
        <input
          v-model="uResource.link"
          type="text"
          class="form-control"
          id="link"
          placeholder="链接...."
        />
      </div>
    </div>
    <hr class="mb-4" />
    <button
      @click="submitForm()"
      class="btn btn-primary btn-lg btn-block"
      type="button"
    >
      提交
    </button>
  </form>
</template>

<script>
import { ref, watch } from "vue";

export default {
  props: {
    resource: Object,
    alert: Object,
  },
  setup(props, context) {
    const uResource = ref(props.resource);
    const types = ["blog", "video", "book"];

    watch(
      () => props.resource,
      (resource, prevResource) => {
        uResource.value = resource;
      }
    );

    const submitForm = () => {
      context.emit("onFormSubmit", uResource);
    };

    return {
      uResource,
      types,
      submitForm,
    };
  },
};
</script>
  • ResourceUpdate.vue
<template>
  <ResourceForm
    @onFormSubmit="handleUpdate($event)"
    :resource="resource"
    :alert="alert"
  />
</template>
6.5 Vue 3.0 添加组件
  • ResourceNew.vue
<template>
  <ResourceForm
    @onFormSubmit="handleCreate($event)"
    :resource="resource"
    :alert="alert"
  />
</template>

<script>
import ResourceForm from "@/components/ResourceForm.vue";
import { useRouter } from "vue-router";
import { createResource } from "@/actions";
import { reactive, toRefs, onBeforeUnmount } from "vue";
export default {
  components: {
    ResourceForm,
  },
  setup() {
    const router = useRouter();
    const data = reactive({
      resource: {
        title: "",
        description: "",
        type: "video",
        link: "",
      },
      alert: {
        success: null,
        error: null,
      },
      timeoutId: null,
    });

    const initAlert = () => {
      return {
        success: null,
        error: null,
      };
    };

    onBeforeUnmount(() => {
      clearAlertTimeout();
    });

    const clearAlertTimeout = () => {
      data.timeoutId && clearTimeout(data.timeoutId);
    };

    const setAlert = (type, message) => {
      data.alert = initAlert();
      data.alert[type] = message;
      data.timeoutId = setTimeout(() => {
        data.alert = initAlert();
        router.push("/");
      }, 2000);
    };

    const handleCreate = async (resource) => {
      // console.log(resource.value);
      await createResource(resource.value);
      setAlert("success", "Resource was created");
    };

    return {
      ...toRefs(data),
      handleCreate,
    };
  },
};
</script>

<style lang="scss" scoped></style>
6.6 Vue 3.0 详情跳转
  • ResourceDetailPage.vue
<template>
  <ResourceDetail :resource="resource">
    <template #buttonLink>
      <button @click="$router.go(-1)" class="btn btn-outline-success">
        返回
      </button>
    </template>
  </ResourceDetail>
</template>

<script>
import { useRoute } from "vue-router";
import { fetchResource } from "@/actions";
import { toRefs, reactive, onMounted } from "vue";
import ResourceDetail from "@/components/ResourceDetail.vue";
export default {
  components: {
    ResourceDetail,
  },
  setup() {
    const route = useRoute();
    const data = reactive({
      resource: null,
    });
    onMounted(async () => {
      // console.log(route.params.id);
      const { id } = route.params;
      data.resource = await fetchResource(id);
    });

    return {
      ...toRefs(data),
    };
  },
};
</script>

<style scoped>
</style>
6.7 Vue 3.0 显示详情页面
  • ResourceList.vue
<template>
  <!-- {/* 数据列表 Starts */} -->
          <ul class="list-group mb-3 resource-list">
            <li v-for="resource in resources" 
                :key="resource._id"
                :class="`${activeItemClass(resource)} list-group-item d-flex justify-content-between lh-condensed resource-list-item`"
                @click="onItemClick(resource)"
            >
              <div>
                <h6 class="my-0">{{ resource.title }}</h6>
                <small class="text-muted">{{ resource.description }}</small>
              </div>
              <span class="text-muted">{{ resource.type }}</span>
            </li>
          
          </ul>
          <!-- {/* 数据列表 Ends */} -->
</template>

<script>
import { computed } from 'vue'
export default {
  props: {
    resources: {
      type: Array,
      default: () => [],

    },
    activeId: String,
  },
  setup(props, context) {
    // methods
    // 1. 选中列表显示数据事件
    const onItemClick = (resource) => {
      // 注册事件
      // 事件名、参数
      context.emit("handleItemClick",resource)
    }

    // computed
    // 2. 选中样式
    const activeItemClass = computed(() => {
      return (resource) => resource._id === props.activeId ? "is-active" : ""
    })


    return { onItemClick, activeItemClass }
  }

}
</script>

<style scope lang="scss">
.resource-list {
  max-height: 350px;
  overflow-y: auto;

  &-item {
    cursor: pointer;

    &:hover {
      background-color: #f3f3f3;
    }
  }

  .is-active {
    background-color: #f3f3f3;
  }
}

// css语法
// .resource-list {
//   max-height: 350px;
//   overflow-y: auto;
// }

// .resource-list-item {
//   cursor: pointer;
// }

// .resource-list:hover {
//   background-color: #f3f3f3;
// }
</style>
6.8 Vue 3.0 slot 插槽
  • ResourceDetail.vue
<slot name="buttonLink"></slot>
  • ResourceDetailPage.vue
<template>
  <ResourceDetail :resource="resource">
    <template #buttonLink>
      <button @click="$router.go(-1)" class="btn btn-outline-success">
        返回
      </button>
    </template>
  </ResourceDetail>
</template>
6.9 Vue 3.0 搜索数据
  • ResourceSearch.vue
<template>
  <!-- {/* 搜索框 start */} -->
  <form class="card p-2">
    <div class="input-group">
      <input
        @keyup="$emit('onsearch', $event.target.value)"
        type="text"
        class="form-control"
        placeholder="写点啥..."
      />
    </div>
  </form>
  <!-- {/* 搜索框 Ends */} -->
</template>

<script>
export default {};
</script>

<style></style>
  • index.js
// 搜索功能
export function searchResources(title) {
  return axios.get(`/api/resources/s/${title}`).then((res) => res.data);
}

7. 总结

  • 新版 Vue 3.0 实战项目,做个笔记
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值