Vue+uniapp 博客展示

940 阅读3分钟

首页

前言

此 uniapp-blog 项目是基于 uni-app + uview-ui 开发

效果图

首页

留言

完整效果请看:sdjBlog.cn

功能描述

已经实现功能

  • 注册登录
  • 文章列表
  • 文章点赞评论
  • 文章分类
  • 留言列表
  • 个人中心

项目结构

- public index.html首页
- src
  - components
    - ArticleList   文章列表
    - LoginModal  注册登录弹框
    - MessageBarrage  留言弹幕
    - NavHeader   头部导航
  - pages
    - classify   
      - articleTag  分类标签列表
      - index  文章分类列表
    - home
      - answer 你的答案
      - articleDetail 文章详情
      - index 博客首页
      - search 文章搜索页
      - view 文章每日一看
    - message 留言列表
    - person
      - index 个人中心
      - infoArticle 历史足迹
      - infoData 个人信息
  - static 字体图标和图片
  - store Vuex状态管理
  - utils API请求和路由跳转封装
  - App.vue 配置App全局样式以及监听应用生命周期
  - main.js Vue初始化入口文件
  - manifest.json 配置应用名称、appid, logo, 版本等打包信息
  - pages.json 页面路由、选项卡等页面信息
  - uni.scss scss全局样式
- unpackage app打包图片

注册登录

注册登录使用弹出框封装,通过 modelShow 来控制弹出框显示隐藏,组件内部通过 show 来同步控制弹出框状态

<u-popup mode="center" width='85%' v-model="show" :closeable='true' 
:mask-close-able='false' border-radius='16' @close='popupClose'></u-popup>

props: {
  modelShow: {
    type: Boolean,
    default: false,
  }
},
data(){
  return {
    show: false
  }
},
methods: {
  popupClose() {
    this.$emit("closeModal", "");
  }
},
watch: {
  modelShow(val) {
    if (val) {
      this.show = val;
    }
  }
}

文章评论

评论输入框固定在页面底部,当input聚焦唤起键盘影响移动端底部fixed定位,可以通过窗口变化来重新计算赋值高度

try {
  const res = uni.getSystemInfoSync();
  this.windowHeight = res.windowHeight;
} catch (e) {
  // error
}
uni.onWindowResize((res) => {
  if(this.windowHeight - res.size.windowHeight > 80){
    this.heightChange = true
  }else{
    this.heightChange = false
  }
})

文章搜索

  • 记录搜索历史,并存储在本地,最多存储最新搜索的8个记录

  • 搜索文字高亮,通过正则替换标题样式

// 存储历史搜索
let keyword = this.keyword;
this.historyList = uni.getStorageSync("historyList") || [];
if(keyword && selectBool == '0'){
  if(uni.getStorageSync("historyList")){
    let historyList = uni.getStorageSync("historyList");
    historyList.unshift(keyword);
    historyList = [...new Set(historyList)];
    if(historyList.length > 8){
      historyList = historyList.slice(0,8);
    }
    this.historyList = historyList;
    uni.setStorageSync("historyList", historyList)
  }else{
    let historyList = [keyword];
    this.historyList = historyList;
    uni.setStorageSync("historyList", historyList);
  }
}
//搜索文字高亮
let replaceReg = new RegExp(keyword, 'g');
dataList.forEach(item=>{
  item.image = `${baseURL}/blogAdmin/file/down?downId=${item.imgId}`;
  item.createTime = item.createTime.split(' ')[0];
  item.title = item.title.replace(replaceReg, `<span style='color: #1BA2E8;'>${keyword}</span>`)
})

Vuex实现

uView简化Vuex在页面中使用,通过mixin全局混入,避免频繁的手动引入

//state声明
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

let lifeData = {};

try{
	// 尝试获取本地是否存在lifeData变量,第一次启动APP时是不存在的
	lifeData = uni.getStorageSync('lifeData');
}catch(e){
	
}

// 需要永久存储,且下次APP启动需要取出的,在state中的变量名
let saveStateKeys = ['vuex_userInfo', 'vuex_token'];

// 保存变量到本地存储中
const saveLifeData = function(key, value){
	// 判断变量名是否在需要存储的数组中
	if(saveStateKeys.indexOf(key) != -1) {
		// 获取本地存储的lifeData对象,将变量添加到对象中
		let tmp = uni.getStorageSync('lifeData');
		// 第一次打开APP,不存在lifeData变量,故放一个{}空对象
		tmp = tmp ? tmp : {};
		tmp[key] = value;
		// 执行这一步后,所有需要存储的变量,都挂载在本地的lifeData对象中
		uni.setStorageSync('lifeData', tmp);
	}
}
const store = new Vuex.Store({
	state: {
		// 如果上面从本地获取的lifeData对象下有对应的属性,就赋值给state中对应的变量
		// 加上vuex_前缀,是防止变量名冲突,也让人一目了然
		vuex_userInfo: lifeData.vuex_userInfo ? lifeData.vuex_userInfo : {},
		vuex_token: lifeData.vuex_token ? lifeData.vuex_token : '',
		// 如果vuex_version无需保存到本地永久存储,无需lifeData.vuex_version方式
		vuex_version: '1.0.1',
	},
	mutations: {
		$storeLife(state, payload) {
			// 判断是否多层级调用,state中为对象存在的情况,诸如vuex_userInfo.info.score = 1
			let nameArr = payload.name.split('.');
			let saveKey = '';
			let len = nameArr.length;
			if(nameArr.length >= 2) {
				let obj = state[nameArr[0]];
				for(let i = 1; i < len - 1; i ++) {
					obj = obj[nameArr[i]];
				}
				obj[nameArr[len - 1]] = payload.value;
				saveKey = nameArr[0];
			} else {
				// 单层级变量,在state就是一个普通变量的情况
				state[payload.name] = payload.value;
				saveKey = payload.name;
			}
			// 保存变量到本地,见顶部函数定义
			saveLifeData(saveKey, state[saveKey])
		}
	}
})

export default store

//mixin
import { mapState } from 'vuex'
import store from "@/store"

// 尝试将用户在根目录中的store/index.js的vuex的state变量,全部加载到全局变量中
let $uStoreKey = [];
try{
	$uStoreKey = store.state ? Object.keys(store.state) : [];
}catch(e){
	
}

module.exports = {
	created() {
		// 将vuex方法挂在到$u中
		// 使用方法为:如果要修改vuex的state中的user.name变量为"史诗" => this.$u.vuex('user.name', '史诗')
    // 如果要修改vuex的state的version变量为1.0.1 => this.$u.vuex('version', '1.0.1')
		this.$u.vuex = (name, value) => {
			this.$store.commit('$storeLife', {
				name,value
			})
		}
	},
	computed: {
		// 将vuex的state中的所有变量,解构到全局混入的mixin中
		...mapState($uStoreKey)
	}
}

问题记录

u-popup组件只有打开才会开始渲染,所以需要监听打开事件open,来对弹框中的u-form进行校验初始化

<u-popup mode="center" width='85%' v-model="show" :closeable='true'
 :mask-close-able='false' border-radius='16' @open='popupOpen' @close='popupClose'></u-popup>
 popupOpen() {
   //触发校验
  this.$refs.uLoginForm.setRules(this.loginRules);
  this.$refs.uRegisterForm.setRules(this.registerRules);
}

微信小程序中u-form表单重置只清理了校验,没初始化数据,需要手动清空数据

popupClose() {
  this.$emit("closeModal", "");
  // #ifdef MP-WEIXIN
  this.loginForm = {
    name: "",
    password: ""
  }
  this.registerForm = {
    name: "",
    password: "",
    confirPwd: "",
    email: ""
  }
  // #endif
  this.$refs.uLoginForm.resetFields();
  this.$refs.uRegisterForm.resetFields();
}

微信小程序v-for中u-image组件会出现宽度设置100%无法继承撑开,可以使用/deep/ u-image来设置100%

.box-title{
  width: 100%;
  height: 200rpx;
  color: #fff;
  border-radius: 20rpx;
  font-size: 32rpx;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  /*  #ifdef  MP-WEIXIN  */
  /deep/ u-image{
    width: 100%;
  }
  /*  #endif  */
}

v-show的隐藏display:none层级不高。可以设置:style或v-if来代替v-show,组件中使用可以,页面中会出现无法隐藏

<view class="classify-empty" v-if='tagLsit.length == 0'>
  <u-empty mode="list" text='文章分类数据为空' margin-top='100'></u-empty>
</view>

数组arr设置v-for中的:key为item时,当数组新增元素时,点击事件item传值获取不对,会错位,所以把key修改成index,当数组对象中存在index为key时,不能设置:key为index

<view class="list-item" v-for='(item,index) in historyList' :key='index'>
  <u-tag  :text="item" @click='itemClick(item)' type="info" shape='circle' />
</view>

微信小程序不支持v-html,考虑兼容性,所以使用rich-text富文本代替

<view class='box-title u-line-1'>
  <rich-text :nodes="item.title"></rich-text>
</view>

说明

  • 项目通过 vue-cli 命令行方式创建

  • 安卓APP通过 HBuilderX 工具打包

建立安装

# 安装依赖
$ npm install

# web端开发环境
$ npm run dev

# web端打包生产环境
$ npm run build

# 微信小程序端打包生产环境
$ npm run build:mp-weixin

微信小程序

小程序

项目地址:

前台展示:https://gitee.com/sdj_work/blog-page(Vue/Nuxt/uni-app)

管理后台:https://gitee.com/sdj_work/blog-admin(Vue/React)

后端Node:https://gitee.com/sdj_work/blog-node(Express/Koa)

博客地址:https://sdjBlog.cn/

项目系列文章:

Vue+Nuxt 博客展示

Vue+uniapp 博客展示

Vue+ElementUI 后台博客管理

node + koa + mongodb 博客接口开发

node + express + mongodb 博客接口开发