大部分文件的源码还有详细注释,完整的的记录在git@gitee.com:also-follow/dark-horse-headline-project.git
src/api/news.js
import request from "@/utils/request";
//封装新闻栏目接口
export function category() {
return request({
url: "/category",
method: "get",
});
}
//封装文章列表
export function post_news(params) {
return request({
url: "/post",
method: "get",
params,
});
}
//封装新闻详情
export function articleDetail(id) {
return request({
url: "/post/" + id,
method: "get",
});
}
//封装点赞文章
export function post_like(id) {
return request({
url: "/post_like/" + id,
method: "get",
});
}
//封装收藏/取消收藏
export function post_star(id) {
return request({
url: "/post_star/" + id,
method: "get",
});
}
//封装收藏/取消收藏
export function post_comment(id) {
return request({
url: "/post_comment/" + id,
method: "get",
});
}
//发表评论封装
export function sendComment(id, data) {
return request({
url: "/post_comment/" + id,
method: "post",
data,
});
}
//取消收藏文章封装
export function postStar(id) {
return request({
url: "/post_star/" + id,
method: "get",
});
}
//搜索文章封装
export function post_search(params) {
return request({
url: "/post_search/",
method: "get",
params,
});
}
src/api/user.js
import request from "@/utils/request";
//封装相关接口
//@表示src路径
//登录
export function login(data) {
//return值返回给login这个函数
return request({
url: "/login", //路径
method: "post", //请求方式
//data:data(左右都相同的时候可以简写只写一个)
data,
});
}
//注册
export function register(data) {
//return值返回给login这个函数
return request({
url: "/register", //路径
method: "post", //请求方式
//data:data(左右都相同的时候可以简写只写一个)
data,
});
}
//获取用户信息
export function userInfo(id) {
// /user/:id :id表示是一个参数而不是路径,正确用法如下
return request({
url: "/user/" + id, //注意:路径是不用加冒号的,冒号只是表示后面的名字是一个参数
method: "get", //请求方式
//统一在拦截器封装了,所以指令注释掉
// headers: {
// //headers表示请求头信息,在我们发送请求的时候,会跟着请求一起发送给后端
// //这里我们利用headers头信息把token传递给后端,用于身份验证
// Authorization: localStorage.getItem("75-token"),
// },
});
}
//文件上传
export function upload(data) {
//return值返回给login这个函数
return request({
url: "/upload", //路径
method: "post", //请求方式
//data:data(左右都相同的时候可以简写只写一个)
data,
});
}
//编辑用户信息
export function user_update(id, data) {
//return值返回给login这个函数
return request({
url: "/user_update/" + id, //路径
method: "post", //请求方式
//data:data(左右都相同的时候可以简写只写一个)
data,
});
}
//关注用户
export function user_follows(id) {
//return值返回给login这个函数
return request({
url: "/user_follows/" + id, //路径
});
}
//取消关注用户
export function user_unfollow(id) {
//return值返回给login这个函数
return request({
url: "/user_unfollow/" + id, //路径
});
}
//关注列表
export function followList() {
return request({
url: "/user_follows",
});
}
//收藏列表
export function starList() {
return request({
url: "/user_star",
});
}
src/components/commentFooter.vue
<template>
<div>
<div class="comment">
<div class="addcomment" v-show="!isFocus">
<input type="text" placeholder="写跟帖" @focus="handlerFocus" />
<span
class="comment"
@click="
$router.push({
path: '/commentList',
query: { id: $route.query.id },
})
"
>
<i class="iconfont iconpinglun-"></i>
<!-- 评论数量 -->
<em>{{ article.comment_length }}</em>
</span>
<!-- 收藏 -->
<i
class="iconfont iconshoucang"
:style="{ color: this.article.has_star ? 'red' : 'black' }"
@click="starFn"
></i>
<i class="iconfont iconfenxiang"></i>
</div>
<div class="inputcomment" v-show="isFocus">
<!-- blur:失去焦点才触发的事件 -->
<textarea
ref="commtext"
rows="5"
v-model="content"
@blur="cancelFn"
></textarea>
<div>
<!-- mousedown:鼠标按下事件
prevent:阻止默认行为
-->
<span @mousedown.prevent="sendFn">发 送</span>
<span @click="cancelFn">取 消</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { articleDetail, post_star, sendComment } from "@/api/news";
export default {
data() {
return {
isFocus: false,
article: {},
content: "",
parent_id: null,
};
},
created() {
//获取新闻详情
let id = this.$route.query.id;
articleDetail(id).then((res) => {
this.article = res.data.data;
});
},
mounted() {},
methods: {
//取消按钮事件
cancelFn() {
this.isFocus = false;
},
//发表评论点击事件
sendFn() {
if (!this.content) {
return this.$toast.fail("评论不能为空");
}
//动态添加parent_id
let query = {
content: this.content,
};
// content: this.content当parent_id有值的时候执行parent_id,没有的话就依旧执行content: this.content
if (this.parent_id) {
query.parent_id = this.parent_id;
}
sendComment(this.article.id, query).then((res) => {
console.log(res);
if (res.data.message == "评论发布成功") {
// 更新浏览器中的评论列表
this.$emit("updateEvent");
//评论条数增加
this.article.comment_length++;
// 清空评论内容
this.content = "";
// 隐藏文本域
this.isFocus = false;
//回复完成后清空id,这样才不会影响下一次的评论
this.parent_id = null;
} else {
this.$toast.fail(res.data.message);
}
});
},
//收藏的点击事件
starFn() {
post_star(this.article.id).then((res) => {
//判断收藏成功或者取消收藏,执行以下代码
if (
res.data.message === "收藏成功" ||
res.data.message === "取消成功"
) {
//收藏或者取消收藏
this.article.has_star = !this.article.has_star;
this.$toast.success(res.data.message);
} else {
this.$toast.fail(res.data.message);
}
});
},
handlerFocus() {
//控制文本域的显示
this.isFocus = true;
// 无法聚焦,因为DOM更新是异步的,可以怎么解决?答:可以使用this.$nextTick
// this.$refs.commtext.focus()
this.$nextTick(() => {
// 文本域自动聚焦 focus
this.$refs.commtext.focus();
});
},
},
};
</script>
<style scoped lang="less">
.comment {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
}
.inputcomment {
padding: 10px;
box-sizing: border-box;
width: 100%;
display: flex;
background-color: #fff;
align-items: flex-end;
textarea {
flex: 3;
background-color: #eee;
border: none;
border-radius: 10px;
padding: 10px;
}
div {
padding: 20px;
}
span {
display: block;
flex: 1;
height: 24px;
line-height: 24px;
padding: 0 10px;
background-color: #f00;
color: #fff;
text-align: center;
border-radius: 6px;
font-size: 13px;
}
}
.addcomment {
width: 100%;
box-sizing: border-box;
padding: 10px;
margin-top: 20px;
display: flex;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
> input {
flex: 4;
height: 30px;
line-height: 30px;
border-radius: 15px;
border: none;
background-color: #eee;
padding-left: 20px;
font-size: 14px;
}
i {
font-size: 20px;
}
> span {
flex: 1;
position: relative;
> em {
position: absolute;
right: 0;
top: -5px;
font-size: 10px;
background-color: #f00;
color: #fff;
border-radius: 5px;
padding: 3px 5px;
}
}
> i {
flex: 1;
}
}
</style>
src/components/commentltem.vue
<template>
<div>
<div class="commentItem">
<div class="top">
<div class="left">
<span>{{ item.user.nickname }}</span>
<span>2分钟前</span>
</div>
<!-- 绑定回复按钮事件,把id传过去 -->
<span @click="replyFn(item.id)">回复</span>
</div>
<!-- 第二层嵌套 start,就算有更多嵌套也不用写了,因为做了判断,当有item.parent的时候就会自己生成嵌套
递归组件,自己调用自己,但是一定要有结束判断
-->
<commentltem
v-if="item.parent"
:item="item.parent"
@replyFnEvent="replyFn"
></commentltem>
<!-- 第二层嵌套 end -->
<div class="bottom">{{ item.content }}</div>
</div>
</div>
</template>
<script>
export default {
//name的意思是相当于通过components注册了组件,组件名就是commentltem,不过它是局部的,只能在当前页面使用
name: "commentltem",
props: {
item: {
type: Object, //规定item的数据类型
required: true, //规定使用组件时,item为必填
},
},
data() {
return {};
},
created() {},
mounted() {},
methods: {
replyFn(id) {
// 子传父, 通知父组件 需要控制底部组件文本域的显示和传ID给底部组件
this.$emit("replyFnEvent", id);
},
},
};
</script>
<style scoped lang="less">
.commentItem {
border: 1px solid #ccc;
padding: 5px;
margin-top: 10px;
.top {
font-size: 12px;
color: #aaa;
display: flex;
justify-content: space-between;
}
.bottom {
font-size: 13px;
line-height: 40px;
}
}
</style>
src/components/newsItem.vue
<template>
<div>
<!-- 1、左右结构 -->
<div class="single" v-if="post.type == 1 && post.cover.length < 3">
<div class="left">
<p class="content">{{ post.title }}</p>
<p class="info">
<span> {{ post.user.nickname }} </span>
<span
>{{
post.comment_length >= 0
? post.comment_length
: post.comments.length
}}跟帖</span
>
</p>
</div>
<img :src="post.cover[0].url" alt />
</div>
<!-- 2、视频 -->
<div class="single2" v-if="post.type == 2">
<p class="content">{{ post.title }}</p>
<div class="playarea">
<img :src="post.cover[0].url" alt />
<div class="playicon">
<van-icon name="play" />
</div>
</div>
<p class="info">
<span>{{ post.user.nickname }}</span>
<span
>{{
post.comment_length >= 0
? post.comment_length
: post.comments.length
}}
跟帖</span
>
</p>
</div>
<!-- 3、上下结构,显示3张图片的 -->
<div class="single3" v-if="post.type == 1 && post.cover.length >= 3">
<p class="content">{{ post.title }}</p>
<div class="imgs">
<img
:src="item.url"
alt
v-for="(item, index) in post.cover"
:key="index"
/>
</div>
<p class="info">
<span> {{ post.user.nickname }} </span>
<span
>{{
post.comment_length >= 0
? post.comment_length
: post.comments.length
}}
跟帖</span
>
</p>
</div>
</div>
</template>
</template>
<script>
export default {
props: {
post: {
type: Object,
required: true,
},
},
};
</script>
<style scoped lang="less">
.info {
font-size: 12px;
padding-left: 5px;
color: #999;
> span:nth-of-type(1) {
padding-right: 15px;
}
}
.content {
font-size: 14px;
padding: 0px 5px;
line-height: 24px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.single {
padding: 15px 0px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #ccc;
flex-wrap: wrap;
.left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
overflow: hidden;
}
img {
width: (120/360) * 100vw;
height: (70/360) * 100vw;
object-fit: cover;
padding-right: 5px;
}
}
.single2 {
border-bottom: 1px solid #ccc;
padding: 8px 0;
.playarea {
width: 100%;
position: relative;
margin-bottom: 10px;
img {
width: 100%;
display: block;
}
.playicon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.4);
box-shadow: 0px 0px 15px #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
.van-icon {
font-size: 40px;
color: #fff;
line-height: 60px;
}
}
}
}
.single3 {
width: 100vw;
.imgs {
width: 100%;
display: flex;
padding: 10px 0 0 0;
img {
width: (120/360) * 100vw;
height: (70/360) * 100vw;
// 让图片自动的调整大小,根据父容器的大小自动调整
object-fit: cover;
padding-right: 5px;
}
}
}
</style>
src/router/index.js
//1.引入两个全局
import VueRouter from "vue-router";
import Vue from "vue";
// import { from } from "core-js/core/array";
//2.注册两个全局组件 router- link和 router-viewu
Vue.use(VueRouter);
//3.创建路由规则
//import 。。。 from "。。。";这种方式引入就是一次性引入多少就一次性加载多少(性能没那么好)
const routes = [
{
path: "/",
//5.重定向,当用户访问localhost:8000的时候强制跳转登录页面
redirect: "/login",
},
{
path: "/login",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/login.vue"),
},
{
path: "/register",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/register.vue"),
},
{
path: "/personal",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/personal.vue"),
},
{
path: "/index",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/index.vue"),
meta: {
// meta我们叫他“路由元信息”,
// 他可以用来保存数据,且路由跳转的时候,这里的数据不会被重置
curIndex: localStorage.getItem("75-token") ? 1 : 0,
},
},
{
path: "/edit_profile",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/edit_profile.vue"),
},
// {
// path: "/index",
// //4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
// component: () => import("../views/index.vue"),
// },
{
path: "/articleDetail",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/articleDetail.vue"),
},
{
path: "/commentList",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/commentList.vue"),
},
{
path: "/myFollow",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/myFollow.vue"),
},
{
path: "/myStar",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/myStar.vue"),
},
{
path: "/category",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/category.vue"),
},
{
path: "/search",
//4.按需引入,也就是当用户访问login页面的时候才加载login.vue组件(性能好一点)
component: () => import("../views/search.vue"),
},
];
//6.根据路由规则生成路由对象
const router = new VueRouter({
//routes:键名是不可以修改的,我们一般取个一样的名字方便简写
routes, //固定写法不能更改(上面的对象跨域更改,但是如果不一样的话要加冒号写上更改的名字,所以我们一般起一样的名字)
});
//重复跳转同一个路由地址导致的报错解决方法
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err);
};
// 导航守卫(路由守卫)
// 导航守卫(路由守卫)是什么?答:就是路由跳转之前会触发的一个函数
// 这个函数就是beforeEach
/**
* 语法:
* router.beforeEach((to, from, next)=>{
* to: 指的跳转的目标路由对象,里面有一个属性to.path就是我们跳转的路径
* from: 指当前的路由对象,里面有一个属性from.path就是当前的路由路径
* next: 表示是否放行的函数
* next() 表示放行
* next(false) 表示不放行
* next(路由路径) 表示跳转指定的路由路径
* })
*
* 使用场景:可以做页面(路由)权限
* 黑马头条项目中:个人中心必须要登录后才能访问,就是可以使用到导航守卫来实现此功能
* 条件一:用户访问的是“个人中心” to.path
* 条件二:用户是否登录 token ---> localstorage
*/
// 登录白名单,在这个名单里面的地址,都要登录后才能访问,不然就会到登录页
let writeArr = ["/personal", "/edit_profile", "/category"];
//路由守卫
router.beforeEach((to, from, next) => {
let token = localStorage.getItem("75-token");
//判断到to.path的值在writeArr数组中的时候,说明跳转的目标路由路径必须登录才能访问(indexOf()数组方法也是字符串方法,匹配是否存在符合条件的值,如果存在则返回首次出现的下标,否则返回-1)
if (writeArr.indexOf(to.path) != -1 && !token) {
next("/login");
} else {
// 当用户访问的页面是登录页面且已经是登录状态了,则让他跳转到首页
if (to.path == "/login" && token) {
next("/index");
} else {
// 因为首页栏目,未登录没有“关注”栏目的,登录后才有“关注”栏目
// 因此登录后栏目的数组长度会发生变化,会导致原来访问的栏目的下标值会加1
// 因此,我们用来控制栏目高亮效果的变量curIndex的值也要加1
// if (localStorage.getItem("75-token") && from.path == "/login") {
// to.meta.curIndex++;
// }
if (to.path == "/login") {
//退出登录后重置首页的curIndex为一
// console.log(128, router);
router.options.routes[4].meta.curIndex = 1;
}
if (to.path == "/index") {
//退出登录后重置首页的curIndex的值
// console.log(128, router);
//设置登录页到首页的下标的初始值
router.options.routes[4].meta.curIndex = token ? 1 : 0;
}
next();
}
}
});
//7.导出路由对象
export default router;
src/style/reset.less
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
src/utils/request.js
//引入axios
import axios from "axios";
//基地址
axios.defaults.baseURL = "http://localhost:3000";
//导出
export default axios;
//引入路由
import router from "@/router/index";
// 添加请求拦截器
axios.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
//统一设置token发送给后端
let token = localStorage.getItem("75-token");
//当token存在的时候,才把token传递给后端
if (token) {
config.headers.Authorization = token;
}
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(
function (response) {
// 对响应数据做点什么
//验证用户是否登录
//未登录后端会返回statusCode属性,当这个属性为401的时候会说用户验证失败,也就是没有登录
//所以判断成立的时候跳到登录页面
if (response.data.statusCode === 401) {
//没有登录跳转登录页面
router.push({
path: "/login",
query: {
//location:获取当前地址
redirectUrl: location.href, //跳转的时候存储一份当前地址
},
});
}
return response;
},
function (error) {
// 对响应错误做点什么
return Promise.reject(error);
}
);
src/views/articleDetail.vue
<template>
<div class="articaldetail">
<div class="header">
<div class="left">
<van-icon name="arrow-left back" @click="$router.back()" />
<span class="iconfont iconnew new"></span>
</div>
<!-- 三元表达式控制显示的关注或者加关注 -->
<!-- class:动态设置按钮样式 -->
<span @click="followFn" :class="{ active: article.has_follow }">{{
this.article.has_follow ? "已关注" : "关注"
}}</span>
</div>
<!-- article数据是通过异步请求回来的,因此数据请求回来之前DOM已经渲染完了
当数据请求回来之后,动态渲染的内容会重新进行渲染
那么问题来了,就是当数据请求回来之前渲染DOM的时候,此刻article是空对象
因为,渲染到昵称的为位置的时候,article.user值是undefined,undefined再来调用nickname就报错了
如何解决?答:可以当数据请求回来之后再渲染DOM即可,如何做呢?答:可以使用v-if做判断,代码如下:v-if="article.id" -->
<div class="detail" v-if="article.id">
<div class="title">{{ article.title }}</div>
<div class="desc">
<span> {{ article.user.nickname }}</span>
<!-- formatData:自己封装的自定义组件 -->
<span v-formatData="article.create_date"></span>
</div>
<div
class="content"
v-if="article.type == 1"
v-html="article.content"
></div>
<!--
controls: 控制器
poster: 设置视频封面图片
-->
<div class="content videoContent" v-else>
<!-- <meta name="referrer" content="no-referrer" />:使页面的视频可以播放 -->
<meta name="referrer" content="no-referrer" />
<video
controls
:src="article.content"
:poster="article.cover[0].url"
></video>
</div>
<div class="opt">
<span class="like" :class="{ active: article.has_like }">
<van-icon name="good-job-o" @click="likeFn" />{{
token ? article.like_length : "点赞"
}}
</span>
<span class="chat"> <van-icon name="chat" class="w" />微信 </span>
</div>
</div>
<!-- 精彩跟帖 -->
<div class="keeps">
<h2>精彩跟帖</h2>
<div class="item">
<div class="head">
<img src="../assets/01.jpg" alt />
<div>
<p>火星网友</p>
<span>2小时前</span>
</div>
<span>回复</span>
</div>
<div class="text">文章说得很有道理</div>
</div>
<div
class="more"
@click="
$router.push({ path: '/commentList', query: { id: $route.query.id } })
"
>
更多跟帖
</div>
</div>
<commentFooter></commentFooter>
</div>
</template>
<script>
import { articleDetail, post_like } from "@/api/news";
import { user_unfollow, user_follows } from "@/api/user";
export default {
data() {
return {
article: [], //接收接口数据的空数组
token: localStorage.getItem("75-token"),
};
},
created() {
//获取到主页传过来的id,将值赋予给接口参数
let id = this.$route.query.id;
articleDetail(id).then((res) => {
console.log(res);
if (id) {
this.article = res.data.data; //定义的数组接收接口数据
}
});
},
mounted() {},
methods: {
//点赞/取消点赞
likeFn() {
post_like(this.article.id).then((res) => {
console.log(res);
if (
res.data.message === "点赞成功" ||
res.data.message === "取消成功"
) {
this.$toast.success(res.data.message);
// 更新浏览器上按钮的效果
this.article.has_like = !this.article.has_like;
// 更新点赞数量
this.article.has_like
? this.article.like_length++
: this.article.like_length--;
} else {
this.$toast.fail(res.data.message);
}
});
},
//关注点击事件
followFn() {
// 判断当前的关注状态,未关注则调用“关注”接口
// 否则调用“取消关注”接口
// 文章详情接口,返回了一个has_follow的字段给我们判断
// 当前用户的关注状态,true表示已关注,false表示未关注
if (this.article.has_follow) {
//取消关注
user_unfollow(this.article.user.id).then((res) => {
console.log(res);
if (res.data.message == "取消关注成功") {
//给用户的提示
this.$toast.success(res.data.message);
// 更新has_follow的值
this.article.has_follow = !this.article.has_follow;
}
});
} else {
//关注
user_follows(this.article.user.id).then((res) => {
console.log(res);
if (res.data.message == "关注成功") {
//给用户的提示
this.$toast.success(res.data.message);
// 更新has_follow的值
this.article.has_follow = !this.article.has_follow;
}
});
}
},
},
};
</script>
<style scoped lang="less">
.articaldetail {
padding-bottom: 52px;
}
.header {
padding: 0px 10px;
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #ccc;
> .left {
height: 100%;
vertical-align: middle;
position: relative;
.van-icon {
position: absolute;
top: 15px;
left: 0;
}
> span {
margin-left: 22px;
font-size: 50px;
}
}
> span {
padding: 5px 15px;
background-color: #f00;
color: #fff;
text-align: center;
border-radius: 15px;
font-size: 13px;
}
> .active {
background: #fff;
color: #000;
border: 1px solid #000;
}
}
.detail {
padding: 15px;
.title {
font-size: 18px;
font-weight: bold;
padding: 10px 0;
}
.desc {
line-height: 30px;
color: #999;
font-size: 13px;
}
/**
vue渲染v-html内容是把它当做组件进行渲染,因此设置里面的标签的样式,同样可以使用
样式穿透设置
*/
/deep/ .content {
text-indent: 2em;
line-height: 24px;
font-size: 15px;
padding-bottom: 30px;
width: 100%;
a {
display: block;
text-indent: 0;
img {
width: 100%;
}
}
}
.videoContent {
text-indent: 0;
video {
width: 100%;
}
}
}
.opt {
display: flex;
justify-content: space-around;
.like,
.chat {
height: 25px;
padding: 0 15px;
font-size: 14px;
line-height: 25px;
text-align: center;
border: 1px solid #ccc;
border-radius: 15px;
}
.active {
color: red;
}
.w {
color: rgb(84, 163, 5);
}
}
.keeps {
border-top: 5px solid #ddd;
padding: 0 15px;
> h2 {
line-height: 50px;
text-align: center;
}
.item {
padding: 10px 0;
border-bottom: 1px solid #ccc;
.head {
display: flex;
justify-content: space-between;
align-items: center;
> img {
width: (50/360) * 100vw;
height: (50/360) * 100vw;
display: block;
border-radius: 50%;
}
> div {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 10px;
> span {
font-size: 12px;
color: #999;
line-height: 25px;
}
}
> span {
color: #999;
font-size: 13px;
}
}
.text {
font-size: 14px;
color: #333;
padding: 20px 0 10px 0;
}
}
.more {
width: 100px;
height: 30px;
line-height: 30px;
text-align: center;
border: 1px solid #ccc;
border-radius: 15px;
margin: 20px auto;
font-size: 13px;
}
}
</style>
src/views/category.vue
<template>
<div>
<!-- 头部 -->
<van-nav-bar title="频道管理" left-arrow @click-left="$router.back()">
</van-nav-bar>
<div class="content">
<h1>点击删除频道</h1>
<van-grid :gutter="10">
<van-grid-item
v-for="value in categoryList"
:key="value.id"
:text="value.name"
@click="delFn(value.id)"
/>
</van-grid>
</div>
<div class="content">
<h1>点击添加频道</h1>
<van-grid :gutter="10">
<van-grid-item
v-for="value in delCategoryList"
:key="value.id"
:text="value.name"
@click="addFn(value.id)"
/>
</van-grid>
</div>
</div>
</template>
<script>
import { category } from "@/api/news";
export default {
data() {
return {
//获取本地储存的数据,没有数据的时候等于一个空数组
categoryList: JSON.parse(localStorage.getItem("categoryList")) || [], //原来的栏目
delCategoryList:
JSON.parse(localStorage.getItem("delCategoryList")) || [], //被删除的栏目
};
},
created() {
// 当缓存中没有数据的时候才需要调用接口,获取栏目的初始数据
if (this.categoryList.length === 0 && this.delCategoryList.length === 0) {
category().then((res) => {
this.categoryList = res.data.data;
// 这个页面不需要渲染“关注”和“头条”,因此需要把他们从数据删除
let arr = this.categoryList.splice(0, 2);
localStorage.setItem("defaultCategory", JSON.stringify(arr));
});
}
},
watch: {
//监听原来的栏目
categoryList: {
deep: true, //因为是复杂数据,所以要深度监听
handler() {
//本地储存两个数组,让他刷新页面的时候不会恢复原样
localStorage.setItem("categoryList", JSON.stringify(this.categoryList));
localStorage.setItem(
"delCategoryList",
JSON.stringify(this.delCategoryList)
);
},
},
},
methods: {
//点击删除事件
delFn(id) {
// 把被删除的添加delCategoryList数组中
let res = this.categoryList.filter((item) => {
return item.id === id;
});
// 把删除的栏目解构添加到下方列表中
this.delCategoryList.push(...res);
// 把不需要删除的过滤出来
this.categoryList = this.categoryList.filter((item) => item.id !== id);
},
//点击添加的事件
addFn(id) {
// splice(下标值, 删除的个数)
// arr.findIndex(function(item){
// item就是数组arr中的元素,
// 这个函数需要返回一个条件判断,当条件成立的时候,会立刻把当前的item对应的下标值返回给findIndex方法。
// })
// 找到id对应的下标值
let index = this.delCategoryList.findIndex((item) => item.id === id);
// 从数组中删除栏目
let res = this.delCategoryList.splice(index, 1);
// 把删除的栏目解构添加到上方列表中
this.categoryList.push(...res);
},
},
};
</script>
<style lang="less" scoped>
/deep/.van-nav-bar {
border-bottom: 1px solid #ddd;
.van-icon {
color: #000;
}
}
.content {
padding: 20px 10px;
h1 {
font-size: 20px;
font-weight: bold;
margin: 10px 0px 20px 0px;
}
/deep/.van-grid {
padding: 0 10px;
.van-grid-item__content {
border: 1px solid #ddd;
.van-grid-item__text {
font-size: 16px;
}
}
}
}
</style>
src/views/commentList.vue
<template>
<div>
<van-nav-bar title="评论列表" left-arrow @click-left="$router.back()" />
<div class="item" v-for="item in comment" :key="item.id">
<div class="head">
<img :src="item.user.head_img | joinpath" alt />
<div>
<p>{{ item.user.nickname }}</p>
<span v-formatData="item.create_date"></span>
</div>
<span @click="replyFn(item.id)">回复</span>
</div>
<!-- 嵌套内容 start-->
<commentltem
v-if="item.parent"
:item="item.parent"
@replyFnEvent="replyFn"
></commentltem>
<!-- 嵌套内容end -->
<!-- 内容 -->
<div class="text">{{ item.content }}</div>
</div>
<!-- 底部评论组件 -->
<commentFooter @updateEvent="updateFn" ref="footer"></commentFooter>
</div>
</template>
<script>
import { post_comment } from "@/api/news";
export default {
data() {
return {
comment: [],
};
},
created() {
// post_comment(this.$route.query.id).then((res) => {
// this.comment = res.data.data;
// });
///调用获取评论列表的函数(因为子传父的事件只有一个调用接口,所以简化代码直接调用它即可)
this.updateFn();
},
mounted() {},
methods: {
//回复的点击事件
replyFn(id) {
this.$refs.footer.handlerFocus();
this.$refs.footer.parent_id = id;
},
//子传父的事件
updateFn() {
//获取评论列表数据
post_comment(this.$route.query.id).then((res) => {
this.comment = res.data.data;
});
},
},
};
</script>
<style scoped lang="less">
/deep/.van-nav-bar {
border-bottom: 1px solid #ddd;
.van-icon {
color: #000;
}
}
.item {
padding: 10px;
border-bottom: 1px solid #ccc;
.head {
display: flex;
justify-content: space-between;
align-items: center;
> img {
width: (50/360) * 100vw;
height: (50/360) * 100vw;
display: block;
border-radius: 50%;
}
> div {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 10px;
> span {
font-size: 12px;
color: #999;
line-height: 25px;
}
}
> span {
color: #999;
font-size: 13px;
}
}
.text {
font-size: 14px;
color: #333;
padding: 20px 0 10px 0;
}
}
</style>
src/views/edit_profile.vue
<template>
<div>
<!--
头部:
--左边点击箭头
$router.back() 返回上一个路由地址
-->
<van-nav-bar
title="个人信息编辑"
left-arrow
@click-left="$router.back()"
@click-right="clickRightFn"
>
<!-- #就是v-slot的简写 -->
<template #right>
<!-- <van-icon name="search" size="18" /> -->
<van-icon name="wap-home-o" size="25" />
</template>
</van-nav-bar>
<div class="content">
<div class="img">
<img :src="user.head_img | joinpath" alt="我是一张图片" />
<!-- 文件上传组件 -->
<van-uploader :after-read="afterRead" />
</div>
<van-cell
title="昵称"
is-link
:value="user.nickname"
@click="showDialogFn"
/>
<!-- 修改昵称弹出款 -->
<van-dialog
@confirm="changeNickname"
:before-close="NicknameBeforeClose"
v-model="showNickName"
title="编辑昵称"
show-cancel-button
>
<van-field
v-model.trim="Nickname"
required
placeholder="请输入昵称"
label="昵称"
/>
</van-dialog>
<van-cell
title="密码"
is-link
value="*****"
@click="showPasswordDialogFn"
/>
<!-- 修改密码弹出款 -->
<van-dialog
title="编辑密码"
show-cancel-button
v-model="showpassword"
@confirm="PwChangeFn"
:before-close="PwBeforeClose"
>
<van-field
required
placeholder="请输入原密码"
label="原密码"
v-model="oldPw"
/>
<van-field
required
placeholder="请输入新密码"
label="新密码"
v-model="newPw"
/>
</van-dialog>
<van-cell
title="性别"
is-link
:value="user.gender == 1 ? '男' : '女'"
@click="showGenderFn"
/>
<van-action-sheet
v-model="genderShow"
:actions="actions"
@select="onSelect"
/>
</div>
</div>
</template>
<script>
import { userInfo, upload, user_update } from "@/api/user";
export default {
data() {
return {
user: {},
id: null,
showNickName: false, //昵称弹窗默认隐藏
Nickname: "",
showpassword: false, //密码弹窗默认隐藏
newPw: "", //新密码
oldPw: "", //原密码
genderShow: false,
actions: [
{ name: "男", gender: 1 },
{ name: "女", gender: 0 },
],
};
},
created() {
this.id = localStorage.getItem("75-userId");
userInfo(this.id).then((res) => {
this.user = res.data.data;
});
},
mounted() {},
methods: {
//昵称弹窗关闭前触发的函数
//组件加上了before-close ,那弹窗必须执行done(函数才会关闭
//昵称
NicknameBeforeClose(action, done) {
//当昵称为空而且用户点击了确认按钮
if (!this.Nickname && action == "confirm") {
done(false);
} else {
done();
}
},
//修改昵称
changeNickname() {
if (!this.Nickname) {
//判断输入不能为空
return this.$toast.fail("昵称不能为空");
}
user_update(this.id, {
//发送请求修改昵称
nickname: this.Nickname,
}).then((res) => {
console.log(res);
if (res.data.message == "修改成功") {
//更新浏览器上面的昵称
this.user.nickname = this.Nickname;
} else {
//失败提示
this.$toast.fail(res.data.message);
}
});
},
//点击事件
showDialogFn() {
//控制点击昵称的时候显示弹出修改窗口
this.showNickName = true;
this.Nickname = this.user.nickname;
},
//修改头像
afterRead(file) {
// 此时可以自行将文件上传至服务器
console.log(file);
//上传文件必须使用formatData数据格式
let formatData = new FormData();
//formatData("键名","键值");
formatData.append("file", file.file);
upload(formatData).then((res) => {
//判断文件是否上传成功,不成功则打断函数往下执行
if (res.data.message === "文件上传成功") {
return this.$toast.fail(
res.data.message + "文件只支持rgp,jpg,png格式"
);
}
let url = res.data.data.url;
//调用修改头像接口
user_update(this.id, {
head_img: url,
}).then((res) => {
if (res.data.message == "修改成功") {
//判断修改成功,更新头像
this.user.head_img = url;
} else {
this.$toast.fail(res.data.message);
}
});
});
},
//修改密码
showPasswordDialogFn() {
// 点击修改密码的时候弹出修改窗口
this.showpassword = true;
},
//点击事件
PwChangeFn() {
//判断旧密码输入框的密码是否等同于数据库的密码
if (this.oldPw != this.user.password) {
return this.$toast.fail("原密码错误");
}
//判断旧密码是否和新密码一样
if (this.oldPw == this.newPw) {
return this.$toast.fail("旧密码不能和新密码相同");
}
//判断新密码是否为空
if (!this.newPw) {
return this.$toast.fail("新密码不能为空");
}
user_update(this.id, {
password: this.newPw,
}).then((res) => {
console.log(res);
if (res.data.message == "修改成功") {
this.user.password = this.newPw;
this.$toast.success(res.data.message);
(this.newPw = ""), (this.oldPw = "");
} else {
this.$toast.fail(res.data.message);
}
});
},
//组件库自带的函数方法
PwBeforeClose(action, done) {
//当昵称为空而且用户点击了确认按钮
if (action == "confirm") {
if (
this.oldPw != this.user.password ||
this.oldPw == this.newPw ||
!this.newPw
) {
done(false);
} else {
done();
}
} else {
done();
}
},
//性别
showGenderFn() {
this.genderShow = true;
},
//点击性别触发的函数
onSelect(actions, index) {
//actions:当前用户点击的选项对应的对象
//index:当前用户点击选项的下标值(这次没用到,只是记录有这个可以用)
user_update(this.id, {
//修改性别数据
gender: actions.gender,
}).then((res) => {
console.log(res);
if (res.data.message === "修改成功") {
//修改成功刷新页面
this.user.gender = actions.gender;
//关闭页面
this.genderShow = false;
} else {
this.$toast.fail(res.data.message);
}
});
},
//点击头部右侧图标跳转
clickRightFn() {
this.$router.push("/index");
},
},
};
</script>
<style scoped lang="less">
.van-nav-bar {
border-bottom: 1px solid #ddd;
/deep/.van-icon {
color: #000;
}
}
.content {
.img {
width: 80px;
height: 100px;
margin: 50px auto;
position: relative;
img {
width: 100%;
}
.van-uploader {
position: absolute;
left: 0;
bottom: 0;
opacity: 0;
}
}
.van-cell {
border-bottom: 1px solid #ddd;
margin: 10px 0;
}
.van-dialog {
input {
width: 250px;
}
}
}
</style>
src/views/index.vue
<template>
<div>
<!-- 头部 -->
<van-nav-bar @click-right="clickRightFn">
<template #left>
<span class="iconfont iconnew"></span>
</template>
<template #title>
<div class="search-box" @click="$router.push('/search')">
<van-icon name="search" />
<span>搜索新闻</span>
</div>
</template>
<template #right>
<van-icon name="manager-o" size="22" />
</template>
</van-nav-bar>
<!-- 导航- 栏目列表 -->
<!-- v-model="curIndex-设置栏目选中效果
change: 点击栏目时触发的事件-->
<van-tabs
v-model="curIndex"
@change="tabChangeFn"
sticky
@click.native="toCategory"
>
<!-- 新闻列表 -->
<van-tab v-for="item in categoryList" :title="item.name" :key="item.id">
<!-- 文章列表 -->
<!--van-pull-refresh:下来刷新
refresh:表示加载完成,false表示加载完成关闭提醒
refresh:表示下拉刷新的时候触发,我们就可以包裹要刷新的内容即可
-->
<van-pull-refresh v-model="isLoading" @refresh="onRefresh">
<!-- van-list:上拉刷新
v-model: 控制数据是否加载完成,false表示加载完成 true加载中
finished:表示是否全部数据加载完成,true表示加载完成,false表示未加载完
load: 上拉加载时触发的事件,那么我就可以在这个事件处理函数中实现加载更多数据
immediate-check: false表示关闭 加载页面的时候触发load事件(但是不能绝对关闭,某些情况下还是会触发)
-->
<van-list
v-model="loading"
:finished="finished"
finished-text="我是有底线的。。。"
@load="onLoad"
:immediate-check="false"
>
<!-- 新闻列表
默认情况下自定义组件不能绑定原生事件
如何解决呢?答:vue提供了一个修饰符给我们使用,使得我们可以在
自定义组件上直接绑定原生的事件,这个修饰符就是.native
-->
<newsItem
:post="item"
v-for="item in newsList"
:key="item.id"
@click.native="toDetail(item.id)"
></newsItem>
</van-list>
</van-pull-refresh>
<!-- <van-icon name="plus" @click="$router.push('/category')" /> -->
</van-tab>
</van-tabs>
</div>
</template>
<script>
import newsItem from "@/components/newsItem.vue";
import { category, post_news } from "@/api/news";
export default {
data() {
return {
//定义一个空数组接收接口返回的新闻栏目
categoryList: [],
//动态的下标值
curIndex: this.$route.meta.curIndex,
//定义一个空数组接收接口返回的文章数据
newsList: [],
isLoading: false, //下拉刷新功能
pageIndex: 1, //当前默认的页数 第一页
pageSize: 8, //当前默认的数据条数
loading: false, //控制数据是否加载完成,false表示加载完成 true加载中
finished: false, //表示是否全部数据加载完成,true表示加载完成,false表示未加载完
};
},
components: {
newsItem,
},
created() {
// 判断栏目列表 是否有缓存数据,如果有则使用缓存的数据,
// 如果没有,则使用从接口中获取到的数据
let categorys = JSON.parse(localStorage.getItem("categoryList")) || [];
let defaultCategory =
JSON.parse(localStorage.getItem("defaultCategory")) || [];
if (defaultCategory.length > 0) {
this.categoryList = [...defaultCategory, ...categorys];
// 调用获取 文章列表的接口,因为文章列表需要根据栏目
// id来获取,又因为只有在这里栏目id才存在
this.getNews();
} else {
//新闻接口发送数据
category().then((res) => {
//接收数据返回的数据
this.categoryList = res.data.data;
//调用封装的文章接口
this.getNews();
});
}
},
methods: {
//跳转频道管理事件
toCategory(e) {
console.log(e.target.className);
if (e.target.className.indexOf("van-tabs__wrap") != -1) {
this.$router.push("/category");
}
},
//新闻点击事件,点击跳转新闻详情页面
toDetail(id) {
this.$router.push({
path: "/articleDetail", //跳转到详情页
query: { id }, //path用query传参,如果市name就用params传参
});
},
// 点击栏目时触发的函数
tabChangeFn(name, title) {
// name: 指的是栏目对应的下标值
// title: 指的是栏目对应的标题
// 因为tas标签中已经用v-model双向绑定了curIndex,因此name的值会自动赋值给curIndex
// this.curIndex = name;
this.$route.meta.curIndex = this.curIndex;
this.onRefresh(); //因为点击切换也要刷新清空数组,所以直接调用下拉刷新的函数
},
//上拉时候触发的函数
onLoad() {
console.log("上拉加载");
// 当第一页的数据还没有请求回来的时候,阻止继续请求下一页的数据
if (this.newsList.length === 0) {
return;
}
// this.$nextTick(()=>{})
this.pageIndex++, //上拉加载的时候页数自加
this.getNews(); //调用接口
},
//下拉刷新
onRefresh() {
console.log("下拉刷新");
// 重置相关的变量
this.pageIndex = 1; // 因为上拉加载的会改变pageIndex的值,所以为了重新获取第一页的数据,需要重置
this.newsList = []; // 因为数组是累加在一起,因此导致报错,但其实我们刷新数据是不需要累加数据的,因此要重置
// 因为上拉加载有可能会导致finished设置会true,因为为了能够实现刷新页面后还能够继续实现上拉加载功能,因此需要重置
this.finished = false;
//调用数据
this.getNews();
},
//封装文章接口方便复用
getNews() {
//拿新闻列表数组的第二个下标就是头条,让用户一进来就是显示头条的数据
post_news({
category: this.categoryList[this.curIndex].id,
//加上页数和数据条数参数
pageIndex: this.pageIndex,
pageSize: this.pageSize,
}).then((res) => {
console.log(res);
// this.newsList = res.data.data;
// 下拉加载更多数据,需要把原来的数据和下一页的数据合并在一起
this.newsList = [...this.newsList, ...res.data.data];
//表示下拉刷新加载完成,关闭刷新
this.isLoading = false;
// 上拉加载完成后 需要把loading设置为false,表示加载完成
this.loading = false;
// 当所有数据都加载完成后,需要把finished设置为true,表示加载完成
// 如何判断所有数据已经加载完了呢?注:后端会返回total这个字段给我们,这个字段表示文章总数量。
// 答:方法一:当this.newsList数组长度等于res.data.total的时候,表示数据已经加载完成了。
// 方法二:当接口返回给我们的数据量小于this.pageSize的值的时候,说明数据库中已经没有更多数据
if (res.data.data.length < this.pageSize) {
this.finished = true; //finished设置为true,表示加载完成
}
});
},
//点击头部右侧图标跳转
clickRightFn() {
this.$router.push("/personal");
},
},
};
</script>
<style lang="less" scoped>
/deep/.van-nav-bar {
background: red;
position: relative;
.iconnew {
font-size: 50px;
color: #fff;
}
.van-nav-bar__title {
width: 60%;
.search-box {
width: 100%;
height: 36px;
line-height: 36px;
text-align: center;
border-radius: 20px;
background: rgba(255, 255, 255, 0.3);
span {
color: #fff;
}
}
}
.van-icon {
color: #fff;
}
}
/deep/.van-tabs {
.van-tabs__wrap {
position: relative;
// padding-right: 40px;
&::after {
content: "+";
position: absolute;
width: 40px;
height: 44px;
text-align: center;
top: 0;
right: 0;
font-size: 30px;
font-weight: bold;
line-height: 40px;
// margin-right: 10px;
z-index: 1;
background-color: #fff;
}
}
}
</style>
src/views/login.vue
<template>
<div id="app">
<div class="container">
<div class="close"><span class="iconfont iconicon-test"></span></div>
<div class="logo"><span class="iconfont iconnew"></span></div>
<div class="inputs">
<!-- <input placeholder="请输入手机号" class="input" /><input
placeholder="密码"
class="input"
type="password"
/> -->
<!-- label:左侧提示
error:字体颜色标红
required:必填提示-->
<van-form ref="form">
<van-field
v-model="user.username"
required
placeholder="请输入手机号"
:rules="[{ required: true, message: '用户名不能为空' }]"
/>
<van-field
v-model="user.password"
type="password"
required
placeholder="请输入密码"
:rules="[{ required: true, message: '密码不能为空' }]"
/>
</van-form>
</div>
<p class="tips">
没有账号?
<a href="#/register" class="">去注册</a>
</p>
<!-- <div class="button">登录按钮</div> -->
<van-button @click="loginFn" type="danger" icon="bill" block round
>红包</van-button
>
</div>
</div>
</template>
<script>
//引入封装好的axios
import { login } from "@/api/user";
export default {
data() {
return {
//如果接口需要多个参数的时候,我们一般将他放在一个对象当中
user: {
username: "",
password: "",
},
};
},
created() {},
mounted() {},
methods: {
//登录按钮
loginFn() {
// vant组件库内置 校验,校验的结果可以通过validate方法获取到,
// 当验证通过则触发then函数,不通过则触发catch函数
// validate方法通过form表单调用的
this.$refs.form
.validate()
.then(() => {
console.log("验证成功");
login(this.user).then((res) => {
console.log(res);
if (res.data.message === "登录成功") {
//保存token和id
localStorage.setItem("75-token", res.data.data.token);
localStorage.setItem("75-userId", res.data.data.user.id);
// 跳转到首页或者其他页面
let query = this.$route.query;
let url = query.redirectUrl && query.redirectUrl.split("#")[1];
if (url) {
this.$router.push(url);
} else {
//跳转页面
this.$router.push({
path: "/index",
});
}
} else {
//失败提示vant组件库的功能
this.$toast.fail(res.data.message);
}
});
})
.catch(() => {
console.log("验证失败");
});
//发送数据请求
},
},
};
</script>
<style scoped lang="less">
//vant库的组件名就是对应的类名
// 样式穿透语法/deep/ 深层穿透,这样我们可以直接设置组件内部的样式
// /deep/ 样式穿透是less预处理器提供的语法 (针对局部样式也就是style要加上scoped,如果设置全局就不需要穿透)
/deep/.van-field {
border-bottom: 2px solid green;
margin-bottom: 15px;
input {
color: green;
}
}
.container {
padding: 20px;
}
.close {
span {
font-size: (27 / 360) * 100vw;
}
}
.logo {
display: flex;
justify-content: center;
span {
display: block;
font-size: (126 / 360) * 100vw;
color: #d81e06;
}
}
.inputs {
input {
margin-bottom: 20px;
}
}
.tips {
text-align: right;
margin-bottom: 20px;
a {
color: #3385ff;
}
}
</style>
src/views/myFollow.vue
<template>
<div>
<!-- 头部 -->
<van-nav-bar title="我的关注" left-arrow @click-left="$router.back()">
</van-nav-bar>
<div class="list" v-for="item in follow" :key="item.id">
<div class="box">
<img :src="item.head_img | joinpath" alt="" />
<div class="center">
<p>{{ item.nickname }}</p>
<span v-formatData="item.create_date"></span>
</div>
<span @click="delFn(item.id)"> 取消关注 </span>
</div>
</div>
</div>
</template>
<script>
import { followList, user_unfollow } from "@/api/user";
export default {
data() {
return {
follow: [], //定义空数组接收关注列表接口数据
};
},
created() {
this.getFollows();
},
mounted() {},
methods: {
//封装关注列表接口
getFollows() {
followList().then((res) => {
//把接口数据赋予数组
console.log(res);
this.follow = res.data.data;
});
},
//关注/取消关注
delFn(id) {
user_unfollow(id).then((res) => {
if (res.data.message === "取消关注成功") {
this.getFollows();
this.$toast.success(res.data.message);
} else {
this.$toast.fail(res.data.message);
}
});
},
},
};
</script>
<style scoped lang="less">
.van-nav-bar {
border-bottom: 1px solid #eee;
}
.list {
.box {
display: flex;
padding: 25px 10px;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #ccc;
> img {
display: block;
width: (50/360) * 100vw;
height: (50/360) * 100vw;
border-radius: 50%;
padding: 0 10px;
}
> .center {
flex: 1;
> p {
line-height: 30px;
font-size: 14px;
}
> span {
font-size: 12px;
color: #999;
}
}
> span {
height: 30px;
background-color: #eee;
font-size: 13px;
border-radius: 30px;
line-height: 30px;
padding: 0px 15px;
}
}
}
</style>
src/views/myStar.vue
<template>
<div>
<!-- 头部 -->
<van-nav-bar title="我的收藏" left-arrow @click-left="$router.back()">
</van-nav-bar>
<van-swipe-cell v-for="item in star" :key="item.id">
<newsItem :post="item" @click.native="toDetail(item.id)"></newsItem>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="delFn(item.id)"
/>
</template>
</van-swipe-cell>
</div>
</template>
<script>
import { starList } from "@/api/user";
import { postStar } from "@/api/news";
import newsItem from "@/components/newsItem.vue";
export default {
components: {
newsItem,
},
data() {
return {
star: [], //接收收藏列表数据的数组
};
},
created() {
this.getStarList();
},
mounted() {},
methods: {
//封装数据接口
getStarList() {
starList().then((res) => {
console.log(res);
this.star = res.data.data;
});
},
//点击跳转文章详情
toDetail(id) {
this.$router.push({
path: "/articleDetail",
query: { id },
});
},
//删除文章事件
delFn(id) {
postStar(id).then((res) => {
console.log(res);
if (res.data.message == "取消成功") {
this.getStarList();
this.$toast.success(res.data.message);
} else {
this.$toast.fail(res.data.message);
}
});
},
},
};
</script>
<style scoped lang="less">
.van-nav-bar {
border-bottom: 1px solid #eee;
}
.van-button {
height: 100%;
}
</style>
src/views/personal.vue
<template>
<div>
<van-nav-bar
title="个人中心"
left-arrow
@click-left="$router.back()"
@click-right="$router.push('/index')"
>
<!-- #就是v-slot的简写 -->
<template #right>
<!-- <van-icon name="search" size="18" /> -->
<van-icon name="wap-home-o" size="25" />
</template>
</van-nav-bar>
<div class="personal">
<router-link to="/edit_profile">
<div class="profile">
<!-- 全局过滤器拼接图片路径
接口请求数据是异步的,通常情况下异步数据请求回来之前DOM已经渲染完成了
当数据请求回来之后,再重新渲染一次。-->
<img :src="user.head_img | joinpath" alt />
<div class="profile-center">
<div class="name">
<span
class="iconfont"
:class="{
iconxingbienan: user.gender == 1,
iconxingbienv: user.gender == 0,
}"
></span
>{{ user.nickname }}
</div>
<!-- 自定义指令转换日期格式 -->
<div class="time" v-formatData="user.create_date">
<!-- {{ user.create_date }} -->
</div>
</div>
<span class="iconfont iconjiantou1"></span>
</div>
</router-link>
<div>
<van-cell
title="我的关注"
is-link
value="关注的用户"
@click="$router.push('/myFollow')"
/>
<van-cell title="我的跟帖" is-link value="跟帖/回复" />
<van-cell
title="我的收藏"
is-link
value="文章/视频"
@click="$router.push('/myStar')"
/>
<van-cell title="设置" is-link />
</div>
<van-button color="#eb6112" block round @click="logauFn">退出</van-button>
</div>
<!-- 内容 -->
</div>
</template>
<script>
import { userInfo } from "@/api/user";
export default {
data() {
return {
user: {},
};
},
created() {
let id = localStorage.getItem("75-userId");
userInfo(id).then((res) => {
console.log(res);
this.user = res.data.data;
});
},
mounted() {},
methods: {
logauFn() {
//清除本地储存的token和id
if (confirm("确定退出么")) {
// localStorage.removeItem("75-token");
// localStorage.removeItem("75-userId");
//退出清除所有本地储存
localStorage.clear();
//当我们跳转页面不需要传参的时候,可以简写,直接下面代码直接跳转
this.$router.push("/login");
}
},
},
};
</script>
<style scoped lang="less">
.van-nav-bar {
background-color: #fff;
}
.van-cell {
background-color: #eee;
border-bottom: 1px solid #ccc;
margin-top: 10px;
}
.van-button {
width: 90%;
margin: 15px auto;
}
.personal {
width: 100vw;
height: 100vh;
background-color: #eee;
}
a {
color: #666;
}
.profile {
display: flex;
padding: 20px 10px;
justify-content: space-between;
align-items: center;
border-bottom: 5px #ddd solid;
img {
width: (70 / 360) * 100vw;
height: (70 / 360) * 100vw;
border-radius: 50%;
}
.profile-center {
flex: 1;
padding: 0 10px;
}
.name {
span {
color: #75b9eb;
}
}
.time {
color: #666;
font-size: 14px;
margin-top: 5px;
}
}
</style>
src/views/register.vue
<template>
<div id="app">
<div class="container">
<div class="close"><span class="iconfont iconicon-test"></span></div>
<div class="logo"><span class="iconfont iconnew"></span></div>
<div class="inputs">
<!-- <input placeholder="请输入手机号" class="input" /><input
placeholder="密码"
class="input"
type="password"
/> -->
<!-- label:左侧提示
error:字体颜色标红
required:必填提示-->
<van-form ref="form">
<van-field
required
v-model="user.username"
placeholder="请输入手机号"
:rules="[
{
required: true,
pattern: /^1[3,4,5,6,7,8,9]\d{9}$/,
message: '请输入正确的手机号',
},
]"
/>
<van-field
required
v-model="user.nickname"
placeholder="请输入昵称"
:rules="[{ required: true, message: '用户名不能为空' }]"
/>
<van-field
type="password"
required
v-model="user.password"
placeholder="请输入密码"
:rules="[{ required: true, message: '密码不能为空' }]"
/>
</van-form>
</div>
<p class="tips">
没有账号?
<a href="#/login" class="">去登录</a>
</p>
<!-- <div class="button">登录按钮</div> -->
<van-button type="primary" block round @click="registerFn"
>注册</van-button
>
</div>
</div>
</template>
<script>
//引入封装好的axios
import { register } from "@/api/user";
export default {
data() {
return {
user: {
username: "13234565678",
password: "",
nickname: "",
},
};
},
created() {},
mounted() {},
methods: {
registerFn() {
//添加校验。校验通过实现注册功能
this.$refs.form
.validate()
.then(() => {
console.log("校验成功");
register(this.user).then((res) => {
//注册成功跳转到登录页面
if (res.data.message === "注册成功") {
this.$router.push({
path: "/login",
});
} else {
//注册失败提示
this.$toast.fail(res.data.message);
}
});
}) //失败捕捉
.catch(() => {
console.log("校验失败");
});
},
},
};
</script>
<style scoped lang="less">
//vant库的组件名就是对应的类名
// 样式穿透语法/deep/ 深层穿透,这样我们可以直接设置组件内部的样式
// /deep/ 样式穿透是less预处理器提供的语法 (针对局部样式也就是style要加上scoped,如果设置全局就不需要穿透)
/deep/.van-field {
border-bottom: 2px solid green;
margin-bottom: 15px;
input {
color: green;
}
}
.container {
padding: 20px;
}
.close {
span {
font-size: (27 / 360) * 100vw;
}
}
.logo {
display: flex;
justify-content: center;
span {
display: block;
font-size: (126 / 360) * 100vw;
color: #d81e06;
}
}
.inputs {
input {
margin-bottom: 20px;
}
}
.tips {
text-align: right;
margin-bottom: 20px;
a {
color: #3385ff;
}
}
</style>
src/views/search.vue
<template>
<div>
<van-nav-bar left-arrow @click-left="$router.back()">
<template #title>
<van-search
shape="round"
placeholder="请输入搜索关键词"
v-model="keyword"
/>
</template>
<template #right>
<span @click="searchFn">搜索</span>
</template>
</van-nav-bar>
<!-- 内容 -->
<div class="content">
<h1>历史记录</h1>
<ul class="content-list">
<li
v-for="(item, index) in record"
:key="index"
@click="historyFn(item)"
>
{{ item }}
</li>
<!-- <li>002</li>
<li>003</li> -->
</ul>
</div>
<div class="content">
<h1>搜索结果</h1>
<ul class="content-list search-box">
<li
v-for="item in searchResult"
:key="item.id"
v-html="
item.title.replace(
keyword,
`<span style=color:red;>${keyword}</span>`
)
"
@click="
$router.push({ path: '/articleDetail', query: { id: item.id } })
"
></li>
<!-- <li>002</li>
<li>003</li> -->
</ul>
</div>
</div>
</template>
<script>
import { post_search } from "@/api/news";
export default {
data() {
return {
keyword: "", //搜索框的值
searchResult: [], //接收符合搜索条件的数据
record: JSON.parse(localStorage.getItem("75-record")) || [], //获取本地储存历史记录
};
},
methods: {
//搜索点击事件
searchFn() {
if (!this.keyword) {
return this.$toast.fail("搜索不能为空");
}
post_search({ keyword: this.keyword }).then((res) => {
this.searchResult = res.data.data;
//利用indexof方法查找数组中是否存在相同的内容,如果不存在则加进去(也可以用数组去重)
if (this.record.indexOf(this.keyword) == -1) {
this.record.push(this.keyword.trim()); //输入款的内容添加进去历史记录
// this.record = [...new Set(this.record)]; //数组去重
localStorage.setItem("75-record", JSON.stringify(this.record)); //本地储存历史纪律
}
});
},
//历史记录的点击事件
historyFn(item) {
this.keyword = item;
this.searchFn();
},
},
};
</script>
<style lang="less" scoped>
/deep/.van-nav-bar {
border-bottom: 1px solid #ddd;
.van-icon {
color: #000;
}
.van-search {
padding: 0px 12px;
}
}
.content {
padding: 20px 10px;
border-bottom: 1px solid #ddd;
h1 {
font-size: 20px;
font-weight: bold;
margin: 10px 0;
}
.content-list {
li {
font-size: 16px;
}
}
.search-box {
li {
text-decoration: underline;
margin: 10px 0;
}
}
}
</style>
src/views/App.vue
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {};
},
created() {},
mounted() {},
methods: {},
};
</script>
<style>
/* 引入全局的字体图标 */
@import "http://at.alicdn.com/t/font_1426139_h6vn3jbl5q.css";
</style>
src/main.js
import Vue from "vue";
import App from "./App.vue";
//引入重置样式
//为什么要引入重置样式呢?因为不同浏览器,对不同的标签默认样式的效果式不一样的
//所有要统一不同的浏览器展示一样的效果,所有要重置统一样式
import "./style/reset.less";
//全局注册vant
import {
Icon,
NavBar,
Button,
Field,
Toast,
Form,
Cell,
CellGroup,
Uploader,
Dialog,
ActionSheet,
Tab,
Tabs,
PullRefresh,
List,
SwipeCell,
Grid,
GridItem,
Search,
} from "vant";
Vue.use(Search);
Vue.use(Grid);
Vue.use(GridItem);
Vue.use(List);
Vue.use(PullRefresh);
Vue.use(Tab);
Vue.use(Tabs);
Vue.use(ActionSheet);
Vue.use(Dialog);
Vue.use(Uploader);
Vue.use(Icon);
Vue.use(NavBar);
Vue.use(Cell);
Vue.use(CellGroup);
Vue.use(Form);
Vue.use(Toast);
Vue.use(Field);
Vue.use(Button);
Vue.use(SwipeCell);
Vue.config.productionTip = false;
//8.引入封装的路由对象
import router from "./router/index";
//全局引入自己封装的组件
import commentFooter from "@/components/commentFooter";
Vue.component("commentFooter", commentFooter);
import commentltem from "@/components/commentltem";
Vue.component("commentltem", commentltem);
//封装全局过滤器,用于拼接图片地址
//过滤器只能用于插值表达式和v-bind中
import axios from "@/utils/request";
Vue.filter("joinpath", (val) => {
if (!val) {
return val;
}
//判断val不存在的时候不处理val
if (val && val.indexOf("http") != -1) {
return val;
}
//接口基地址拼接图片路径 形成完整图片
return axios.defaults.baseURL + val;
});
// 全局指令,格式化日期
// Vue.directive("自定义指令名",
//{ inserted(el,binding){
// el: 指的是使用该指令的标签
// binding.value: 指的是使用指令时传入的值
// inserted:当标签插入到页面中的时候触发的函数,以后都不会触发
// },
//update(el, binding){
// update: 当传入的数据变化的时候触发(也就是binding.value的值变化的时候触发)
// } })
Vue.directive("formatData", {
inserted(el, binding) {
let time = null;
// 当传入的日期不存在则 返回当前日期
if (!binding.value) {
time = new Date().toLocaleDateString();
}
time = new Date(binding.value).toLocaleDateString();
//设置标签内容
el.innerText = time;
},
update(el, binding) {
let time = null;
// 当传入的日期不存在则 返回当前日期
if (!binding.value) {
time = new Date().toLocaleDateString();
}
time = new Date(binding.value).toLocaleDateString();
//设置标签内容
el.innerText = time;
},
});
new Vue({
router, //9.路由对象注入vue实例
render: (h) => h(App),
}).$mount("#app");