l1035118279

l1035118279 查看完整档案

呼和浩特编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

l1035118279 提出了问题 · 9月8日

windows 环境下 shell脚本如何获取时间?

现在公司让做备份,备份的服务器是windows server 2012 的。现在文件也获取了,就是名字需要改成 ‘数据库-2020-9-8-14-21-33.tar.gz’ 但是windows下怎么也获取不到时间,我在git 的bash 上执行date可以获取时间
image.png
但是在脚本里获取时间,应该是报错了。因为不会执行下面的代码。
image.png
红框内的文件是备份文件。
image.png
执行了以后,文件名称也没有变化。
有没有大佬知道怎么在windows的shell脚本里获取时间???~~

关注 3 回答 2

l1035118279 赞了文章 · 8月21日

MySQL定时备份方案

虽说现在这世道有些爱情是有价的,但是数据是无价的,数据备份是尤为的重要,可以在你未来的某一天不小心删库了,不用着急跑路。

image

本片文章介绍的方案是利用Linux自身的crontab定时任务功能,定时执行备份数据库的脚本。

技术要点:

  • 数据库备份dump命令
  • shell脚本
  • Linux定时任务crontab

数据备份dump

数据库都有一个导出数据库内数据和结构的命令,就是备份。
将备份的数据还原会将原来的数据中的表删了重建,再插入备份中的数据,这是恢复。
这一点需要注意,如果恢复之前的数据比备份的多,恢复后多的数据就没有了。

列出我常用的两种数据库的备份和恢复命令

postgresql:

备份pg_dump -h [ip] -U [用户名] [库名] >[导出的.sql 文件]
恢复psql -s [库名] -f [导出.sql 文件]

mysql:

备份mysqldump -h -u [用户名] -p [库名] > [导出的.sql 文件]
恢复mysql -u [用户名] -p [库名] < [导出的.sql 文件]

shell脚本

要完成一个功能完善的备份方案,就需要shell脚本。
我们要让这个脚本备份到指定路径,并压缩存放,最多30个,超过30个删除最早的,并记录操作日志。
啥也不说了,话都在脚本里,干了!

#用户名
username=root
#密码
password=nicai
#将要备份的数据库
database_name=l_love_you

#保存备份文件最多个数
count=30
#备份保存路径
backup_path=/app/mysql_backup
#日期
date_time=`date +%Y-%m-%d-%H-%M`

#如果文件夹不存在则创建
if [ ! -d $backup_path ]; 
then     
    mkdir -p $backup_path; 
fi
#开始备份
mysqldump -u $username -p$password $database_name > $backup_path/$database_name-$date_time.sql
#开始压缩
cd $backup_path
tar -zcvf $database_name-$date_time.tar.gz $database_name-$date_time.sql
#删除源文件
rm -rf $backup_path/$database_name-$date_time.sql
#更新备份日志
echo "create $backup_path/$database_name-$date_time.tar.gz" >> $backup_path/dump.log

#找出需要删除的备份
delfile=`ls -l -crt  $backup_path/*.tar.gz | awk '{print $9 }' | head -1`

#判断现在的备份数量是否大于阈值
number=`ls -l -crt  $backup_path/*.tar.gz | awk '{print $9 }' | wc -l`

if [ $number -gt $count ]
then
  #删除最早生成的备份,只保留count数量的备份
  rm $delfile
  #更新删除文件日志
  echo "delete $delfile" >> $backup_path/dump.log
fi

给脚本起个顾名思义的漂亮名字 dump_mysql.sh
给脚本赋予可执行权限 chmod +x dump_mysql.sh, 执行后脚本变绿了就是可实行文件
执行方法:./加脚本名称

chmod命令参数含义--
+ 代表添加某些权限
x 代表可执行权限

定时任务crontab

crontab是Linux自带的一个定时任务功能,我们可以利用它每天凌晨执行一次dump_mysql.sh脚本。

crontab用法:
  • crontab -l 查看定时任务列表
  • crontab -e 编辑(新增/删除)定时任务

运行crontab -e命令,打开一个可编辑的文本,输入00 01 * * * /app/dump_mysql.sh
保本并退出即添加完成。

内容解释:

00 01 * * * /app/dump_mysql.sh 分两部分看,
第一部分00 01 * * * 是定时任务的周期,第二部分/app/dump_mysql.sh到时间做的事情。
周期表达式是五个占位符,分别代表:分钟、小时、日、月、星期

占位符用*表示,用在第一位就是每分钟,第二位每小时,依此类推
占位符用具体数字表示具体时间,10用在第一位就是10分,用在第三位表示10号,依此类推
占位符用-表示区间,5-7用在第一位就是5分到7分,用在第五位表示周5到周日,依此类推
占位符用/表示间隔,5-10/2用在第一位就是5分到10分间隔2分钟,用在第二位表示5点到10点间隔2小时,依此类推
占位符用,表示列表,5,10用在第一位就是5分和10分,用在第四位表示5月和10月,依此类推


2020年7月20日更新

脚本中密码安全性问题

多谢思友hack的提醒,脚本里面直接放数据库密码是不安全的。
我们脚本执行后msyql也会打印一行警告:
mysqldump: [Warning] Using a password on the command line interface can be insecure
译:在命令行界面上使用密码可能不安全

关于这个问题网上的处理方法也比较一致,mysqldump命令去掉-u用户名-p密码参数,使用--defaults-file参数读取配置的用户名密码。

也是官网给出的方案:
image.png

在命令行上指定密码应该被认为是不安全的。为了避免在命令行上给出密码,请使用选项文件。参见 第6.1.2.1节: 用户密码安全指南。

image.png

编辑my.cnf

编辑数据库家目录的my.cnf,我的路径是/etc/my.cnf。在[client]下面添加密码,如果没有[client],就编写一个,不然下面的password 会无效。

[client]
password="nicai"

默认使用当前登录Linux服务器的账号作为连接数据库的账号,我刚好都是root ,用户名就可以不配,如果不一致,需要加一条

user=xxx

编辑完了最好给my.cnf 设置600的权限,600代表只有拥有者有读写权限。

shell> chmod 600 my.cnf

如果密码中含有特殊字符,一定要用双引号引起来,不然运行mysqldump命令会报错
mysqldump: Got error: 1045: Access denied for user 'root'@'localhost' (using password: YES) when trying to connect

修改参数

运行mysqldump命令时,使用--defaults-file=/etc/my.cnf参数代替-uroot -ppassword的参数即可。
脚本中去掉用户名和密码,mysqldump命令按照下面修改:

mysqldump --defaults-file=/etc/my.cnf $database_name >$backup_path/$database_name-$date_time.sql

上面的方法虽然是官方提供的,但还是把明文写在文件里,只不过是加了读写权限而已,还是没太有说服力,可以考虑下面的方法
使用SHC对脚本加密:

Linux下脚本加密工具SHC对shell脚本进行加密,SHC可以将shell脚本转换为一个可执行的二进制文件,这也是一个办法。

对备份的文件也需要加密:

tar配合openssl完成加密压缩:
tar -czvf - 源文件 | openssl des3 -salt -k 密码 -out 压加密文件.tar.gz
解密:
openssl des3 -d -k 密码 -salt -in 加密文件.tar.gz | tar xvf -

查看原文

赞 4 收藏 2 评论 2

l1035118279 收藏了文章 · 8月18日

2020 前端面试总结

l1035118279 赞了文章 · 8月18日

2020 前端面试总结

赞 47 收藏 33 评论 0

l1035118279 收藏了文章 · 5月19日

我发现了7个关于 CSS backgroundImage 好用的技巧

作者:ryanmcdermott
译者:前端小智
来源:github

点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

背景图像可能是我们所有前端开发人员在我们的职业生涯中至少使用过几次的CSS属性之一。大多数人认为背景图像不可能有任何不寻常的地方,但经过研究,答案并非如此。所以本文收集了七个我认为最有用的技巧,并创建了一些代码示例。

1.背景图如何才能完美适配视口

让背景图适配视口很容易,需要使用下面 CSS 即可:

body {
  background-image: url('https://images.unsplash.com/photo-1573480813647-552e9b7b5394?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2253&q=80');
  background-repeat: no-repeat;
  background-position: center;
  background-attachment: fixed;
  background-size: cover;
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
}

clipboard.png

事例源码:https://codepen.io/duomly/pen...

2.如何在CSS中使用多个背景图片?

如果我想在背景中添加一张以上的图片怎么办?CSS3 中可以直接 指定多个背景路径,如下所示:

body {
  background-image: url(https://image.flaticon.com/icons/svg/748/748122.svg), url(https://images.unsplash.com/photo-1478719059408-592965723cbc?ixlib=rb-1.2.1&auto=format&fit=crop&w=2212&q=80);
  background-position: center, top;
  background-repeat: repeat, no-repeat;
  background-size: contain, cover;
}

clipboard.png

事例源码:https://codepen.io/duomly/pen...

3.如何创建一个三角形的背景图像

另一个很酷的背景特效就是三角形背景,当我们想展示某些完全不同的选择(例如白天和黑夜或冬天和夏天)时,这种特效就更加棒。

思路是这样的,首先创建两个div,然后将两个背景都添加到其中,然后,第二个div使用clip-path属性画出三角形。

clipboard.png

html

<body>
  <div class="day"></div>
  <div class="night"></div>
</body>

css

body {
  margin: 0;
  padding: 0;
}

div {
  position: absolute;
  height: 100vh;
  width: 100vw;
}

.day {
  background-image: url("https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2613&q=80");
  background-size: cover;
  background-repeat: no-repeat;
}

.night {
  background-image: url("https://images.unsplash.com/photo-1493540447904-49763eecf55f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  background-size: cover;
  background-repeat: no-repeat;
  clip-path: polygon(100vw 0, 0% 0vh, 100vw 100vh);
}

源码:https://codepen.io/duomly/pen...

4.如何在背景图像上添加叠加渐变?

有时我们想在背景上添加一些文字,但有的图片太亮,导致字看不清楚,所以这里我们就需要让背景图叠加一些暗乐来突出文字效果。

例如,可以通过添加粉红橙色渐变或红色至透明渐变来增强日落图像,这些情况下使用叠加的渐变就很容易做到。

clipboard.png

css

body {
  background-image: 
    linear-gradient(4deg, rgba(38,8,31,0.75) 30%, rgba(213,49,127,0.3) 45%, rgba(232,120,12,0.3) 100%),
    url("https://images.unsplash.com/photo-1503803548695-c2a7b4a5b875?ixlib=rb-1.2.1&auto=format&fit=crop&w=2250&q=80");
  background-size: cover;
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: center
}

源码:https://codepen.io/duomly/pen...

5.如何创建一个颜色动态变化的背景

如果你很多颜色,你想确认哪种颜色更适合背景图片的颜色,刚动态更改背景颜色的技巧就很有用。

css

HTML CSSResult
EDIT ON
@keyframes background-overlay-animation {
  0%   {
      background-image: 
        linear-gradient(4deg, rgba(255,78,36,0.3) 50%, rgba(255,78,36,0.3) 100%), url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
  25%  {
      background-image: 
         linear-gradient(4deg, rgba(213,49,127,0.3) 50%, rgba(213,49,127,0.3) 100%), url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
  50%  {
    background-image: 
       linear-gradient(4deg, rgba(36,182,255,0.3) 50%, rgba(36,182,255,1) 100%),
     url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
  100% {
    background-image: 
        linear-gradient(4deg, rgba(0,255,254,0.3) 50%, rgba(0,255,254,0.3) 100%),
        url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
}

@-webkit-keyframes background-overlay-animation {
  0%   {
      background-image: 
        linear-gradient(4deg, rgba(255,78,36,0.3) 50%, rgba(255,78,36,0.3) 100%)
        url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
  25%  {
      background-image: 
         linear-gradient(4deg, rgba(213,49,127,0.3) 50%, rgba(213,49,127,0.3) 100%),
        url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
  50%  {
    background-image: 
       linear-gradient(4deg, rgba(36,182,255,0.3) 50%, rgba(36,182,255,1) 100%),
     url("https://images.unsplash.com/photo-1559310589-2673bfe16970?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80");
  }
  100% {
    background-image: 
        linear-gradient(4deg, rgba(0,255,254,0.3) 50%, rgba(0,255,254,0.3) 100%),
  

图片描述

源码:https://codepen.io/duomly/pen...

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

6. 如何制作网格背景图像?

有时候会遇到一些需要有艺术或者摄影类的项目,他们一般要求网站要有艺术信息,要有创意。网络的背景就挺有创意的,效果如下:

clipboard.png

HTML

<body>
<div class="container">
  <div class="item_img"></div>
  <div class="item"></div>
  <div class="item_img"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item_img"></div>
  <div class="item"></div>
  <div class="item_img"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item_img"></div>
  <div class="item"></div>
  <div class="item_img"></div>
  <div class="item"></div>
  <div class="item_img"></div>
  <div class="item"></div>
</div>
</body>

scss

body {
 margin: 0;
  padding: 0;
}

.container {
  position: absolute;
  width: 100%;
  height: 100%;
  background: black;
  display: grid;
  grid-template-columns: 25fr 30fr 40fr 15fr;
  grid-template-rows: 20fr 45fr 5fr 30fr;
  grid-gap: 20px;
  .item_img {
    background-image: url('https://images.unsplash.com/photo-1499856871958-5b9627545d1a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2207&q=80');
  background-repeat: no-repeat;
  background-position: center;
  background-attachment: fixed;
  background-size: cover;
}
}

源码:https://codepen.io/duomly/pen...

7.如何将背景图像设置为文本颜色?

使用background-image background-clip ,可以实现背景图像对文字的优美效果。 在某些情况下,它可能非常有用,尤其是当我们想创建一个较大的文本标题而又不如普通颜色那么枯燥的情况。

图片描述

HTML

<body>
  <h1>Hello world!</h1>
</body>

SCSS

body {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  width: 100%;
  text-align: center;
  min-height: 100vh;
  font-size: 120px;
  font-family:Arial, Helvetica, sans-serif;
}

h1 {
   background-image: url("https://images.unsplash.com/photo-1462275646964-a0e3386b89fa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2600&q=80");
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

源码:https://codepen.io/duomly/pen...


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/duomly/discove...


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

l1035118279 收藏了文章 · 5月14日

历害了!教你自己搭建一个私人网盘..

本文教大家用docker搭建一款自己的私有网盘,教程给大家分享一下。
作者:zhaoolee
https://www.jianshu.com/p/54f...

开源云盘选择

搭建前我仔细看了一下各个开源私有云盘的实现,有以下几种:

  • owncloud 
  • sealife 
  • nextcloud 

对这几家比较了以下,考虑了以下因素: 

  • 开源且免费,可以自定义插件开发 
  • 全客户端的支持,免费更好,ui 视觉还能过得去 
  • 支持外挂磁盘,可以随时更改,不需要分块、加密和过多的文件控制、权限控制等等,简单就好 
  • 部署难度,vm 还行,最好可以 Docker 

最终我选择了 nextcloud,至于更多的详细差异,大家可以根据需求选择。

安装docker

# 通过yum源安装docker
sudo yum -y install docker

# 启动docker
sudo systemctl start docker

# 开机自启
sudo systemctl enable docker

配置Docker

docker run -d -p 8080:80 nextcloud

访问主机ip的8080端口,为网盘设置管理员名称和密码。

比如,我的主机ip为149.28.54.241, 那么我访问的就是149.28.54.241:8080。

进入网盘后, 可以获取全平台的客户端

至此网盘已经搭建完成!

自由存取文件

可以通过客户端上传图片, 也可以直接拖拽上传

把文件分享给好友

实现同步盘的功能(用webdrive连接)

点击右下角设置

获取webdav地址

mac直接挂载

连接

认证

挂载成功

其实挂载同步盘后, 你可能发现自己的服务器流量还是太小, 传输大文件,如果断掉就会需要重传, 我这里推荐一个工具

transmit支持webdav协议, 可以让我们在网络状况很差的情况下,也能进行稳定同步。

把手机也连上:

输入管理员账户和密码

可以看到刚刚在浏览器上传的图片

手机上传图片

pc可以查看手机上传的图片


探索插件(可以跳过)。

点击页面右上角齿轮图标, 可以安装插件

这个网盘能记笔记

其余的大多数应用我都试了一遍, 除了记事本, 其余的基本上是从入门到放弃

日历打六分


小结

随着科技的发展, 人们的隐私信息会被互联网巨头们进行交易, 这时候建立自己的私有云盘就变得比较重要了。

开始接触到nextcloud的时候, 感觉挺惊艳的, 不仅开源免费, 而且是平台覆盖,支持文件分享,支持webdav数据同步, 而且还支持各种扩展(虽然有些不太好用), 后来又找到了docker镜像的部署方式, 发现原来搭建自己的私有云盘可以如此简单, 爱折腾的小伙伴可以按照我的步骤尝试一下~~~

如有错误或其它问题,欢迎小伙伴留言评论、指正。如有帮助,欢迎点赞+转发分享。

欢迎大家关注民工哥的公众号:民工哥技术之路
image.png

查看原文

l1035118279 赞了文章 · 5月14日

历害了!教你自己搭建一个私人网盘..

本文教大家用docker搭建一款自己的私有网盘,教程给大家分享一下。
作者:zhaoolee
https://www.jianshu.com/p/54f...

开源云盘选择

搭建前我仔细看了一下各个开源私有云盘的实现,有以下几种:

  • owncloud 
  • sealife 
  • nextcloud 

对这几家比较了以下,考虑了以下因素: 

  • 开源且免费,可以自定义插件开发 
  • 全客户端的支持,免费更好,ui 视觉还能过得去 
  • 支持外挂磁盘,可以随时更改,不需要分块、加密和过多的文件控制、权限控制等等,简单就好 
  • 部署难度,vm 还行,最好可以 Docker 

最终我选择了 nextcloud,至于更多的详细差异,大家可以根据需求选择。

安装docker

# 通过yum源安装docker
sudo yum -y install docker

# 启动docker
sudo systemctl start docker

# 开机自启
sudo systemctl enable docker

配置Docker

docker run -d -p 8080:80 nextcloud

访问主机ip的8080端口,为网盘设置管理员名称和密码。

比如,我的主机ip为149.28.54.241, 那么我访问的就是149.28.54.241:8080。

进入网盘后, 可以获取全平台的客户端

至此网盘已经搭建完成!

自由存取文件

可以通过客户端上传图片, 也可以直接拖拽上传

把文件分享给好友

实现同步盘的功能(用webdrive连接)

点击右下角设置

获取webdav地址

mac直接挂载

连接

认证

挂载成功

其实挂载同步盘后, 你可能发现自己的服务器流量还是太小, 传输大文件,如果断掉就会需要重传, 我这里推荐一个工具

transmit支持webdav协议, 可以让我们在网络状况很差的情况下,也能进行稳定同步。

把手机也连上:

输入管理员账户和密码

可以看到刚刚在浏览器上传的图片

手机上传图片

pc可以查看手机上传的图片


探索插件(可以跳过)。

点击页面右上角齿轮图标, 可以安装插件

这个网盘能记笔记

其余的大多数应用我都试了一遍, 除了记事本, 其余的基本上是从入门到放弃

日历打六分


小结

随着科技的发展, 人们的隐私信息会被互联网巨头们进行交易, 这时候建立自己的私有云盘就变得比较重要了。

开始接触到nextcloud的时候, 感觉挺惊艳的, 不仅开源免费, 而且是平台覆盖,支持文件分享,支持webdav数据同步, 而且还支持各种扩展(虽然有些不太好用), 后来又找到了docker镜像的部署方式, 发现原来搭建自己的私有云盘可以如此简单, 爱折腾的小伙伴可以按照我的步骤尝试一下~~~

如有错误或其它问题,欢迎小伙伴留言评论、指正。如有帮助,欢迎点赞+转发分享。

欢迎大家关注民工哥的公众号:民工哥技术之路
image.png

查看原文

赞 17 收藏 13 评论 0

l1035118279 收藏了文章 · 4月29日

你可能还不知的 7 个 CSS 好用的属性

作者:Mustapha Aouas
译者:前端小智
来源:dev
点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

学习CSS是构建好看网页的一种方式。 但是,在学习过程中,我们倾向于(大部分时间)限制自己,一遍又一遍地使用相同的属性。 毕竟,我们是一种习惯性的动物,我们会使用自己习惯且熟悉的东西。

因此,在这篇文章中,向你介绍7个 比较少见且好用的 CSS 属性,希望对你有所帮助。

1. vertical-align

CSS 的属性 vertical-align 用来指定行内元素(inline)或表格单元格(table-cell)元素的垂直对齐方式。

就像定义说的,这个属性允许你垂直对齐文本。它对于顺序指示器(st, nd等)、需要的输入星号(*)或没有正确居中的图标特别有用。vertical-align取其中一个值:super | top | middle | bottom | baseline (default) | sub | text-top | text-bottom,或从基线开始的长度(px%em, rem等等)。

baseline: 使元素的基线与父元素的基线对齐。HTML规范没有详细说明部分可替换元素的基线,如<textarea> ,这意味着这些元素使用此值的表现因浏览器而异。

sub:使元素的基线与父元素的下标基线对齐。

super:使元素的基线与父元素的上标基线对齐。

text-top:使元素的基线与父元素的上标基线对齐。

text-bottom:使元素的底部与父元素的字体底部对齐。

middle:使元素的中部与父元素的基线加上父元素x-height(译注:x高度)的一半对齐。

图片描述

注意 vertical-align 只对行内元素、表格单元格元素生效:不能用它垂直对齐块级元素。

资源:MDN

2. writing-mode

writing-mode 属性定义了文本水平或垂直排布以及在块级元素中文本的行进方向。为整个文档设置书时,应在根元素上设置它(对于 HTML 文档应该在 html 元素上设置)。 它采用以下值之一horizontal-tb (default) | vertical-rl | vertical-lr

clipboard.png

horizontal-tb:对于左对齐(ltr)脚本,内容从左到右水平流动。对于右对齐(rtr)脚本,内容从右到左水平流动。下一水平行位于上一行下方。

vertical-rl:对于左对齐(ltr)脚本,内容从上到下垂直流动,下一垂直行位于上一行左侧。对于右对齐(rtr)脚本,内容从下到上垂直流动,下一垂直行位于上一行右侧。

vertical-lr:对于左对齐(ltr)脚本,内容从上到下垂直流动,下一垂直行位于上一行右侧。对于右对齐(rtr)脚本,内容从下到上垂直流动,下一垂直行位于上一行左侧。

资源:MDN

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

3. font-variant-numeric

font-variant-numeric CSS属性控制数字,分数和序号标记的替代字形的使用。

它采用以下这些值之一: normal | ordinal | slashed-zero | lining-nums | oldstyle-nums | proportional-nums | tabular-nums | diagonal-fractions | stacked-fractions

此属性对于设置数字样式很有用。 根据情况,你可能希望显示老式的数字或带有斜杠的零,对于这些情况,font-feature-settings很有用。

图片描述

请注意,font-variant-numericfont-feature-settings组属性的一部分。 诸如font-variant-capsfont-variant-ligatures之类的属性也属于该组。
还要注意,像所有font-feature-settings属性一样,你的字体需要实现上述功能才能正常工作。 我使用的字体是Fira Sans

资源:MDN

4. user-select

每当我们有不想让用户选择的文本,或者相反,如果发生了双击或上下文单击,希望选择所有文本时,user-select属性将非常有用。

此属性采用以下值之一:none | auto | text | all

none:元素及其子元素的文本不可选中。 请注意这个Selection 对象可以包含这些元素。 从Firefox 21开始, none 表现的像 -moz-none,因此可以使用 -moz-user-select: text 在子元素上重新启用选择。

auto
auto 的具体取值取决于一系列条件,具体如下:

  • ::before::after 伪元素上,采用的属性值是 none
  • 如果元素是可编辑元素,则采用的属性值是 contain
  • 否则,如果此元素的父元素的 user-select 采用的属性值为 all,则该元素采用的属性值也为 all
  • 否则,如果此元素的父元素的 user-select 采用的属性值为 none,则该元素采用的属性值也为 none
  • 否则,采用的属性值为 text

text:用户可以选择文本。
all:在一个HTML编辑器中,当双击子元素或者上下文时,那么包含该子元素的最顶层元素也会被选中。

图片描述

资源:MDN

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

5. clip-path

clip-path CSS 属性可以创建一个只有元素的部分区域可以显示的剪切区域。区域内的部分显示,区域外的隐藏。剪切区域是被引用内嵌的URL定义的路径或者外部svg的路径,或者作为一个形状例如circle()clip-path属性代替了现在已经弃用的剪切 clip属性。

此属性采用以下值之一:circle() | ellipse() | polygon() | path() | url()

由于这是对该属性的介绍,因此,这里不会深入研究每个值。

我使用最多的两个值是circlepolygoncircle(radius at pair)值有两个参数,第一个参数是圆的半径,第二个参数是表示圆心的点。polygon(pair, pair, pair ...)值取3个或更多的点,表示一个三角形、一个矩形等等。

图片描述

6. shape-outside

shape-outside的CSS 属性定义了一个可以是非矩形的形状,相邻的内联内容应围绕该形状进行包装。 默认情况下,内联内容包围其边距框; shape-outside提供了一种自定义此包装的方法,可以将文本包装在复杂对象周围而不是简单的框中。它采用与clip-path相同的值。

clip-path定义用户如何查看元素,shape-outside定义其他HTML元素如何查看元素。

clipboard.png

资源:MDN

7. background-clip

最后,backgroundclip CSS属性设置元素的背景是否扩展到其borderpaddingcontent 框之下。

此属性采用以下值之一:border-box (default) | padding-box | content-box | text

图片描述

资源:MDN

总结

下图是结合上面 7 个属性实现的布局,让大家加深一下印象。

clipboard.png

如果你还知道一些新奇的属性,欢迎留言。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/mustapha/7-ama...


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

l1035118279 收藏了文章 · 4月28日

基于vue2,eggjs,mysql的个人博客(正在更新)

简单做个博客。
功能点:注册、登录、cookie、权限控制、文章列表、文章详情、文章目录、点赞、评论、分页加载
整体架构:
image.png

前端

使用vue-cli3创建项目

npm install -g @vue/cli
vue create hello-world

为了避开烦人的 eslint,选择了手动选择特性:
image.png
同时没有选择 Linter/Formatter,使用 vscode 中的插件 prettier 和 vetur 配合格式化代码。
image.png

使用axios封装http请求方法

参考了vue中Axios的封装和API接口的管理,对网络请求进行了发封装。
image.png
网络请求统一放在/src/request中,config.js中是基本配置信息:
image.png
http.js中封装请求拦截、响应拦截、错误统一处理。
api.js中是网站所有接口,将其导出后,挂载到 vue.prototype.$api 上,这样全局可以使用 this.$api.xxx 使用接口:
image.png
然后在main.js引入并挂载:
image.png

cookie/session

博客采用cookie/session的方式来记录会话状态,在app.vuecreated生命周期函数中通过checkLogin()接口查询用户登录状态,具体的逻辑为:
当当网流程图3.png

首页分页加载

  let documentEle = document.documentElement
  let needLoadMore =
    documentEle.scrollTop + documentEle.clientHeight + 50 > documentEle.scrollHeight;
  if (needLoadMore && !this.nomore) {
    this.loadingMore = true;
    //暂时不能再滚动加载数据
    this.nomore = true;
    this.loadMoreArticle();
  }

document.documentElement.scrollTop:文档滚动的距离
document.documentElement.clientHeight:文档在可视范围内的高度
document.documentElement.scrollHeight:文档总高度

判断思路是:视口的高度 + 文档的滚动距离 >= 文档总高度,可以预留50的距离做预加载。前端每一次判断触底后,会向后端请求下一页数据,并返回 nomore 字段,表示是否还有未加载文章。这里有个细节是:每一次触底后手动将 nomore 设为 true,后端返回 nomore 后再修改其值,这样做的目的是防止在请求返回前再次发送请求。
参考:搞清clientHeight、offsetHeight、scrollHeight、offsetTop、scrollTop

如何展示博文

没有实际做博客之前,以为每一篇博文都是通过单独写html标签的方式排版的...然后通过别人的项目,发现了正确的做法是:
使用markdown/富文本编辑器编辑文章,生成html片段,在vue中使用v-html语法插入该html片段,文章便以html标签的形式渲染出来可。博客中使用的markdown编辑器是mavonEditor,另外使用highlightjs高亮代码。

<div v-html="articleInfo.contentHtml" class="article-container" ref="content"></div>

提取目录

    extractCatalog() {
      let contentElementRef = Array.from(
        this.$refs.content.querySelectorAll("h1,h2,h3,h4,h5,h6")
      );
      contentElementRef.forEach((item, index) => {
        item.id = item.localName + "-" + index;
        this.catalogList.push({
          tagName: item.localName,
          href: `#${item.localName}-${index}`,
          text: item.innerText
        });
      });
    }

上一小节中,将文章的html片段渲染到了div容器元素中,接下来可以使用querySelectorAll("h1,h2,h3,h4,h5,h6")函数来找出文中所有的标题Dom节点,querySelectorAll()的好处在于它会按照传入参数的顺序进行查找,所以不用担心乱序。获取到目录后对各级目录编号,以便生成锚点进行跳转。

判断对应节是否出现

    watchPageScrollFunc() {
      let contentElementRef = Array.from(
        this.$refs.content.querySelectorAll("h1,h2,h3,h4,h5,h6")
      );
      for (let index = 0; index < contentElementRef.length; index++) {
        const viewPortHeight =
          window.innerHeight ||
          document.documentElement.clientHeight ||
          document.body.clientHeight;
        const elementHeight = contentElementRef[index].clientHeight;
        const el = contentElementRef[index];
        const top =
          el.getBoundingClientRect() &&
          el.getBoundingClientRect().top + elementHeight;
        if (top <= viewPortHeight && top > 0) {
          this.firstVisibleElemetHref = this.catalogList[index].href;
          break;
        }
      }
    }
    
    window.addEventListener("scroll", this.watchPageScrollFunc);

判断方法:元素上边到视口上边的距离 + 元素自身高度 <= 视口高度 && 元素上边到视口上边的距离 + 元素自身高度 > 0
其中,获取元素到视口上边的距离用到:getBoundingClientRect(),某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性:

rectObject = object.getBoundingClientRect();

rectObject.top // 元素上边到视窗上边的距离;

rectObject.right // 元素右边到视窗左边的距离;

rectObject.bottom // 元素下边到视窗上边的距离;

rectObject.left // 元素左边到视窗左边的距离;

在整个文章的Dom结构中,使用querySelectorAll("h1,h2,h3,h4,h5,h6")去搜索所有标题类的Dom节点,每一次滚动都去依次动态检查这些标题元素距离视口上方的距离,找到第一个出现在视口中的Dom元素就返回。另外加上节流函数可优化性能。

参考:如何判断元素是否在可视区域ViewPort

评论的实现

评论数据采用的数据结构如下,这里只做到了二级评论,数据结构定下来,具体的实现还是比较简单的。

let comments = [
  {
    author: "admin",
    content: "留言1",
    articleId: "9",
    time: "2020-04-12 10:59",
    id: "0",
    replyList: [
      {
        author: "ghm",
        content: "回复留言1",
        articleId: "9",
        replyTo: "admin",
        time: "2020-04-12 10:59",
        id: "0-0"
      }
    ]
  },
  {
    author: "admin",
    content: "留言2",
    articleId: "9",
    time: "2020-04-12 10:59",
    id: "1",
    replyList: [
      {
        author: "ghm",
        content: "回复留言2",
        articleId: "9",
        replyTo: "admin",
        time: "2020-04-12 11:00",
        id: "1-0"
      }
    ]
  }
];

可添加表情的输入框

第一版的实现方式:在<input>中直接插入emojiunicode,展示效果如下所示:
image.png
这样做的问题在于:展示效果没有图片好,并且在不同的浏览器中会展现出不同的效果。果断换用图片形式展示表情,但是<input>是不能插入<img>标签的,于是参考了掘金的实现方式,使用contenteditable这个css属性将普通Dom元素变为可编辑的Dom元素,这样就可以使用appendChild()的方式将<img>插入,最终的显示效果也是明显优于直接使用emoji的。

  <div
    class="textarea"
    ref="inputContent"
    contenteditable="true"
    autocomplete="off"
    :placeholder="placeholder"
    draggable="false"
    spellcheck="false"
  ></div>

image.png

后端

数据库表的设计

表1:文章列表:
image.png

表2: 文章详情:
image.png

表3:用户详情
image.png

webServer

使用了 eggjs 作为 web服务器,eggjs 的文档很完善,照着文档撸代码就够了。eggjs奉行约定优于配置,有以下优点:

  1. 统一目录结构
  2. 统一分层设计:router-controller-service-model
  3. 全套安全、日志、测试方案
  4. 高扩展性的插件机制

初始化

npm init egg --type=simple
npm i

目录结构

image.png

路由

框架约定在app/router.js文件中统一配置所有路由

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/user/:id', controller.user.info);
};

完整的理由定义如下:

router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);

注意事项:

  • 在 Router 定义中, 可以支持多个 Middleware 串联执行
  • Controller 必须定义在 app/controller 目录中。
  • 一个文件里面也可以包含多个 Controller 定义,在定义路由的时候,可以通过 ${fileName}.${functionName} 的方式指定对应的 Controller。
  • Controller 支持子目录,在定义路由的时候,可以通过 ${directoryName}.${fileName}.${functionName} 的方式制定对应的 Controller。

框架中一些常用的路由用法:

  • 获取查询参数

    // curl http://127.0.0.1:7001/search?name=egg
    ctx.query.name
  • 获取路径参数

    // app/router.js
    module.exports = app => {
      app.router.get('/user/:id/:name', app.controller.user.info);
    };
    
    // app/controller/user.js
    exports.info = async ctx => {
      ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
    };
    
    // curl http://127.0.0.1:7001/user/123/xiaoming
    
  • post请求boby的获取

    ctx.request.body
  • 重定向

     // 内部路由重定向
     app.router.redirect('/', '/home/index', 302);
     // 外部路由重定向
     ctx.redirect(\`http://cn.bing.com\`);

控制器(Controller)

简单的说 Controller 负责解析用户的输入,处理后返回相应的结果。框架推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的service方法处理业务,得到业务结果后封装并返回:

  1. 获取用户通过 HTTP 传递过来的请求参数。
  2. 校验、组装参数。
  3. 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
  4. 通过 HTTP 将结果响应给用户。

定义:
所有的 Controller 文件都必须放在 app/controller 目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。

// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    const createRule = {
      title: { type: 'string' },
      content: { type: 'string' },
    };
    // 校验参数
    ctx.validate(createRule);
    // 组装参数
    const author = ctx.session.userId;
    const req = Object.assign(ctx.request.body, { author });
    // 调用 Service 进行业务处理
    const res = await service.post.create(req);
    // 设置响应内容和响应状态码
    ctx.body = { id: res.id };
    ctx.status = 201;
  }
}
module.exports = PostController;

上面定义的PostController的方法可以通过文件名和方法名的方式使用。

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.post('createPost', '/api/posts', controller.post.create);
}

定义的 Controller 类,会在每一个请求访问到 server 时实例化一个全新的对象,而项目中的 Controller 类继承于 egg.Controller,会有下面几个属性挂在 this 上:this.ctxthis.appthis.servicethis.configthis.logger

服务(Service)

Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:

  • 保持 Controller 中的逻辑更加简洁。
  • 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。
  • 将逻辑和展现分离,更容易编写测试用例

使用场景:

  • 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
  • 第三方服务的调用,比如 GitHub 信息获取等。

Service 文件必须放在 app/service 目录,可以支持多级目录,访问的时候可以通过目录名级联访

app/service/biz/user.js => ctx.service.biz.user

由于它继承于 egg.Service,故拥有下列属性方便我们进行开发:this.ctxthis.appthis.servicethis.configthis.logger

MySQL

egg提供了 egg-mysql插件来访问 MySQL 数据库

安装插件:

$ npm i --save egg-mysql

开启插件:

// config/plugin.js
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};

最后在 config/config.${env}.js 配置各个环境的数据库连接信息

// config/config.${env}.js
exports.mysql = {
  // 单数据库信息配置
  client: {
    // host
    host: 'mysql.com',
    // 端口号
    port: '3306',
    // 用户名
    user: 'test_user',
    // 密码
    password: 'test_password',
    // 数据库名
    database: 'test',
  },
  // 是否加载到 app 上,默认开启
  app: true,
  // 是否加载到 agent 上,默认关闭
  agent: false,
};

Sequelize

Sequelize 是 nodejs 社区广泛使用的 ORM 框架,将关系数据库的表结构映射为对象,让开发者使用 js 语法完成数据库操作。另外Sequelize 提供了 Migrations,帮助开发者管理数据库的每一次变更,因此每一次库表的变动都应通过 Migrations 来实现。

egg 提供了集成 Sequelize 的脚手架:

npm init egg --type=sequelize

上线

配置服务器环境

1.一台linux服务器
作者买的阿里云最便宜的ECS云服务器,配置为1核2G,1M带宽,作为学习使用完全够了,操作系统选择的CentOS。然后参照阿里云给出的教程在服务器上部署NodeJs环境部署mysql
2.购买域名
在阿里云上购买域名后,按照新手引导设置域名解析,同时设置 www 和 @,网站便可通过 www.xxx.com 和 xxx.com 访问。同时,想要使用域名访问网站,还需要对域名进行备案。
3.本地数据库迁移到云服务器
作者使用的数据库可视化工具是navicat,在navicat中对选中的数据库做转储SQL文件处理,再在云服务器的mysql中新建数据库,运行此SQL文件,便完成了数据库的迁移。
image.png

上传文件

1.下载文件传输工具 Xftp
2.部署前端代码

npm run build

打包好后,使用 Xftp 将 dist 文件夹上传到服务器中
3.部署后端代码
将后端代码 git clone 到服务器中

npm i
npm start

配置nginx

1.安装nginx

yum install nginx

安装好的 nginx 会在 /etc/nginx
2.使用 Xftp 修改 nginx 配置
找到/etc/nginx目录下的 nginx.conf文件,使用记事本打开编辑:

user  root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
  include       mime.types;
  default_type  application/octet-stream;

  sendfile        on;
  keepalive_timeout  65;

  server {
    listen       80;
    server_name  localhost;
    root  /root/project/dt_blog/frontend/dist/;
    index  index.html;
    add_header Access-Control-Allow-Origin *;
      
    location /api {
      proxy_pass  http://127.0.0.1:7001;
      proxy_set_header  HOST $host;
    }
        
    location / {
      try_files $uri $uri/ @router;
      index index.html;
    }

    location @router {
      rewrite ^.*$ /index.html last;
    }
  }
}

其中这两个配置很重要:

location / {
  try_files $uri $uri/ @router;
  index index.html;
}

location @router {
  rewrite ^.*$ /index.html last;
}

网站部署好后,可以正常访问,但是打开二级页面后再刷新,就会404,原因是:v-router设置的路径并不是真实存在的路由,工程中的路由跳转都是通过 js 实现的,而将前端打包好的 dist 目录部署在服务器后,在浏览器中访问根路径会默认打到 index.html 中,但是直接访问其他路径,就没有一个真实的路径与其对应。(参考:https://www.cnblogs.com/kevingrace/p/6126762.html

做好以上配置后,就可以通过 服务器ip:80 来访问部署好的网站了,但是一直通过 ip 地址访问网站不太合理,那么就需要给网站配置域名。

配置域名

查看原文

l1035118279 收藏了文章 · 4月28日

后台管理系统,前端根据角色动态设置菜单栏和路由

后台管理系统都有这种需求,不同角色账号进来后,就只能看到自己权限内的页面

这种权限限制,需要前后端共同完成。后端需要在用户越权访问时,返回错误提示。

也可让后端直接返回路由列表。只是这样不够灵活,之后每新增一个页面都要他们配置路由和权限,不符合前后端分离原则。

前期准备

  1. 与产品经理、后端同学讨论,得到一份角色类型清单
  2. 约定角色分别能进入哪些页面

核心逻辑

  1. 创建路由列表和菜单列表(左侧/顶部),两者格式相似,菜单就是多些图标啥的字段
  2. 将路由列表分为两部分:登录后才能看的(权限列表)和未登录也能看的(游客列表)
  3. 为权限列表里的每个路由添加角色数组字段,里面的角色才能访问此路由
  4. 在路由配置文件中添加跳转到新页面前的导航钩子,在里面根据用户登录后返回的角色信息,与权限列表进行比对,计算得出其所能访问的路由列表,保存到 vuex 中
  5. 通过 router.addRoutes() 方法,将两个列表拼接起来(Vue 框架)
  6. 同样对比计算得出可见的菜单列表,赋值并保存到 vuex 中
  7. 显示页面

代码

游客列表:routes.js

import layoutHeaderAside from '@/layout/header-aside'

// 由于懒加载页面太多的话会造成webpack热更新太慢,所以开发环境不使用懒加载,只有生产环境使用懒加载
const _import = process.env.NODE_ENV === 'development' ? file => require('@/views/' + file).default : file => () => import('@/views/' + file)

/**
 * 在主框架内显示
 */
const frameIn = [
  {
    path: '/',
    redirect: { name: 'index' },
    component: layoutHeaderAside,
    children: [
      // 首页
      {
        path: '/index',
        name: 'index',
        component: _import('system/index')
      },
      // 刷新页面 必须保留
      {
        path: '/refresh',
        name: 'refresh',
        hidden: true,
        component: _import('system/function/refresh')
      },
      // 页面重定向 必须保留
      {
        path: '/redirect/:route*',
        name: 'redirect',
        hidden: true,
        component: _import('system/function/redirect')
      }
    ]
  }
]

/**
 * 在主框架之外显示
 */
const frameOut = [
  // 登录
  {
    path: '/login',
    name: 'login',
    component: _import('system/login')
  }
]

/**
 * 错误页面
 */
const errorPage = [
  {
    path: '*',
    name: '404',
    component: _import('system/error/404')
  }
]

// 导出需要显示菜单的
export const frameInRoutes = frameIn

// 重新组织后导出
export default [
  ...frameIn,
  ...frameOut,
  ...errorPage
]

权限列表:auth-routes.js

import layoutHeaderAside from '@/layout/header-aside'
const _import = process.env.NODE_ENV === 'development' ? file => require('@/views/' + file).default : file => () => import('@/views/' + file)

const lists = [
  {
    path: '/',
    redirect: { name: 'index' },
    component: layoutHeaderAside,
    children: [
      // 机构管理
      {
        path: '/organization-management',
        name: 'organizationManagement',
        meta: {
          title: '机构管理',
          auth: true,
          roles: [0]  // 有权进入的角色
        },
        component: _import('pages/organization-management')
      },
      // 人员管理
      {
        path: '/personnel-management',
        name: 'personnelManagement',
        meta: {
          title: '人员管理',
          auth: true,
          roles: [0]
        },
        component: _import('pages/personnel-management')
      },
      // 角色管理
      {
        path: '/roles-management',
        name: 'rolesManagement',
        meta: {
          title: '角色管理',
          auth: true,
          roles: [0]
        },
        component: _import('pages/roles-management')
      },
      // 角色授权
      {
        path: '/roles-impower',
        name: 'rolesImpower',
        meta: {
          title: '授权',
          auth: true,
          roles: [0]
        },
        component: _import('pages/roles-management/components/roles-impower')
      },
      {
        path: 'authority-management',
        name: 'authority-management',
        meta: {
          title: '权限管理',
          auth: true,
          roles: [0]
        },
        component: _import('pages/authority-management')
      },
      {
        path: 'agency-register-approval',
        name: 'agency-register-approval',
        meta: {
          title: '机构注册审批',
          auth: true,
          roles: [1]
        },
        component: _import('pages/agency-register-approval')
      },
      {
        path: 'program-info-management',
        name: 'program-info-management',
        meta: {
          title: '项目信息管理',
          auth: true,
          roles: [1]
        },
        component: _import('pages/program-info-management')
      },
      {
        path: 'product-category-management',
        name: 'product-category-management',
        meta: {
          title: '产品类目管理',
          auth: true,
          roles: [1]
        },
        component: _import('pages/product-category-management')
      }
    ]
  }
]

export default lists

菜单列表:aside.js

// 菜单 侧边栏
export default [
  {
    path: '/index',
    title: '首页',
    icon: 'home',
    roles: [0, 1]
  },
  {
    path: '/organization-management',
    title: '机构管理',
    icon: 'institution',
    roles: [0]
  },
  {
    title: '用户管理',
    icon: 'user',
    roles: [0],
    children: [
      {
        path: '/personnel-management',
        title: '人员管理',
        icon: '',
        roles: [0]
      },
      {
        path: '/roles-management',
        title: '角色管理',
        icon: '',
        roles: [0]
      }
    ]
  },
  {
    title: '权限管理',
    icon: 'shield',
    roles: [0],
    children: [
      {
        path: '/authority-management',
        title: '权限管理',
        icon: '',
        roles: [0]
      }
    ]
  },
  {
    title: '注册审批',
    icon: 'legal',
    roles: [1],
    children: [{
      path: '/agency-register-approval',
      title: '机构注册审批',
      icon: '',
      roles: [1]
    }]
  },
  {
    title: '项目管理',
    icon: 'window-restore',
    roles: [1],
    children: [{
      path: '/program-info-management',
      title: '项目信息管理',
      icon: '',
      roles: [1]
    }]
  },
  {
    title: '类目管理',
    icon: 'database',
    roles: [1],
    children: [{
      path: '/product-category-management',
      title: '产品类目管理',
      icon: '',
      roles: [1]
    }]
  }
]

路由配置文件

import routes from './routes'
// 侧边栏菜单数据
import menuAside from '@/menu/aside'

/**
 * 路由拦截
 * 权限验证
 */
router.beforeEach(async (to, from, next) => {
  // 进度条
  NProgress.start()
  // 关闭搜索面板
  store.commit('d2admin/search/set', false)

  async function getRouteAndMenu () {
    await store.dispatch('d2admin/user/GenerateRoutes') // 获取可访问路由,在 vuex 中保存
    router.addRoutes(store.state.d2admin.user.accessedRouters) // 和原有的固定路由合并到一起
    // 获取侧边栏菜单,在 vuex 中保存
    await store.dispatch('d2admin/menu/GenerateMenu', { 
      role: store.state.d2admin.user.info.role,
      menuAside
    }) 
    // 设置侧边栏菜单
    store.commit('d2admin/menu/asideSet', store.state.d2admin.menu.aside)
    next()
  }

  if (from.name === null && to.name === '404') {
    // 避免刷新出现 404 页面
    getRouteAndMenu()
  }

  // 在需要验证的路由中,进一步判断角色路由
  if (to.matched.some(r => r.meta.auth)) {
    // 将cookie里是否存有token作为验证是否登录的条件
    const token = util.cookies.get('token')
    if (token && token !== 'undefined') {
      const accessedRouters = store.state.d2admin.user.accessedRouters
      if (accessedRouters.length <= 0) {
        // 本地没有保存可访问路由,就需要计算
        getRouteAndMenu()
      } else {
        next()
      }
    } else {
      // 没有登录的时候跳转到登录界面
      // 携带上登陆成功之后需要跳转的页面完整路径
      next({
        name: 'login',
        query: {
          redirect: to.fullPath
        }
      })
      NProgress.done()
    }
  } else {
    // 不需要身份校验 直接通过
    next()
  }
})

store 里的登录 module: account.js

actions: {
    /**
     * @description 登录
     * @param {Object} context
     * @param {Object} payload username {String} 用户账号
     * @param {Object} payload password {String} 密码
     * @param {Object} payload route {Object} 登录成功后定向的路由对象 任何 vue-router 支持的格式
     */
     // 登录后保存用户角色信息
    login ({ dispatch }, {
      username = '',
      password = ''
    } = {}) {
      return new Promise((resolve, reject) => {
        // 开始请求登录接口
        AccountLogin({
          username,
          password
        })
          .then(async res => {
            // 设置 cookie 一定要存 uuid 和 token 两个 cookie
            // 整个系统依赖这两个数据进行校验和存储
            // uuid 是用户身份唯一标识 用户注册的时候确定 并且不可改变 不可重复
            // token 代表用户当前登录状态 建议在网络请求中携带 token
            // 如有必要 token 需要定时更新,默认保存一天
            util.cookies.set('uuid', res.userId)
            util.cookies.set('token', res.token)
            // 设置 vuex 用户信息,保存用户名称和用户角色
            await dispatch('d2admin/user/set', {
              name: res.userName,
              role: res.userType
            }, { root: true })
            // 用户登录后从本地储存中加载一系列的设置
            await dispatch('load')
            // 结束
            resolve()
          })
          .catch(err => {
            console.log('err: ', err)
            reject(err)
          })
      })
    }
}

store 里的用户 module: user.js

// 原始菜单列表
import authRoutes from '@/router/auth-routes'

// 判断当前路由字段的 roles 数组里是否包含用户角色
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return route.meta.roles.some(role => role === roles)
  } else {
    return true
  }
}

export default {
  namespaced: true,
  state: {
    // 用户信息
    // 其中的 role , 0 是管理员, 1 是非管理员
    info: {},
    accessedRouters: []
  },
  mutations: {
    // 保存当前用户的可访问路由
    setRouters (state, routers) {
      state.accessedRouters = routers
    }
  },
  actions: {
    // 生成根据权限可访问的路由
    // 保存并返回
    GenerateRoutes({ state, commit }) {
      return new Promise(resolve => {
        const { role } = state.info;
        const accessedRouters = authRoutes.filter(v => {
          if (hasPermission(role, v)) {
            if (v.children && v.children.length > 0) {
              v.children = v.children.filter(child => {
                if (hasPermission(role, child)) {
                  return child
                }
                return false;
              });
              return v
            } else {
              return v
            }
          }
          return false;
        });
        commit('setRouters', accessedRouters);
        resolve(accessedRouters);
      })
    }
  }
}

store 里的菜单 module: menu.js

与路由 module 一样步骤

退出登录时,清除内存里的数据

store 里的用户 module: account.js

退出登录时,清空当前内存中保存的计算后的路由数据、菜单数据

async function logout () {
        // 删除cookie
        util.cookies.remove('token')
        util.cookies.remove('uuid')
        // 清空 vuex 用户信息
        await dispatch('d2admin/user/set', {}, { root: true })
        // 清空 vuex 的路由和菜单信息
        commit('d2admin/user/setRouters', [], { root: true })
        commit('d2admin/menu/setMenu', [], { root: true })
        // 跳转路由
        router.push({ name: 'login' })
      }

注意事项

  • 这里只把路由和菜单保存在内存中,因此页面刷新后,如果当前是权限列表里的路径,会由于路由列表被重置为游客列表导致转到 404 页面。这也是为什么在路由配置文件里,我也进行了 404 判断,比较麻烦。大家可以把路由和菜单保存在本地,刷新后从本地恢复即可。
  • 角色用有意义的名称如 admin, user 等,比用数字代表更直观
  • 在退出登录时,清空当前计算出来的数据,避免被下个用户看到
查看原文

认证与成就

  • 获得 13 次点赞
  • 获得 28 枚徽章 获得 1 枚金徽章, 获得 6 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-05-05
个人主页被 316 人浏览