阳光沙滩博客系统-手机登录
 如果说现有的登录,可不可以呢? 也是可以的,只不过会有问题: 你在PC端,也就是浏览器上登录了,如果你在手机上登录,那么PC端的就会下线; 假设使用同一个token,修改代码可以实现,但无法同时更新各端的token当token过期的时候。比如这次请i娶是从手机上发起的,token过期了,就会生成新的token和token_key,这时手机是可以更新的,但是浏览器没法更新。 退出登录也得改,如果使用同一个token,退出的时候,就是干掉自己端的token_key。 所以我们要使用各端独立的token 比如微信:你可在网页版/电脑/pad上登录(这几个是一致的,也就一个登录了,其他登录的就会下线),手机上app的就是独立的。微信是有两个token的。 如何去实现各端的token,使用一个接口。 1、知道来源:是从PC发起的登录,还是移动端发起的登录,from字段。从请求头也可以判断,设备比较多的话比较麻烦,所以还单独加个字段。不传的就网页端,传的话就是其他的,比如说移动端。 2、登录、流程差不多,token里会添加来源字段。每次访问就知道是从哪里发起的请求了。比如说发起评论,就可以知道这个评论是从移动端还是PC端来的。 保存token_key到refreshToken里,需要多一个字段。 3、退出登录和过期:之前是做法是干掉所有的refreshToken记录,重新创建新的。不删除,只修改,如果没有就添加。 修改点: 登录 数据库 解析用户流程 退出登录 测试点: 测试登录,测试PC端登录和移动端登录 (pass) 测试权限的使用是否正常(用户解析)(pass) 退出登录,测试PC端退出登录,移动端退出登录 具体的修改请查看视频吧 课程地址:JavaWeb实现个人博客系统
 
              2020-09-08 15:13
             
              5007
             
                博客系统
              
                手机登录
              
                移动端
              
                安卓开发
              
                android
              
RecyclerView显示多种类型
 RecyclerView显示多种类型 对于这个问题我也好无语呀,这个很简单呀。 纯体力活。 RecyclerView课程地址 Android控件之RecyclerView 相关提问: Recycleview 多种条目类型 如何绑定数据 案例源码地址: https://github.com/TrillGates/RecyclerViewMultiTypeDemo 效果: 解析 网络权限     
 主布局activity_main.xml 
    
 主Activity:MainActivity.kt 
class MainActivity : AppCompatActivity() {
    private val contentAdapter by lazy {
        ContentListAdapter()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //初始化数据
        initData()
        //初始化控件
        initView()
    }
    private fun initView() {
        contentListView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            this.adapter = contentAdapter
            addItemDecoration(object : RecyclerView.ItemDecoration() {
                override fun getItemOffsets(
                    outRect: Rect,
                    view: View,
                    parent: RecyclerView,
                    state: RecyclerView.State
                ) {
                    outRect.bottom = 2
                }
            })
        }
    }
    private fun initData() {
        val dataJson = requestData.resultJson
        val gson = Gson()
        val response = gson.fromJson
(dataJson, Response::class.java)
        contentAdapter.addData(response.data)
        println(response.code)
        println(response.message)
        println(response.data.size)
    }
}
 适配器ContentListAdapter.kt 
class ContentListAdapter : RecyclerView.Adapter() {
    private val data = ArrayList()
    companion object {
        const val NONE_IMG: Int = 0
        const val ONE_IMG: Int = 1
        const val TWO_IMG: Int = 2
        const val THREE_IMG: Int = 3
    }
    fun addData(contents: List) {
        data.clear()
        data.addAll(contents)
        notifyDataSetChanged()
    }
    class InnerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    }
    override fun getItemViewType(position: Int): Int {
        return when (data[position].covers.size) {
            0 -> NONE_IMG
            1 -> ONE_IMG
            2 -> TWO_IMG
            else -> THREE_IMG
        }
    }
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): InnerHolder {
        val itemView: View
        when (viewType) {
            NONE_IMG -> {
                val itemBind = DataBindingUtil.inflate(
                    LayoutInflater.from(parent.context),
                    R.layout.item_no_image,
                    parent,
                    false
                )
                itemView = itemBind.root
                itemView.tag = itemBind
            }
            ONE_IMG -> {
                val itemBind = DataBindingUtil.inflate(
                    LayoutInflater.from(parent.context),
                    R.layout.item_one_image,
                    parent,
                    false
                )
                itemView = itemBind.root
                itemView.tag = itemBind
            }
            TWO_IMG -> {
                val itemBind = DataBindingUtil.inflate(
                    LayoutInflater.from(parent.context),
                    R.layout.item_two_image,
                    parent,
                    false
                )
                itemView = itemBind.root
                itemView.tag = itemBind
            }
            else -> {
                val itemBind = DataBindingUtil.inflate(
                    LayoutInflater.from(parent.context),
                    R.layout.item_three_image,
                    parent,
                    false
                )
                itemView = itemBind.root
                itemView.tag = itemBind
            }
        }
        return InnerHolder(itemView)
    }
    override fun getItemCount(): Int {
        return data.size
    }
    override fun onBindViewHolder(holder: InnerHolder, position: Int) {
        val dataBinding = holder.itemView.tag
        val data = data[position]
        when (dataBinding) {
            is ItemNoImageBinding -> {
                dataBinding.item = data
            }
            is ItemOneImageBinding -> {
                dataBinding.item = data
            }
            is ItemTwoImageBinding -> {
                dataBinding.item = data
            }
            is ItemThreeImageBinding -> {
                dataBinding.item = data
            }
        }
    }
}
 数据bean类 data class ListItem(
    val title: String,
    val covers: ArrayList,
    val viewCount: Int,
    val commentCount: Int
);
 图片加载工具类 class ImageLoadUtils {
    companion object {
        @BindingAdapter("imagePath")
        @JvmStatic
        fun loadImage(image: ImageView, imageUrl: String?) {
            if (!TextUtils.isEmpty(imageUrl)) {
                Glide.with(image.context).load(imageUrl).into(image)
            }
        }
    }
}
 布局之类的代码请下载吧。 就是这么简单,全部代码请到github上载。 RecyclerViewMultiTypeDemo  
              2020-09-06 16:22
             
              4409
             
                安卓开发
              
                RecyclerView
              
                控件
              
                博客系统
              
                测试
              
vue.js下载文件
 vue.js下载文件 有个同学不知道如何去下载文件,早上比较忙,弄了一份代码给他。晚上回来,发现不对。 现在整理一下,案例,下载一张图片: 基于axios 需要有axios import axios from 'axios'
 get请求:   // get请求
  requestGet(url, params = {}) {
    return new Promise((resolve, reject) => {
      axios.get(url, params).then(res => {
        resolve(res.data)
      }).catch(error => {
        reject(error)
      })
    })
  },
 接口 export const getFile = (url, params) => {
  return http.requestGet(url, params);
};
 调用  download() {
        api.getFile("/images/vip_ad.png",
          {
            responseType: 'blob',
            headers: {
              'Content-Type': 'application/octet-stream'
            },
          }).then(result => {
          console.log(result);
          this.convertRes2Blob(result);
        });
      },
 以二进制的形式保存成文件  convertRes2Blob(response) {
        // 将二进制流转为blob
        let blob = new Blob([response]);
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
          // 兼容IE,window.navigator.msSaveBlob:以本地方式保存文件
          window.navigator.msSaveBlob(blob, decodeURI("文件名.png"))
        } else {
          console.log('save....');
          // 创建新的URL并指向File对象或者Blob对象的地址
          const blobURL = window.URL.createObjectURL(blob);
          // 创建a标签,用于跳转至下载链接
          const tempLink = document.createElement('a');
          tempLink.style.display = 'none';
          tempLink.href = blobURL;
          tempLink.setAttribute('download', decodeURI("文件名.png"));
          // 兼容:某些浏览器不支持HTML5的download属性
          if (typeof tempLink.download === 'undefined') {
            tempLink.setAttribute('target', '_blank')
          }
          // 挂载a标签
          document.body.appendChild(tempLink);
          tempLink.click();
          document.body.removeChild(tempLink);
          // 释放blob URL地址
          window.URL.revokeObjectURL(blobURL);
        }
      },
 测试 如果要下载Pdf的话,修改一下路径即可。 比如说:    convertRes2Blob(response) {
        // 将二进制流转为blob
        let blob = new Blob([response]);
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
          // 兼容IE,window.navigator.msSaveBlob:以本地方式保存文件
          window.navigator.msSaveBlob(blob, decodeURI("文件名.pdf"))
        } else {
          console.log('save....');
          // 创建新的URL并指向File对象或者Blob对象的地址
          const blobURL = window.URL.createObjectURL(blob);
          // 创建a标签,用于跳转至下载链接
          const tempLink = document.createElement('a');
          tempLink.style.display = 'none';
          tempLink.href = blobURL;
          tempLink.setAttribute('download', decodeURI("文件名.pdf"));
          // 兼容:某些浏览器不支持HTML5的download属性
          if (typeof tempLink.download === 'undefined') {
            tempLink.setAttribute('target', '_blank')
          }
          // 挂载a标签
          document.body.appendChild(tempLink);
          tempLink.click();
          document.body.removeChild(tempLink);
          // 释放blob URL地址
          window.URL.revokeObjectURL(blobURL);
        }
      },
      download() {
        api.getFile("/2020/07/13/数组的操作.pdf",
          {
            responseType: 'blob',
            headers: {
              'Content-Type': 'application/octet-stream'
            },
          }).then(result => {
          console.log(result);
          this.convertRes2Blob(result);
        });
      },
 下载结果: 打开正常
 
              2020-09-06 15:46
             
              3566
             
                vue.js
              
                前端
              
                开发
              
                测试
              
                下载
              
Nuxt.js时间格式化
 如果我们在客户端渲染,直接导入我们的date.js问题不大,按我们的博客系统管理中心的方式格式化即可! export function formatDate(date, fmt) {
    if (/(y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
    }
    let o = {
        'M+': date.getMonth() + 1,
        'd+': date.getDate(),
        'h+': date.getHours(),
        'm+': date.getMinutes(),
        's+': date.getSeconds()
    };
    for (let k in o) {
        if (new RegExp(`(${k})`).test(fmt)) {
            let str = o[k] + '';
            fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str))
        }
    }
    return fmt
}
function padLeftZero(str) {
    return ('00' + str).substr(str.length)
}
 如果在服务端进行渲染如何格式化呢? 我们可以通过过滤器来转换格式。 在plugins目录下创建一个文件名为:dateformat.js import Vue from 'vue'
export function formatDate(dateStr, fmt) {
  let date = new Date(dateStr)
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
  }
  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  };
  for (let k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      let str = o[k] + '';
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str))
    }
  }
  return fmt
}
function padLeftZero(str) {
  return ('00' + str).substr(str.length)
}
let filters = {
  formatDate
};
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
});
export default filters
 像我们的ElementUI一样注册插件   plugins: [
    {
      src: '@/plugins/element-ui',
      ssr: true
    },
    {
      src: '@/plugins/word-cloud',
      ssr: false
    },
    {
      src: '@/plugins/dateformat',
      ssr: true
    },
  ],
 使用:      
       {{item.blogCreateTime | formatDate('yyyy-MM-dd hh:mm:ss')}}
     
 
              2020-09-08 15:12
             
              3545
             
                博客系统
              
                Nuxt.js
              
                服务端渲染
              
                毕业设计
              
                前端开发
              
前端侧栏Tab和内容滚动联动切换标题
 前端侧栏Tab和内容滚动联动切换标题 我们阳光沙滩博客系统课程里有一个关于页面 我们做成这个样子 点击侧栏可以切换右边内容;滚动右侧内容,可以切换左边的内容。 基本UI实现 左侧使用 ElementUI的组件Tabs https://element.eleme.cn/#/zh-CN/component/tabs 同学们可以去这里使用,至于怎么依赖ElementUI,可以参考官方说明,或者学习我们的前端课程。     
      
        
        
        
        
        
      
    
 右侧布局  
      
        
          关于阳光沙滩
        
        
          阳光沙滩创立于2014年11月22日,那是一个阳光明媚的下午。韩寒的电影《后会无期》里面有一句台词:sun of beach
          (阳光沙滩),于是阳光沙滩创立。阳光沙滩是一个学习编程的社区网站。
          你可以在这里学习,写博客,写笔记,分享经验,提问题,分享链接。你可以遇到志同道合的人,收获知识、经验与同性朋友。
        
       
      
      
        
          我们的愿景
        
        
          让每一个热爱编程的年轻人成为优秀的程序员。
        
       
      
      
        
        
          广告投放 ad@sunofbeach.net(仅作演示)
          
          商务合作 bd@sunofbeach.net(仅作演示)
          
          内容投诉 rp@sunofbeach.net(仅作演示)
        
       
      
     
 左边固定,右边加个margin值。 事件处理 事件主要包括两部分:滚动、点击左侧 滚动 滚动的话,我们需要处理侧栏悬浮 添加滚动监听:  mounted() {
      ......
      window.addEventListener("scroll", this.onWindowScroll);
      this.onWindowScroll();
    },
 记得删除监听哦     beforeDestroy() {
      window.removeEventListener('scroll', this.onWindowScroll);
    },
 这里面就是滑动的代码了 onWindowScroll() {
        let dy = document.documentElement.scrollTop;
        let dx = document.documentElement.scrollLeft;
        let leftBox = document.getElementById('about-left-part');
        let parentBox = document.getElementById('about-box');
        if (leftBox && parentBox) {
          let parentBoxTop = parentBox.offsetTop;
          // console.log(parentBoxTop);
          if (dy > parentBoxTop) {
            leftBox.style.top = '0px';
          } else {
            leftBox.style.top = (parentBoxTop - dy) + 'px';
          }
          //处理横向滑动
          if (dx > 0) {
            leftBox.style.left = -dx + "px";
          } else {
            leftBox.style.left = parentBox.offsetLeft + "px";
          }
        }
        if (!this.isClickLeftSwitch) {
          //处理标签的滚动范围
          for (let i = 0; i < this.contentHeaders.length - 1; i++) {
            let first = this.contentHeaders[i];
            let second = this.contentHeaders[i + 1];
            if (dy >= first.offTop && dy < second.offTop) {
              this.target = first.id;
              //console.log(this.target);
              break;
            }
          }
        } else {
          this.isClickLeftSwitch = false;
        }
 点击事件处理  onLeftItemClick() {
        this.isClickLeftSwitch = true;
        this.$router.push({
          path: '/about#' + this.target
        });
        // console.log(this.target);
      }
 具体的话请去视频教程吧
  
              2020-09-06 16:20
             
              3661
             
                博客系统
              
                前端开发
              
                毕业设计
              
                测试
              
                门户
              
阳光沙滩博客系统API接口
 阳光沙滩博客系统API接口 分两部分:用户的API接口、门户的API接口 在线预览网址: 博客系统 baseUrl http://api.coincent.cn
 统一返回结果bean类 ResponseResult{
code	integer($int32)
data	{...}
message	string
success	boolean
}
 用户API接口 用户bean类 SobUser{
avatar	string
createTime	string($date-time)
email	string
id	string
loginIp	string
password	string
regIp	string
roles	string
sign	string
state	string
updateTime	string($date-time)
userName	string
}
 用户登录 接口: /user/account/login/{captcha}/
 方法:POST 参数 captcha string(path,必须) 验证码,图灵验证码,请看后面的接口 sobUser SobUser(body,必须) 登录信息,邮箱/用户名,密码(md5值) from 来源,非必须,来源,不填写为m_为手机端登录,p_为PC端登录 返回: 20000 成功 40003 账号被禁止 40000 失败-详情请看提示信息 退出登录 接口: /user/account/logout
 方法:GET 参数:无 返回: 40002 账号未登录 20000 成功 查询二维码的登录状态 接口: /user/account/qr_code_state/{loginId}
 方法:GET 参数: loginId 登录的ID,获取二维的时候会返回,请看后面的接口 返回: 40008 等待扫描 40009 二维码已过期 20001 登录成功 更新二维码的登录状态 手机端使用此接口 接口: /user/account/qr_code_state/{loginId}
 方法:PUT 参数 loginId 登录的ID,扫码解析可以获取到 返回: 40002 账号未登录(指你手机的账号未登录) 20000 登录成功 更新用户邮箱地址 步骤:先获取邮箱验证码,获取到邮箱验证码再来更新邮箱 接口: /user/email
 方法:PUT 参数: email string类型,新邮箱地址,query,必须 verify_code string类型,邮箱验证码, query,必须 返回: 40002 账号未登录 40000 验证码错误 20000 修改成功 用户注册 接口: /user/join_in
 方法:POST 参数 captcha_code string类型 query 图灵验证码 必须 email_code string类型 query 邮箱验证码 必须 sobUser SobUser类型 body 用户名、邮箱地址、密码(MD5值)必须 返回: 40000 操作失败,请看提示信息 20002 注册成功 更新用户密码 步骤:获取邮箱验证码,需要提交邮箱和验证码以及新的密码(md5值) 接口: /user/password/{verifyCode}
 方法:PUT 参数 verifyCode 邮箱验证码string类型,要与提交的邮箱地址对应,必须 sobUser SobUser类型,需要提交新的密码MD5值以及获取验证码的邮箱地址 返回: 40000 操作失败,请看提示 20000 密码修改完成 获取用户信息 接口: /user/user_info/{userId}
 方法:GET 参数: userId 用户ID,path,必须 返回: 40000 用户不存在 20000 获取成功 更新用户信息 接口: /user/user_info/{userId}
 方法:PUT 参数 userId 用户ID,path 必须 sobUser SobUser类型、可经修改签名、用户名、头像 返回: 40002 权限不足,只能修改自己账号的 40002 账号未登录 20000 修改成功 获取图灵验证码 接口 /user/utils/captcha
 方法:GET 参数:无 检查邮箱验证码是否正确 接口 /user/utils/check_email_code
 方法:GET 参数: captchaCode string类型 图灵验证码 query 必须 email string类型 邮箱地址 query 必须 emailCode 邮箱验证码 string类型 query 必须 返回: 40000 操作失败,具体请看信息 20000 验证码正确 查询用户的token是否有 接口 /user/utils/check_token
 方法:GET 参数:无 返回: 40000 用户未登录 20000 用户已登录,返回用户相关信息 检查邮箱地址是否有注册 接口 /user/utils/email
 方法:GET 参数 email string 邮箱地址 query 必须 返回 40000 邮箱未注册 20000 邮箱已注册 获取扫码登录的二维码 接口 /user/utils/pc_login_qr_code
 方法:GET 参数:无 返回: code 为登录ID url 为验证码的ID 检查用户名是否有注册 接口 /user/utils/user_name
 方法:GET 参数: userName 用户名 string query 必须 返回: 40000 未注册 20000 已经注册 发送邮箱验证码 接口 /user/utils/verify_code
 方法:GET 参数: captchaCode 图灵验证码 query 必须 email 邮箱地址 query 必须 type 类型 选填于(register,forget,update)注册、找回密码、更新密码 返回 40000 操作失败,请看提示 20000 验证码发送成功 门户接口 评论Bean类 Comment{
articleId	string
content	string
createTime	string($date-time)
id	string
parentContent	string
state	string
updateTime	string($date-time)
userAvatar	string
userId	string
userName	string
}
 获取文章详情 接口 /portal/article/{articleId}
 方法:GET 参数: articleId 文章id,必须,path 返回 40000 文章不存在 20000 获取文章成功 获取文章分类列表 接口 /portal/article/categories
 方法:GET 参数:无 返回: 20000 获取分类成功 获取标签列表 接口 /portal/article/label/{size}
 方法:GET 参数: size 获取标签个数,必须,最大值为30 返回: 20000 获取分类成功 根据分类获取文章内容 接口 /portal/article/list/{categoryId}/{page}/{size}
 方法:GET 参数: categoryId 分类ID path 必须 page 页码 path 必须 size 表示每一页的数量,最大值为30,必须 返回: 20000 获取文章列表成功 获取最新文章列表 接口: /portal/article/list/{page}/{size}
 方法:GET 参数: page 页码 必须 path size 每页数量 必须 path 获取相关文章 接口 /portal/article/recommend/{articleId}/{size}
 方法:GET 参数: articleId 文章ID , 必须 path size 获取推荐的数量 必须 path 返回: 20000 获取推荐列表成功 获取置顶文章列表 接口 /portal/article/top
 方法:GET 参数:无 返回: 20000 获取置顶文章成功 提交文章评论 接口 /portal/comment
 方法:POST 参数 comment Comment类型 body articleId 文章ID 必须 content 评论内容 必须 parentContent 被回复的内容,选填 返回 40002 账号未登录 40000 操作失败,请看提示信息 20000 评论成功 删除评论 接口 /portal/comment/{commentId}
 方法:DELETE 参数: commentId 评论id path 必须 返回: 40002 账号未登录 40000 评论不存在 20000 删除评论成功 40002 权限不足 获取文章的评论列表 接口 /portal/comment/list/{articleId}/{page}/{size}
 方法:GET 参数 articleId 文章ID path 必须 page 页码 path 必须 size 每页数量 path 必须 返回: 20000 获取评论列表成功 图片访问接口 一般来说,上传图片之类的会返回图片ID,通过此接口可以访问图片内容 接口 /portal/image/{imageId}
 方法:GET 参数: imageId 图片的ID,path 必须 返回: 图片流 获取扫码登录的二维码图片 接口 /portal/image/qr-code/{code}
 方法:GET 参数: code 登录码 必须,请求登录二维码信息获得,前面接口 返回 图片流 搜索内容 接口 /portal/search
 方法:GET 参数: categoryId 分类ID query 选填 keyword 关键字 query 必须 page 页码 query 必须 size 每页数量 query 必须 sort 排序方式,query 选填:根据时间的升序(1)和降序(2),根据浏览量的升序(3)和降序(4) 返回: 40000 操作失败,请看信息 20000 搜素成功 获取友情链接列表 接口 /portal/web_size_info/friend_link
 方法:GET 参数:无 返回: 20000 获取友情链接列表成功 获取首页轮播图 接口 /portal/web_size_info/loop
 方法:GET 参数:无 返回: 20000 获取轮播图列表成功
 
              2020-09-08 15:10
             
              5860
             
                API
              
                接口
              
                博客系统
              
                开发
              
                前后端分离