npm i -g @vue/cli
vue ui
常见问题1:Windows上运行vue,提示无法加载文件,表示用户权限不足。
解决方案:用管理员身份打开终端,输入set-ExecutionPolicy RemoteSigned,然后输入y
export default对象的属性:
:存放父组件传过来的children。
标签添加scope属性后,不同组件间的css不会相互影响。安装插件后,启动项目
在输入栏找到运行地址后打开网页。
在App.vue中添加需要导入的包
<template>
<NavBar />
<router-view/>
</template>
<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap';
import NavBar from './components/NavBar';
export default {
name: "App",
components: {
NavBar,
}
}
</script>
<style>
</style>
在component中添加NavBar.vue,html部分直接利用bootstrap:
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">MySpace</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">好友列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">用户动态</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">登陆</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">注册</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
name: "NavBar",
}
</script>
<style scoped>
</style>
由于多个页面存在相似的部分,所以外面在component中创建contentBase.vue,这样在其他页面直接调用countentBase组件即可:
<template>
<div class="home">
<div class="container">
<div class="card">
<div class="card-body">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ContentBase",
}
</script>
<style scoped>
.container {
margin-top: 20px;
}
</style>
在homeview.vue中使用contentBase:
<template>
<CountentBase>
首页
</CountentBase>
</template>
<script>
import CountentBase from '@/components/CountentBase.vue'
export default {
name: 'HomeView',
components: {
CountentBase
}
}
</script>
在router/index.js中添加路由:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue';
import UserListView from '../views/UserListView.vue';
import UserProfileView from '../views/UserProfileView.vue';
import LoginView from '../views/LoginView.vue';
import RegisterView from '../views/RegisterView.vue';
import NotFoundView from '../views/NotFoundView.vue';
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/userlist',
name: 'userlist',
component: UserListView
},
{
path: '/userprofile',
name: 'userprofile',
component: UserProfileView
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/register',
name: 'register',
component: RegisterView
},
{
path: '/404',
name: '404',
component: NotFoundView
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
添加路由后已经可以按网址目录实现网页的跳转。
将NavBar中a标签全部换为router-link,在页面的NavBar上实现链接的跳转。
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">Myspace</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'home'}">首页</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'userlist'}">好友列表</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'userprofile'}">用户动态</router-link>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'login'}">登录</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'register'}">注册</router-link>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
name: "NavBar",
}
</script>
<style scoped>
</style>
用户动态主页面UserProfileView.vue:
<template>
<ContentBase>
<div class="row">
<div class="col-3">
<UserProfileInfo @follow123="follow" @unfollow123="unfollow" :user="user" />
<UserProfileWrite @post_a_post123="post_a_post" />
</div>
<div class="col-9">
<UserProfilePosts :posts="posts" />
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase'
import UserProfileInfo from '../components/UserProfileInfo';
import UserProfilePosts from '../components/UserProfilePosts';
import UserProfileWrite from '../components/UserProfileWrite';
import { reactive } from 'vue';
export default {
name: 'UserList',
components: {
ContentBase,
UserProfileInfo,
UserProfilePosts,
UserProfileWrite
},
// 初始化变量、函数
setup() {
const user = reactive({
id: 1,
username: "YanYuchen",
lastName: "Yan",
firstName: "Yuchen",
followerCount: 0,
is_followed: false,
});
const posts = reactive({
count: 3,
posts: [
{
id: 1,
userId: 1,
content: "今天上了web课真开心",
},
{
id: 2,
userId: 1,
content: "今天上了算法课,更开心了",
},
{
id: 3,
userId: 1,
content: "今天上了acwing ,开心极了",
},
]
});
// 关注函数
const follow = () => {
if (user.is_followed) return;
user.is_followed = true;
user.followerCount ++ ;
};
// 取消关注函数
const unfollow = () => {
if (!user.is_followed) return;
user.is_followed = false;
user.followerCount -- ;
};
//发送文本信息
const post_a_post = (content) => {
posts.count ++ ;
posts.posts.unshift({ //最前面添加元素
id: posts.count,
userId: 1,
content: content,
})
};
return {
user,
follow,
unfollow,
posts,
post_a_post
}
}
}
</script>
<style scoped>
</style>
UserProfileInfo.vue:
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-3">
<img class="img-fluid" src="https://cdn.acwing.com/media/user/profile/photo/29150_lg_1f2ac240fe.jpg" alt="">
</div>
<div class="col-9">
<div class="username">{{ fullName }}</div>
<div class="fans">粉丝:{{ user.followerCount }}</div>
<!-- v-if判断,没有关注的话就是关注按钮,已经关注的话就是取消关注按钮 -->
<!-- v-on:click或@click属性:绑定事件 -->
<button @click="follow1234" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">+关注</button>
<button @click="unfollow1234" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">取消关注</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue';
export default {
name: "UserProfileInfo",
props: {
user: {
type: Object,
required: true,
},
},
// computed:动态计算某个数据
// props:存储父组件传递给子组件的数据
setup(props, context) {
// 从UserProfileView中读取user数据
let fullName = computed(() => props.user.lastName + ' ' + props.user.firstName);
// 关注函数
const follow1234 = () => {
context.emit('follow123'); //context.emit():触发父组件绑定的函数
};
// 取消关注函数
const unfollow1234 = () => {
context.emit("unfollow123"); // 触发父组件UserProfileView中unfollow函数
}
return {
fullName,
follow1234,
unfollow1234,
}
}
}
</script>
<style scoped>
img {
border-radius: 50%;
}
.username {
font-weight: bold;
}
.fans {
font-size: 12px;
color: gray;
}
button {
padding: 2px 4px;
font-size: 12px;
}
</style>
UserProfilePosts.vue:
<template>
<div class="card">
<div class="card-body">
<!-- v-for是循环,相当于有多少个post建多少个div -->
<div v-for="post in posts.posts" :key="post.id">
<div class="card single-post">
<div class="card-body">
{{ post.content }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UserProfilePosts",
props: {
posts: {
type: Object,
required: true,
},
}
}
</script>
<style scoped>
.single-post {
margin-bottom: 10px;
}
</style>
UserProfileWrite.vue;
<template>
<div class="card edit-field">
<div class="card-body">
<label for="edit-post" class="form-label">编辑帖子</label>
<textarea v-model="content" class="form-control" id="edit-post" rows="3"></textarea>
<!-- @click绑定函数post_a_post,点击调用函数post_a_post -->
<button @click="post_a_post" type="button" class="btn btn-primary btn-sm">发帖</button>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
name: "UserProfileWrite",
setup(props, context) {
let content = ref('');
const post_a_post = () => {
context.emit('post_a_post123', content.value); // 触发父组件中的post_a_post
content.value = "";
};
return {
content,
post_a_post,
}
}
}
</script>
<style scoped>
.edit-field {
margin-top: 20px;
}
button {
margin-top: 10px;
}
</style>
利用ajax从API中获取用户信息
<template>
<ContentBase>
<div class="card" v-for="user in users" :key="user.id" @click="open_user_profile(user.id)">
<div class="card-body">
<div class="row">
<div class="col-1 img-field">
<img class="img-fluid" :src="user.photo" alt="">
</div>
<div class="col-11">
<div class="username">{{ user.username }}</div>
<div class="follower-count">{{ user.followerCount }}</div>
</div>
</div>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase'
import $ from 'jquery';
import { ref } from 'vue';
import router from '@/router/index';
import { useStore } from 'vuex';
export default {
name: 'UserList',
components: {
ContentBase,
},
setup() {
const store = useStore();
let users = ref([]);
// 获取用户信息
$.ajax({
url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',
type: "get",
success(resp) {
users.value = resp;
}
});
// 打开用户信息
const open_user_profile = userId => {
if (store.state.user.is_login) {
router.push({
name: "userprofile",
params: {
userId
}
})
} else {
router.push({
name: "login"
});
}
}
return {
users,
open_user_profile
};
}
}
</script>
<style scoped>
img {
border-radius: 50%;
}
.username {
font-weight: bold;
height: 50%;
}
.follower-count {
font-size: 12px;
color: gray;
height: 50%;
}
.card {
margin-bottom: 20px;
cursor: pointer;
}
.card:hover {
box-shadow: 2px 2px 10px lightgrey;
transition: 500ms;
}
.img-field {
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
<template>
<ContentBase>
<div class="row justify-content-md-center">
<div class="col-3">
<form @submit.prevent="login">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input v-model="username" type="text" class="form-control" id="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input v-model="password" type="password" class="form-control" id="password">
</div>
<div class="error-message">{{ error_message }}</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase'
import { ref } from 'vue';
import { useStore } from 'vuex';
import router from '@/router/index';
export default {
name: 'LoginView',
components: {
ContentBase,
},
setup() {
const store = useStore();
let username = ref('');
let password = ref('');
let error_message = ref('');
const login = () => {
error_message.value = "";
// 调用store中login函数
store.dispatch("login", {
username: username.value,
password: password.value,
//成功回调函数
success() {
router.push({name: 'userlist'}); //登陆成功则跳转userlist页面
},
//失败回调函数
error() {
error_message.value = "用户名或密码错误";
}
});
};
return {
username,
password,
error_message,
login,
}
}
}
</script>
<style scoped>
button {
width: 100%;
}
.error-message {
color: red;
}
</style>
在store文件夹下:
index.js:
import { createStore } from 'vuex'
import ModuleUser from './user';
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
user: ModuleUser,
}
});
user.js:
import $ from 'jquery';
import jwt_decode from 'jwt-decode';
const ModuleUser = {
state: {
id: "",
username: "",
photo: "",
followerCount: 0,
access: "",
refresh: "",
is_login: false,
},
getters: {
},
mutations: {
updateUser(state, user) {
state.id = user.id;
state.username = user.username;
state.photo = user.photo;
state.followerCount = user.followerCount;
state.access = user.access;
state.refresh = user.refresh;
state.is_login = user.is_login;
},
// 更新令牌
updateAccess(state, access) {
state.access = access;
},
// 退出登陆
logout(state) {
state.id = "";
state.username = "";
state.photo = "";
state.followerCount = 0;
state.access = "";
state.refresh = "";
state.is_login = false;
}
},
actions: {
// 登陆
login(context, data) {
$.ajax({
// 先获取token
url: "https://app165.acapp.acwing.com.cn/api/token/",
type: "POST",
data: {
username: data.username,
password: data.password,
},
// 成功回调函数
success(resp) {
const {access, refresh} = resp;
// jwt解码
const access_obj = jwt_decode(access);
// 每隔4.5分钟刷新一次
setInterval(() => {
$.ajax({
url: "https://app165.acapp.acwing.com.cn/api/token/refresh/",
type: "POST",
data: {
refresh,
},
success(resp) {
context.commit('updateAccess', resp.access);
}
});
}, 4.5 * 60 * 1000);
// 获取用户信息
$.ajax({
url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",
type: "GET",
data: {
user_id: access_obj.user_id,
},
// jwt授权
headers: {
'Authorization': "Bearer " + access,
},
success(resp) {
// 成功登陆则调用updateuser
context.commit("updateUser", {
...resp,
access: access,
refresh: refresh,
is_login: true,
});
data.success();
},
})
},
error() {
data.error();
}
});
},
},
modules: {
}
};
export default ModuleUser;
在vue的ui界面点击build运行,打包完成后的结果在dist文件夹中。
如果打包后打开index出现空白页面,则在vue.config.js文件中,将路径修改为相对路径’./',即添加一行publicPath: ‘./’,
vue.config.js:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
publicPath: './',
transpileDependencies: true
})