Vue 3.0 CompostionAPI + VueRouter + Axios + Bootstrap + Sass 文章管理项目
1. Vue 3.0 介绍和脚手架文档
2. Vue 3.0 快速入门指南
2.1 Vue 3.0 搭建脚手架项目
2.2 Vue 3.0 文件结构梳理
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5b39a2d81ccca1041b2470cec441d5b1.png#pic_center)
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'
export default {
setup(){
const counter = ref(1000);
const increment = (num) => {
counter.value += num;
};
return { counter, increment };
}
}
</script>
<style>
</style>
3. Vue 3.0 组件
3.1 Vue 3.0 组件拆分
<template>
<h1>1000</h1>
</template>
<script>
export default {};
</script>
<style>
</style>
<template>
<div class="my-app">
<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";
export default {
components: {
CounterView,
},
setup(){
const counter = ref(1000);
const increment = (num) => {
counter.value += num;
};
return { counter, increment };
}
}
</script>
<style>
</style>
3.2 Vue 3.0 属性传值
<template>
<h1>{{counter}}</h1>
</template>
<script>
export default {
props: {
counter: {
type: Number,
required: true,
default: 500,
}
}
};
</script>
<style>
</style>
<template>
<div class="my-app">
<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";
export default {
components: {
CounterView,
},
setup(){
const counter = ref(1000);
const increment = (num) => {
counter.value += num;
};
return { counter, increment };
}
}
</script>
<style>
</style>
3.3 Vue 3.0 事件注册
<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) => {
context.emit("onIncrement", num)
};
return { increment };
}
}
</script>
<style>
</style>
<template>
<div class="my-app">
<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";
export default {
components: {
CounterView,
},
setup(){
const counter = ref(1000);
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),
}
}
<template>
<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>
</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 实现视图切换
<!-- {} -->
<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';
...
const isDetailView = ref(true)
return {
isDetailView
}
4. Vue 3.0 数据的交互
4.1 Vue 3.0 添加数据
<!-- 添加按钮 -->
<button @click="addResource" class="btn btn-sm btn-primary">
添加数据
</button>
...
const addResource = () => {
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 调整样式
<!-- {} -->
<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 -->
...
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>
<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>
</template>
<script>
import { computed } from 'vue'
export default {
props: {
resources: {
type: Array,
default: () => [],
},
},
setup(props, context) {
const onItemClick = (resource) => {
context.emit("handleItemClick",resource)
}
return { onItemClick }
}
}
</script>
<style>
</style>
<template>
<ResourceDetail :resource="selectedResource" v-else></ResourceDetail>
</template>
<script>
...
setup(){
const selectedResource = ref(null);
const activeResource = computed(() => {
return selectedResource.value ||
(getResourcesLength > 0 && data.resources[0]) ||
null
})
const selectResource = (resource) => {
selectedResource.value = resource
}
}
};
</script>
<style>
</style>
<template>
<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>
</template>
<script>
export default {
props: {
resource: {
validator: (prop) => typeof prop === "object" || prop === null,
required: true,
},
},
};
</script>
<style>
</style>
4.4 Vue 3.0 选中状态
选中列表数据改变其样式
ResourceList.vue
<template>
<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>
</template>
<script>
import { computed } from 'vue'
export default {
props: {
resources: {
type: Array,
default: () => [],
},
activeId: String,
},
setup(props, context) {
const onItemClick = (resource) => {
context.emit("handleItemClick",resource)
}
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
ResourceHome.vue
onMounted(async () => {
const resources = await fetchResources()
});
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 数据赋值
import axios from 'axios'
export function fetchResources(){
return axios.get("/api/resources").then((res) => res.data)
}
onMounted(async () => {
const resources = await fetchResources()
data.resources = resources
});
4.8 Vue 3.0 调整更新组件
<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>
<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 调整类型选项
<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 数据更新到服务器端
export function updateResource(id, resource){
return axios
.patch(`/api/resources/${id}`, resource)
.then((res) => res.data)
}
<button
@click="handleUpdate"
class="btn btn-primary btn-lg btn-block"
type="button">提交
</button>
...
import { updateResource } from '@/actions'
...
const handleUpdate = async () => {
const updatedResource = await updateResource(uResource.value._id, uResource.value)
}
context.emit("onUpdateResource", updateResource)
<ResourceUpdate
@onUpdateResource="handleUpdateResource($event)"
:resource="activeResource"
v-if="isDetailView" />
...
const handleUpdateResource = (newResource) => {
const index = data.resources.findIndex(
(resource) => resource._id === newResource._id
)
data.resources[index] = newResource
selectResource(newResource)
}
5.3 Vue 3.0 弹窗提醒
<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 {
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 设置定时器取消提醒
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 配置路由
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,
});
export default router;
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 子级路由
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: "/",
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,
});
export default router;
6.3 Vue 3.0 导航
<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>
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: "/",
name: "base",
redirect: { name: "resourceHomePage"}
},
{
path: "/resources",
name: "resourceHomePage",
component: ResourceHome
},
{
path: "/resources/new",
name: "resourceNewPage",
component: ResourceNew
}
];
const router = createRouter({
history: createWebHistory(),
routes,
linkExactActiveClass: "active",
});
export default router;
6.4 Vue 3.0 重构更新组件
<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>
<template>
<ResourceForm
@onFormSubmit="handleUpdate($event)"
:resource="resource"
:alert="alert"
/>
</template>
6.5 Vue 3.0 添加组件
<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) => {
await createResource(resource.value);
setAlert("success", "Resource was created");
};
return {
...toRefs(data),
handleCreate,
};
},
};
</script>
<style lang="scss" scoped></style>
6.6 Vue 3.0 详情跳转
<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 () => {
const { id } = route.params;
data.resource = await fetchResource(id);
});
return {
...toRefs(data),
};
},
};
</script>
<style scoped>
</style>
6.7 Vue 3.0 显示详情页面
<template>
<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>
</template>
<script>
import { computed } from 'vue'
export default {
props: {
resources: {
type: Array,
default: () => [],
},
activeId: String,
},
setup(props, context) {
const onItemClick = (resource) => {
context.emit("handleItemClick",resource)
}
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 插槽
<slot name="buttonLink"></slot>
<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 搜索数据
<template>
<form class="card p-2">
<div class="input-group">
<input
@keyup="$emit('onsearch', $event.target.value)"
type="text"
class="form-control"
placeholder="写点啥..."
/>
</div>
</form>
</template>
<script>
export default {};
</script>
<style></style>
export function searchResources(title) {
return axios.get(`/api/resources/s/${title}`).then((res) => res.data);
}
7. 总结