基于 UniAPP 社区论坛项目多端开发实战

这篇具有很好参考价值的文章主要介绍了基于 UniAPP 社区论坛项目多端开发实战。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

基于 UniAPP 社区论坛项目多端开发实战

学习目的:

​ 通过学习,对标企业级「大前端」开发实践标准,从零到一构建「社区论坛类」客户端应用项目全流程, 输出相关 Web H5、微信小程序、安卓 APP、IOS APP 多客户端应用。

基于 UniAPP 社区论坛项目多端开发实战,Web端+HTML+CSS,uni-app

一、项目准备

1.1 ThinkSNS 简介及相关文档

ThinkSNS 简介

ThinkSNS 是智士软件旗下的开源微博、社交系统,含微博、论坛、资讯、频道、商城、活动、定位、找人、直播、问答等一系列功能。

ThinkSNS 采用 PHP+MySQL 技术,核心(微博、用户系统)+ 多应用 + 多插件的产品模式,拥有Web、H5、IOS、Android APP以及微信、支付宝、百度等小程序多端服务场景。

ThinkSNS 服务

ThinkSNS 能够建立社交网站、微博平台、教育平台、商城系统、粉丝社区、知识社区、企业文化社区等多种互联网平台,是移动互联网创业的首选二次开发平台。

社区、论坛实践项目简介

  1. 基于 ThinkSNS 开源免费体系,搭建行业内基础 PGC、UGC 内容管理社区、论坛服务体系。
  2. 商家运营人员可以发布管理 PGC 资讯,管理用户、动态、广告等操作
  3. 客户端用户可以查阅、点赞、评论、分享相关动态资讯内容,可以自主发布、删除 UGC 内容、同步 UGC 内容审核

相关文档

  1. API 接口文档,详情查阅《 ThinkSNS 社区服务接口文档.md 》
  2. ThinkSNS 综合管理系统,开源代码 git 地址:https://gitee.com/joysapp/thinksns-plus
  3. 客户端小程序源代码 git 地址:https://gitee.com/buhehpc/jxsns.git

1.2 使用 UniAPP 构建项目

  1. 使用 Hbuilder uni-app 默认空白模板构建项目 study
  1. 在 manifest.json 文件中配置微信小程序相关信息

1.3 构建项目文件结构

  1. 构建项目基础结构
  1. 初始化基础页面及配置 pages.json Pages 路由
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
		{
			"path": "pages/index/index",
			"style": {
				// custorm 取消顶部的 navBar 显示
				"navigationStyle": "custom",
				"enablePullDownRefresh": true
			}
		}, {
			"path": "pages/feeds/feeds",
			"style": {
				"navigationBarTitleText": "动态中心",
				"enablePullDownRefresh": true
			}

		}, {
			"path": "pages/me/me",
			"style": {
				"navigationStyle": "custom",
				"enablePullDownRefresh": true
			}

		}
]

1.4 配置页面 TabBar 导航

"tabBar": {
		"color": "#000",
		"selectedColor": "#0050FF",
		"list": [{
				"iconPath": "/static/tabbar-icons/index.png",
				"selectedIconPath": "/static/tabbar-icons/index_s.png",
				"text": "首页",
				"pagePath": "pages/index/index"
			},
			{
				"iconPath": "/static/tabbar-icons/feeds.png",
				"selectedIconPath": "/static/tabbar-icons/feeds_s.png",
				"text": "动态",
				"pagePath": "pages/feeds/feeds"
			},
			{
				"iconPath": "/static/tabbar-icons/me.png",
				"selectedIconPath": "/static/tabbar-icons/me_s.png",
				"text": "我的",
				"pagePath": "pages/me/me"
			}
		]
}

1.5 使用 npm 引入 uView UI 插件库

  1. 使用 HBuilder 导入插件 uViewUI 或者使用 npm 安装相关依赖(推荐使用 npm 安装)
// 如果您的项目是HX创建的,根目录又没有package.json文件的话,请先执行如下命令:
npm init -y
// 安装
npm install uview-ui
// 更新
npm update uview-ui
  1. main.js引入uView库
// main.js
import uView from 'uview-ui';
Vue.use(uView);
  1. 编辑器安装相关依赖 工具 — 插件安装 — scss 编译支持
  1. App.vue引入基础样式
/* App.vue */
<style lang="scss">
@import "uview-ui/index.scss";
</style>
  1. uni.scss引入全局scss变量文件
/* uni.scss */
@import "uview-ui/theme.scss";
  1. pages.json配置easycom规则(按需引入)
// pages.json
{
    "easycom": {
        // 下载安装的方式需要前面的"@/",npm安装的方式无需"@/"
        // "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
        // npm安装方式
        "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
    },
    // 此为本身已有的内容
    "pages": [
        // ......
    ]
}

二、首页功能实现

2.1 首页 header 广告位轮播图功能实现

关键技术点:

  1. swiper 轮播器组件的使用
  2. 使用 this.$u.get/post 异步请求数据
  3. 使用 v-for 循环遍历渲染轮播内容
<!-- 页面 header 相关部分 -->
<view class="header-box">
		<!-- 顶部广告位轮播图 -->
		<swiper class="swiper" :indicator-dots="false" :autoplay="true" :interval="2500" :duration="500">
				<swiper-item v-for="item in swiperAdList" :key="item.id">
					<navigator open-type="navigate" :url=" '/pages/webview/webview?url='+item.link">
						<image class="banner-swiper-img" :src="item.image" mode="aspectFill" />
					</navigator>
				</swiper-item>
		</swiper>
		<!-- 遮罩使用弧形框 -->
		<image class="crile" src="@/static/crile.png" mode="aspectFill" />
		<!-- 两个选项按钮 -->
		<view class="card-header">
				<view class="card-one card-left" @tap="gotoFeeds('/pages/feeds/feeds')">
					<image class="img" src="@/static/coffee.png" mode="aspectFill" />
					<view class="iright">
						<view class="title">精彩动态</view>
					</view>
				</view>
				<view class="card-one card-right" @tap="gotoFeeds('/pages/me/me')">
					<image class="img" src="@/static/ran.png" mode="aspectFill" />
					<view class="iright">
						<view class="title">个人中心</view>
					</view>
				</view>
		</view>
			<!-- Tab 选项卡 -->
		<view class="tabs-box">
		<view class="one-nav" :class="currentSwiperIndex === 0 ? 'nav-actived' : '' " @tap="swiperChange(0)">推荐</view>
		<view class="one-nav" :class="currentSwiperIndex === 1 ? 'nav-actived' : '' " @tap="swiperChange(1)">资讯</view>
	</view>
</view>
<script>
			// 请求 广告轮播图信息
			async getAdverts() {
				let adverts = await this.$u.api.getAdvert({
					space: '1,2,3'
				})
				this.swiperAdList = adverts.data.map(item => {
					return {
						id: item.id,
						link: item.data.link,
						image: item.data.image
					}
				})
			}
</script>

2.2 全局配置 WebView 展示第三方 web 页面

关键技术点:我们通常为整个应用的 Web H5 相关页面,构建一个专门用来展示的 webview 路由页面,但是要注意路由传参的时候,对应的 url 需要 decodeURI 、encodeURI

<template>
	<web-view :src="url" />
</template>
<script>
	export default {
		data() {
			return {
				url:''
			}
		},
		// 接收传递过来的参数地址,将 web-view 相关页面展开集合到一个地方
		onLoad(options){
			this.url = decodeURI(options.url)
		}
	}
</script>

2.3 uViewUI 网络 http 请求及 API 集中管理

  1. 请求头、拦截、返回 /common/http.interceptor.js 相关配置
// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token变量
const install = (Vue, vm) => {
	// 此为自定义配置参数,具体参数见上方说明
	Vue.prototype.$u.http.setConfig({
		baseUrl: 'http://47.115.83.135/api/v2', // 请求的本域名
		dataType: 'json', // 设置为json,返回后会对数据进行一次JSON.parse()
		showLoading: true, // 是否显示请求中的loading
		loadingText: '请求中...', // 请求loading中的文字提示
		loadingTime: 800, // 在此时间内,请求还没回来的话,就显示加载中动画,单位ms
		originalData: true, // 是否在拦截器中返回服务端的原始数据
		loadingMask: true, // 展示loading的时候,是否给一个透明的蒙层,防止触摸穿透
		// 配置请求头信息
		header: {
			'content-type': 'application/json;charset=UTF-8'
		},
	});
	
	// 请求拦截,配置Token等参数
	Vue.prototype.$u.http.interceptor.request = (config) => {
		// 引用token
		// 方式一,存放在vuex的token,假设使用了uView封装的vuex方式
		// 见:https://uviewui.com/components/globalVariable.html
		// config.header.token = vm.token;
		
		// 方式二,如果没有使用uView封装的vuex方法,那么需要使用$store.state获取
		// config.header.token = vm.$store.state.token;
		
		// 方式三,如果token放在了globalData,通过getApp().globalData获取
		// config.header.token = getApp().globalData.username;
		
		// 方式四,如果token放在了Storage本地存储中,拦截是每次请求都执行的
		// 所以哪怕您重新登录修改了Storage,下一次的请求将会是最新值
		// const token = uni.getStorageSync('token');
		// config.header.token = token;
		config.header.Token = 'xxxxxx';
		
		// 可以对某个url进行特别处理,此url参数为this.$u.get(url)中的url值
		// if(config.url == '/user/login') config.header.noToken = true;
		// 最后需要将config进行return
		return config;
		// 如果return一个false值,则会取消本次请求
		// if(config.url == '/user/rest') return false; // 取消某次请求
	}
	
	// 响应拦截,判断状态码是否通过
	Vue.prototype.$u.http.interceptor.response = (res) => {
		if(res.code == 401) {
			// 如果返回false,则会调用Promise的reject回调,
			// 并将进入this.$u.post(url).then().catch(res=>{})的catch回调中,res为服务端的返回值
			vm.$u.toast('当前请求 API 接口不存在');
			return false;
		} else {
			// res为服务端返回值,可能有code,result等字段
			// 这里对res.result进行返回,将会在this.$u.post(url).then(res => {})的then回调中的res的到
			// 如果配置了originalData为true,请留意这里的返回值
			return res;
		}
	}
}

export default {
	install
}
  1. 请求 API 集中管理 /common/http.api.js
// 此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分:
const install = (Vue, vm) => {
	let api = {}
	// 获取广告位列表信息
	api.getAdvert = params => vm.$u.get('/advertisingspace/advertising', params)
	
	// 获取动态列表信息
	api.getFeeds = params => vm.$u.get('/feeds', params)
	
	// 获取资讯列表信息
	api.getNews = params => vm.$u.get('/news', params)
	
	// 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
	vm.$u.api = api
}

export default {
	install
}
  1. 在 main.js 中配置,插件注册第二个参数 app 要传入到实例中去,所以要注意引入顺序
const app = new Vue({
    ...App
})
// http拦截器,此为需要加入的内容,如果不是写在common目录,请自行修改引入路径
import httpInterceptor from '@/common/http.interceptor.js'
// 这里需要写在最后,是为了等Vue创建对象完成,引入"app"对象(也即页面的"this"实例)
Vue.use(httpInterceptor, app)
// http接口API集中管理引入部分
import httpApi from '@/common/http.api.js'
Vue.use(httpApi, app)
app.$mount()

2.4 首页「推荐、资讯」平滑切换实现

关键技术点:

  1. 使用轮播器,不自动播放,来构建一个左右滑动切换页面的效果
  2. 需要注意的是,轮播器的高度要设置一个固定值
<!-- 内容轮播导航实现 -->
<swiper  class="swiper-box" style="height:1000upx" :current="0">
	<!-- 推荐动态实现 -->
	<swiper-item class="swiper-item sns-now">
				...动态页面瀑布流展示
	</swiper-item>
	<!-- 资讯列表实现 -->
	<swiper-item class="swiper-item sns-news">
				...资讯页面列表展示
	</swiper-item>
</swiper>

2.5 首页「推荐」瀑布流布局效果实现

瀑布流布局其核心是基于一个网格的布局,而且每行包含的项目列表高度是随机的(随着自己内容动态变化高度),同时每个项目列表呈堆栈形式排列,最为关键的是,堆栈之间彼此之间没有多余的间距差存大。

实现瀑布流的解决方案,可以使用 CSS3 布局来实现,也可以使用 JS 脚本来实现,使用 CSS3 实现代码如下:

/*
  这里的关键属性是column-count,设置列数为2。然后是break-inside:avoid,为了控制文本块分解成单独的列,以免项目列表的内容跨列,破坏整体的布局,这样就实现了瀑布流布局。
*/
.waterfall {
     width: 80%;
     margin: 0 auto;
     /* 瀑布流容器内元素的间隔 */
     column-gap:10px;
     /* 瀑布容器内排列的列数 */
     column-count: 2;
}
.item {
     padding: 10px;
     margin-bottom: 10px;
     /* avoid避免在主体框中插入任何中断(页面,列或区域) */
 		 break-inside:avoid
}

使用 CSS3 实现瀑布流布局,性能高于js,缺点是用户体验差,比如:移除数据、更新数据会造成整个页面结构的复杂变化,让用户突然失去焦点的感觉非常不友好!所以使用 js 实现优势也是比较明显的,我们这里使用 uViewUI 的瀑布流插件来实现

<u-waterfall v-model="flowList" ref="uWaterfall">
	<template v-slot:left="{leftList}">
		<view v-for="(item, index) in leftList" :key="index">
			<!-- 这里编写您的内容,item为您传递给v-model的数组元素 -->
		</view>
	</template>
	<template v-slot:right="{rightList}">
		<view v-for="(item, index) in rightList" :key="index">
			<!-- 这里编写您的内容,item为您传递给v-model的数组元素 -->
		</view>
	</template>
</u-waterfall>
<script>
export default {
    methods:{
       remove(id) {
          this.$refs.uWaterfall.remove(id);
        },
        clear() {
          this.$refs.uWaterfall.clear();
        }
    }
}
</script>

PS :swiper 实现 滑动切换页面的效果需要设置一个固定的高度值,但是咱们瀑布流的高度是未知的,那么该怎么办呢?iViewUI 中的 u-waterfall 源码分析,我们要通过全局事件设置的方式来动态的设置 高度,同时,咱们这个定制化的 u-waterfall 需要单独再拿出来进行使用,拓展咱们定制化的插件

  1. 在 uViewUI 中创建一个自定义使用的 u-waterfall-sns.vue 插件,主要分为三步:

    • 在 uViewUI 组件库 components 中找到 u-waterfall 文件,复制一份命名为u-waterfall-sns
    • 文件内 u-waterfall.vue 插件,复制一份命名为u-waterfall-sns.vue 插件
    • u-waterfall-sns.vue 插件内,name 属性更改为 u-waterfall-sns,就可以调用咱们拓展出来的插件了
  2. 使用 uni.$on 设置全局事件,在 u-waterfall-sns.vue 插件内部瀑布流渲染完毕后触发这个事件,动态修改首页轮播切换页面内的 swiper 高度

    data() {
    	return {
    		// 轮播器高度
    		swiperSliderHeight: '500px'
    	}
    },
    async onLoad() {
    		// 根据瀑布流计算的高度设置全局事件,动态修正页面滑动轮播器高度
    		uni.$on('swiperHeightChange', height =>{
    			console.log(height)
    			this.swiperSliderHeight = height
    		})
    },
    
  3. 设置 image 组件的 mode 属性为 widthFil ,让动态瀑布流错落有致的状态更友好

2.6 首页「推荐、资讯」轮播切换动态设置轮播器高度

关键技术点:切换 资讯、动态 的时候,轮播器切换页面的高度是需要动态调整的,我们需要监听行为进行调整

export default {
		data() {
			return {
				// 当前 推荐 资讯 滑动位置
				currentSwiperIndex: 0,
				// 滑动页面轮播器的高度
				swiperSliderHeight: '500px',
				swiperSliderFeedsHeight: 0,
				swiperSliderNewsHeight: 0
			}
		},
		async onLoad() {
			// 在这里注册一个 uniAPP 的顶层事件,用来作为数据通信
			uni.$on("swiperHeightChange", height => {
				this.swiperSliderFeedsHeight = height
				this.swiperSliderHeight = height
			})

			// 我们要在这里初始化请求相关数据
			this.getAdverts()
			this.getFeedsList()
			this.getNewsList()
			
		},
		methods: {
			// 请求 广告轮播图信息
			async getAdverts(){},
			// 请求 feeds 列表数据
			async getFeedsList(){},
			// 请求资讯列表数据
			async getNewsList(){
				let news = await this.$u.api.getNews()
				let newsList= news.data.map(item => {
					console.log(timeFrom(new Date(item.created_at)))
					return {
						...item,
						cover: this.BaseFileURL + item.image.id
					}
				})
				
				this.newsList = [...this.newsList , ...newsList]
				this.swiperSliderNewsHeight = this.newsList.length * 95 + 100 + 'px'
				this.swiperSliderHeight = this.swiperSliderNewsHeight
			},
      	 // 页面滑动左右分页的时候实现的效果
			swiperSlider(event){
				if(event.detail.current === 0){
					this.swiperSliderHeight = this.swiperSliderFeedsHeight
				}else{
					this.swiperSliderHeight = this.swiperSliderNewsHeight
				}
				this.currentSwiperIndex = event.detail.current
			},
			// 点击按钮实现切换效果
			swiperChange(index){
				if(index === 0){
					this.swiperSliderHeight = this.swiperSliderFeedsHeight
				}else{
					this.swiperSliderHeight = this.swiperSliderNewsHeight
				}
				this.currentSwiperIndex = index
			}
		}
}

2.7 首页滚动状态下动态设置「推荐、资讯」位置及 NavBar 状态

在插件市场找到一个功能比较丰富的 NavBar 插件 https://ext.dcloud.net.cn/plugin?id=813

PS:注意,导入别人的插件后,咱们先体验体验,在使用的时候,尽量去看看插件的源码,以便于我们对当前使用插件更深层次的理解和掌握

第一步:导入和使用插件

// main.js 中全局引入插件 uni-nav-bar
import uniNavBar from "@/components/uni-nav-bar.vue"
Vue.component("uni-nav-bar", uniNavBar);
<!-- 顶部导航 -->
<uni-nav-bar v-if="navBarShowTag">
			<view class="tabs-box">
				<view class="one-nav" :class="currentSwiperIndex === 0 ? 'nav-actived' : '' " @tap="swiperChange(0)">推荐</view>
				<view class="one-nav" :class="currentSwiperIndex === 1 ? 'nav-actived' : '' " @tap="swiperChange(1)">资讯</view>
			</view>
</uni-nav-bar>
// 根据滚动状态动态显示隐藏导航栏
data() {
			return {
				// navBar 显示状态控制
				navBarShowTag: false,
        // 记录 推荐滚动 所在的位置
				oldFeedsScrollTop: 0,
				// 记录 资讯滚动 所在位置
				oldNewsScrollTop: 0
			}
}

第二步:监听页面滚动事件,判断滚动显示和隐藏 navBar 导航状态

// 监听滚动事件,动态显示隐藏 Navbar
onPageScroll(event) {
			if (event.scrollTop > 220) {
				this.navBarShowTag = true
			} else {
				this.navBarShowTag = false
			}
}

第三步:记录滚动状态下「动态、资讯」位置

// 监听滚动事件,记录滚动位置
onPageScroll(event) {
			if (this.currentSwiperIndex === 0) {
				this.oldFeedsScrollTop = event.scrollTop
			} else {
				this.oldNewsScrollTop = event.scrollTop
			}
}

第四步:切换「动态、资讯」滚动到指定记录位置

// 页面滑动左右分页的时候实现的效果
swiperSlider(event) {
				if(event.detail.current === 0) {
					this.swiperSliderHeight = this.swiperSliderFeedsHeight
					uni.pageScrollTo({
						duration: 0, //过渡时间必须为0,uniapp bug,否则运行到手机会报错
						scrollTop: this.oldFeedsScrollTop, //滚动到目标位置
					})
				} else {
					this.swiperSliderHeight = this.swiperSliderNewsHeight
					uni.pageScrollTo({
						duration: 0, //过渡时间必须为0,uniapp bug,否则运行到手机会报错
						scrollTop: this.oldNewsScrollTop, //滚动到目标位置
					})
				}
				this.currentSwiperIndex = event.detail.current
},
// 点击按钮实现切换效果
swiperChange(index) {
				if (index === 0) {
					this.swiperSliderHeight = this.swiperSliderFeedsHeight
					uni.pageScrollTo({
						duration: 0, //过渡时间必须为0,uniapp bug,否则运行到手机会报错
						scrollTop: this.oldFeedsScrollTop, //滚动到目标位置
					})
				} else {
					this.swiperSliderHeight = this.swiperSliderNewsHeight
					uni.pageScrollTo({
						duration: 0, //过渡时间必须为0,uniapp bug,否则运行到手机会报错
						scrollTop: this.oldNewsScrollTop, //滚动到目标位置
					})
				}
				this.currentSwiperIndex = index
}

2.8 顶部下拉、底部置底请求更新数据

关键技术点:

  1. onReachBottom、onPullDownRefresh 全局方法调用
  2. this.$refs.waterfall.clear() 调用插件对应方法清空数据流
// 下拉到底请求新的数据
onReachBottom() {
			// 请求新的数据
			if (this.currentSwiperIndex === 0) {
				this.getFeeds()
			} else {
				this.getNews()
			}
},
// 顶部下拉请求新数据
onPullDownRefresh() {
			this.feedsList = []
			this.$refs.waterfall.clear()
			if (this.currentSwiperIndex === 0) {
				this.getFeeds()
			} else {
				this.getNews()
			}
},
methods: {
			// 获取动态信息
			async getFeeds() {
				let feeds = await this.$u.api.getFeeds()
				let feedsList = feeds.feeds.map(item => {
					return {
						...item,
						cover: this.BaseFileURL + item.images[0].file,
						avatar: !!item.user.avatar ? item.user.avatar.url : '/static/nopic.png',
						name: item.user.name,
					}
				})
				this.feedsList = [...this.feedsList, ...feedsList]
				// 在这里注册一个 uniAPP 的顶层事件,用来作为数据通信
				uni.$once("swiperHeightChange", height => {
					console.log('瀑布流计算出来轮播器高度为:' + height)
					this.swiperSliderFeedsHeight = height + 'px'
					this.swiperSliderHeight = this.swiperSliderFeedsHeight
				})
			},
			// 获取咨询列表信息
			async getNews() {
				let news = await this.$u.api.getNews()
				let newsList = news.map(item => {
					return {
						...item,
						cover: this.BaseFileURL + item.image.id
					}
				})
				this.newsList = [...this.newsList, ...newsList]
				this.swiperSliderNewsHeight = (this.newsList.length * 95 + 120) + 'px'
				this.swiperSliderHeight = this.swiperSliderNewsHeight
			}
}

三、资讯动态相关页面功能实现

3.1 使用 Grid 栅格布局实现动态瀑布流页面

技术关键点:

  1. 动态请求数据,并对数据进行加工处理成为我们期望的 分组数组 目标数据
  2. grid 栅格化布局
  3. 顶部下拉刷新更新数据
  4. 滚动置底请求数据
  5. 请求状态控制器
<template>
	<view class="rfeeds">
		<view class="one-feeds-box" v-for=" (feedsList, i) in showFeedsList " :key="i">
			<view v-for=" (item, k) in feedsList " 
				  :key="item.id" class="one-feed" 
				  :class="k % 6 == 0 ? ( i%2==0 ? 'feed-big-left' :'feed-big-right' ) : '' ">
				<navigator :url=" '/subpages/feedinfo/feedinfo?id=' + item.id">
					<image :src="item.cover" class="feed-content" mode="aspectFill" :lazy-load="true" />
				</navigator>
			</view>
		</view>
		<!-- 分享按钮组件 -->
		<goto-share />
	</view>
</template>
<script>
	export default {
		data() {
			return {
				// 列表数据
				feedsList: [],
				// 用来展示的栅格系统的列表数据
				showFeedsList: [],
				// 记录请求 feed 状态
				canRequestFeeds: "yes"
			};
		},
		onLoad() {
			this.getFeeds()
		},
		// 顶部下拉刷新新数据
		onPullDownRefresh() {
			this.feedsList = []
			this.getFeeds()
		},
		// 滚动置底刷新请求数据
		onReachBottom() {
			this.getFeeds()
		},
		methods: {
			// 获取请求数据
			async getFeeds() {
				if (this.canRequestFeeds === "yes") {
					uni.showToast({
						title: "请求中...",
						icon: "loading",
						duration: 10000
					});
					// 设置状态
					this.canRequestFeeds = "no"
					let res = await this.$u.api.getFeeds()
					let feeds = res.feeds.map(item => {
						return {
							id: item.id,
							cover: this.BaseFileURL + item.images[0].file
						}
					})
					if (feeds.length > 5) {
						// 记录总列表数据
						this.feedsList = [...this.feedsList, ...feeds];
						// 在这里要处理页面的内容布局模式为栅格模式,6个元素一组
						let showArrList = [];
						for (let i = 0; i < this.feedsList.length; i++) {
							if (i % 6 == 0 && !!this.feedsList[i + 5]) {
								// 返回一个新的数组,包含从 start 到 end (不包括该元素)元素。
								// 该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法 Array.splice()。
								showArrList.push(this.feedsList.slice(i, i + 6))
							}
						}
						this.showFeedsList = showArrList
						this.canRequestFeeds = "yes"
					} else {
						this.canRequestFeeds = "end"
						uni.showToast({
							title: "到底啦!",
							icon: "success",
							duration: 1000
						});
					}
				}
				// 如果请求状态到底了后,则提示到底了
				if (this.canRequestFeeds === "end") {
					uni.hideToast();
					uni.showToast({
						title: "到底啦!",
						icon: "success",
						duration: 1000
					}) 
				}
			},
		},
	};
</script>

3.2 使用分包构建业务逻辑页面

技术关键点:page.json 中配置分包业务逻辑

"subPackages": [{
		"root": "subpages",
		"pages": [{
			"path": "feedinfo",
			"style": {
				"navigationBarTitleText": "动态详情",
				"navigationBarBackgroundColor": "#FFFFFF",
				"navigationBarTextStyle": "black",
				"backgroundColor": "#FFFFFF"
			}
		}]
}],

3.3 动态详情页面功能实现

技术关键点:

  1. 分享朋友圈
wx.showShareMenu({
	withShareTicket: true,
	menus: ['shareAppMessage', 'shareTimeline']
})
  1. 图片预览
methods: {
			previewImage(index) {
				uni.previewImage({
					current: index,
					urls: this.feedInfo.images
				});
			}
}
  1. 使用日期格式化过滤器显示时间
// 引入 时间日期格式化显示函数
import timeFrom from '@/tools/timeFrom.js'
// 设置过滤器
filters: {
			timeFormate(timeDate) {
				let Time = new Date(timeDate);
				let timestemp = Time.getTime();
				let t = timeFrom(timestemp, "yyyy年mm月dd日");
				return t;
			}
},
// 调用过滤器
<text>{{ feedInfo.created_at | timeFormate }} 发布</text>

3.4 资讯详情页面富文本解析功能实现

技术关键点:

  1. 顶部毛玻璃插件的引入及使用
// 引入毛玻璃组件
import picBlur from "@/components/pic-blur/pic-blur.vue";
components: {
	picBlur
},
<!-- 顶部毛玻璃背景图 -->
<!-- 可选值 xs s m l xl -->
<pic-blur :params="{
	width:'750rpx',
	height:'520rpx',
	image: newInfo.cover,
	blur:'xs'
}"></pic-blur>
  1. 资讯详情内容 富文本内容 深度解析
import htmlParse from "@/components/html-parse/parse.vue";

<view class="info-content">
    <html-parse :content="newInfo.content" />
</view>

async onLoad(options) {
    // 分享
    wx.showShareMenu({
        withShareTicket: true,
        menus: ['shareAppMessage', 'shareTimeline']
    })
    let res = await this.$u.api.getNewInfo(options);
    res = res.data

    // let cp = res.content.replace(/@!\[(\d*).jpg\]\((\d*)\)/g,"<img src='" +this.BaseFileURL +'$2' + "' />")
    let cp = res.content.replace(/@!\[.*\]\((\d*)\)/g, "<img src='" + this.BaseFileURL + '$1' + "' />")

    this.newInfo = {
        ...res,
        cutTitle: res.title.length > 11 ? res.title.substring(0, 11) + "..." : res.title,
        cover: this.BaseFileURL + res.image.id,
        userId: res.user_id,
        content: cp,
        views_count: res.hits
    }
    this.getRequestOK = true;
}
  1. NavBar 根据滚动动态显示
<!-- 顶部导航 -->
<uni-nav-bar backState="2000" fontColor="#FFF" :titleCenter="false" type="transparent">
	<view slot="left">
		<image class="hicon" src="/static/home.png" mode="aspectFit" @tap="goHome" />
	</view>
</uni-nav-bar>

<uni-nav-bar v-if="navBarShow" backState="1000" :titleCenter="false">
  {{ newInfo.cutTitle }}
</uni-nav-bar>

data() {
  return {
	 	// 是否显示 navbar
	 	navBarShow: false
	};
},
onPageScroll(res) {
	if (res.scrollTop > 100) {
		this.navBarShow = true;
	} else {
		this.navBarShow = false;
	}
}

3.5 评论组件评论列表展示功能实现

关键技术点:使用 props 接收到的数据可以直接渲染到页面上,但是 props 接收到的数据如果修改后,则不会动态触发 对应 dom 结构的内容更新,我们需要做一个变量去保存 props 传递的初始化值然后基于此变量更新页面结构和内容

<template>
	<view>
		<!-- 评论列表 -->
		<view class="comments">
			<view class="title gohere" id="gohere" ref="gohere">最新评论</view>
			<view class="no-comment" v-if="commintsList.length == 0">暂无评论</view>
			<view v-for="(commentItem, index) in commintsList" :key="index" class="one-comment">
				<!-- 一级评论相关 -->
				<view class="commenter">
					<view class="info">
						<view class="left">
							<u-avatar size="50" class="avatar" :src="!!commentItem.user.avatar ? commentItem.user.avatar.url : '' " />
							<view class="name">{{ commentItem.user.name }}</view>
						</view>
					</view>
					<view class="content">{{ commentItem.body }}</view>
					<view class="uptime">{{ commentItem.created_at | timeFormate }} 评论</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
	import timeFrom from "@/tools/timeFrom.js";
	export default {
		props: {
			oneInfo: Object,
			type: String,
		},
		data() {
			return {
				// 当前动态评论列表详情
				commintsList: [],
				// props 传递的数据无法改变触发 DOM 更新
				oneInfoClone: []
			};
		},
		async created() {
			this.oneInfoClone = this.oneInfo
			this.getCommentsList();
		},
		methods: {
			// 获取评论列表
			async getCommentsList() {
				if (this.type === "feed") {
					// 获取当前动态评论列表信息
					let res = await this.$u.api.getFeedComments({
						id: this.oneInfo.id
					});
					this.commintsList = res.data.comments;
				} else {
					// 获取当前资讯评论列表信息
					let res = await this.$u.api.getNewComments({
						id: this.oneInfo.id
					});
					this.commintsList = res.data.comments;
				}
			}
		},
		// 过滤器
		filters: {
			timeFormate(timeDate) {
				let Time = new Date(timeDate);
				let timestemp = Time.getTime();
				let t = timeFrom(timestemp, "yyyy年mm月dd日");
				return t;
			},
		},
	};
</script>

四、用户注册、登陆业务功能实现

4.1 用户登陆、注册功能组件实现及触发显示

关键技术点:

  1. 获取当前微信用户的 昵称 和 头像信息
  2. 使用 u-form 实现登陆、手机注册、邮箱注册基础逻辑
  3. 使用 u-form 实现表单验证相关逻辑
<template>
	<view class="login" :class="{show:show}">
			<button class="submit-btn" open-type="getUserInfo" @getuserinfo="getWechatUserInfo" v-if="getUserInfoTag">
				<image src="/static/wechat.png" class="wechat-img" />
				<text>一键获取微信信息</text>
			</button>

			<u-form :model="form" ref="uForm" v-show="!getUserInfoTag">
				<u-form-item label="账号" prop="login" label-width="150" required v-if="loginType === 'login'">
					<u-input v-model="form.login" placeholder='输入手机号/邮箱/昵称' />
				</u-form-item>
				<u-form-item label="密码" label-width="150" required prop="password">
					<u-input v-model="form.password" type="password" placeholder='限4-20个字符,区分大小写' />
				</u-form-item>
				<view class="btns">
					<u-button class="ubtn" @click="submit">提交</u-button>
					<u-button class="ubtn" @click="cancel">取消</u-button>
				</view>
				<view class="type">
					<u-subsection active-color="#007cba" font-size="24" height="52" :list="subsectionList" :current="0" @change="sectionChange"></u-subsection>
				</view>
			</u-form>
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				// 登陆组件是否显示
				show: false,
				// 登陆方式
				loginType: 'login',
				// 登陆方式选择器
				subsectionList: [{	name: '账号登陆'},
					{	name: '手机注册'	},
					{	name: '邮箱注册'	}
				],
				// 是否获取用户信息
				getUserInfoTag: true,
				// 用户信息输入框
				form: {
					login: '',
					avatar: '',
					name: '',
					phone: '',
					email: '',
					code: '',
					password: '',
					repassword: ''
				},
				// 用户输入规则校验
				rules: {
					login: [{
						validator: (rule, value, callback) => {
							if (this.loginType === 'login') {
								return !this.$u.test.isEmpty(value)
							} else {
								return true
							}
						},
						message: '必填 * 输入内容不许为空',
						trigger: ['change', 'blur'],
					}],
					name: [{
							asyncValidator: (rule, value, callback) => {
								this.$u.api.findUser({
									name: value
								}).then(res => {
									// 如果验证不通过,需要在callback()抛出new Error('错误提示信息')
									if (!!value && res.statusCode === 200) {
										callback(new Error('当前用户昵称已存在'));
									} else {
										// 如果校验通过,也要执行callback()回调
										callback();
									}
								})
							},
							trigger: ['blur'],
						}
					],
         // ... 其他验证规则
				}
			}
		},
		// 必须要在onReady生命周期设置校验规则,因为onLoad生命周期组件可能尚未创建完毕
		onReady() {
			this.$refs.uForm.setRules(this.rules);
		},
		async created() {
			// 每次这个组件展开,我们就去判断一下当前 token 是否可以获取新的 token 如果可以获取,咱们就关闭登陆状态
			let res = await this.$u.api.getUserMsg()
			// 点赞消息数量
			if (res.statusCode === 200) {
				this.show = false
				return
			} else {
				this.show = true
			}

			wx.getSetting({
				success: res => {
					if (res.authSetting["scope.userInfo"]) {
						uni.getUserInfo({
							success: res => {
								// 如果用户授权了,则做两件事,第一件事
								this.form.login = res.userInfo.nickName
								this.form.name = res.userInfo.nickName
								this.form.avatar = res.userInfo.avatarUrl
								this.getUserInfoTag = false
							},
							fail: () => {
								console.log('用户未授权!')
							}
						})
					}
				}
			})
		},
		methods: {
			// 关闭弹窗
			closeLogin() {
				this.show = false
			},
			// 打开登陆弹窗
			openLogin() {
				this.show = true
			},
			// 跳转到 H5 页面
			gotoWeb(url) {
				wx.navigateTo({
					url: '/pages/webview/webview?url=' + encodeURI(url)
				});
			},
			// 微信授权
			getWechatUserInfo() {
				uni.getUserInfo({
					success: res => {
						// 进行下一步操作
						this.getUserInfoTag = false
						this.form.login = res.userInfo.nickName
						this.form.name = res.userInfo.nickName
						this.form.avatar = res.userInfo.avatarUrl
					},
					fail: () => {
						console.log('用户未授权!')
					}
				})
			},
			// 获取验证码
			getCode() {
				uni.showModal({
					title: '验证码获取成功',
					content: '8888'
				})
			},
			// 取消表单输入
			cancel() {
				this.form = {
					login: '',
					name: '',
					phone: '',
					email: '',
					code: '',
					password: '',
					repassword: ''
				}
			},
			// 提交表单验证
			submit() {
				this.$refs.uForm.validate(async valid => {
					if (!valid) {
						uni.showToast({
							title: '请检查输入',
							icon: 'loading'
						})
						return false
					}
					switch (this.loginType) {
						case "login":
							let resa = await this.$u.api.userLogin({
								login: this.form.login,
								password: this.form.password
							})
							if (resa.statusCode === 200) {
								// 登陆成功
								this.loginAfter(resa.data.access_token)
							} else {
								uni.showModal({
									title: '登陆失败',
									content: resa.data.message
								})
							}
							break;
						case "phone":
							let resb = await this.$u.api.userRegister({
								// 必须,用户名
								name: this.form.name,
								// 必须,验证码发送模式。
								verifiable_type: 'sms',
								// 必须,用户收到的验证码。(教学阶段验证码统一使用 8888 )
								verifiable_code: '8888',
								// 如果 `verifiable_type` 为 `sms` 则必须, 手机号码。
								phone: this.form.phone,
								// 可选,密码,如果不输入密码,允许用户无密码注册。
								password: this.form.password
							})
							console.log(resb)
							if (resb.statusCode === 201) {
								// 登陆成功
								this.loginAfter(resb.data.token)
							} else {
								uni.showModal({
									title: '登陆失败',
									content: resb.data.message
								})
							}
							break;
						case "email":
							let resc = await this.$u.api.userRegister({
								// 必须,用户名
								name: this.form.name,
								// 必须,验证码发送模式。
								verifiable_type: 'mail',
								// 必须,用户收到的验证码。(教学阶段验证码统一使用 8888 )
								verifiable_code: '8888',
								// 如果 `verifiable_type` 为 `mail` 则必须, E-Mail。
								email: this.form.email,
								// 可选,密码,如果不输入密码,允许用户无密码注册。
								password: this.form.password
							})
							console.log(resc)
							if (resc.statusCode === 201) {
								// 登陆成功
								this.loginAfter(resc.data.token)
							} else {
								uni.showModal({
									title: '登陆失败',
									content: resc.data.message
								})
							}
							break;
						default:
							uni.showToast({
								title: '未知用户状态',
								icon: 'loading'
							})
							break;
					}
				});

			},
			// 注册、登陆成功后设置相关逻辑
			async loginAfter(token) {
				this.show = false
				uni.setStorageSync('token', token)

				// 获取未读消息提示
				let res = await this.$u.api.getUserMsg()
				let name = this.form.name
				if (this.loginType === 'login') {
					name = this.form.login
				}
				let loginInfo = {
					name,
					avatar: this.form.avatar,
					liked: res.data.user.liked,
					commented: res.data.user.commented
				}
				this.userLoginAction(loginInfo)
				uni.$emit('userLogin')
			},
			// 更改 登陆 注册 方式选择
			sectionChange(index) {
				switch (index) {
					case 1:
						this.loginType = "phone";
						break;
					case 2:
						this.loginType = "email";
						break;
					default:
						this.loginType = "login";
						break;
				}
			}

		}
	}
</script>

4.2 Vuex + Storage 登陆状态管理

关键技术点:

  1. 构建 vuex 注册 main.js 全局,并配合使用 storage 实现刷新后数据保存
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
	// 这个代表全局可以访问数据对象,就像是咱们在组件中声明的 data 属性
	state: {
		loginState: !!uni.getStorageSync('loginState') ? true : false,
		userInfo: !!uni.getStorageSync('userInfo') ? JSON.parse(uni.getStorageSync('userInfo')) : {
			name: '未知用户',
			avatar: '/static/nopic.png',
			liked: 0,
			commented: 0
		}
	},
	// 这个实时监听 state 内的数据对象变化,类似 咱们组件中的 computed 属性,会依赖 state 数据变化而变化
	getters: {

	},
	// 用来同步设置 state 的值
	mutations: {
		userLogin(state, userInfo) {
			state.loginState = true
			state.userInfo = userInfo
			uni.setStorageSync('loginState', 'ok')
			uni.setStorageSync('userInfo', JSON.stringify(userInfo))
		},
		userLogout(state) {
			state.loginState = false
			state.userInfo = {
				name: '未知用户',
				avatar: '/static/nopic.png',
				liked: 0,
				commented: 0
			}
			uni.clearStorageSync('userInfo')
			uni.clearStorageSync('loginState')
			uni.clearStorageSync('token')
		}
	},
	// 通过提交 mutations 内部的方法,异步更新 state 的状态,官方推荐都使用这种方法比较合适
	actions: {
		userLoginAction(context, userInfo) {
			context.commit('userLogin', userInfo)
		},
		userLogoutAction(context) {
			context.commit('userLogout')
		}
	}
})
export default store
  1. 使用 mapState、mapActions 实现 vuex 变量快速调用
import {
		mapState,
		mapActions
	} from 'vuex'
computed: {
	...mapState(['loginState', 'userInfo'])
},
methods: {
			...mapActions(['userLoginAction', 'userLogoutAction'])
}
  1. 使用 uni.$on $emit 触发首页及个人中心页面登录后对应的数据更新
// index onload
uni.$on('indexUserLogin', ()=>{
		this.currentSwiperIndex = 0
		this.feedsList = []
		this.$refs.waterfall.clear()
		this.getFeedsList()
})
// me onload
// 用户登录后触发数据更新
uni.$on('meUserLogin', this.getInfos)

uni.$emit('meUserLogin')
uni.$emit('indexUserLogin')

4.3 个人中心页面基础功能实现

关键技术点:

  1. 请求当前登录用户发布动态列表数据
// 获取当前用户的 动态信息
let res = await this.$u.api.getFeeds({
		type: 'users'
})
  1. 使用 瀑布流 插件实现排版布局
<u-waterfall v-model="feedsList" ref="waterfall">
		<template v-slot:left="{leftList}">
		...
		</template>
		<template v-slot:left="{leftList}">
		...
		</template>
</u-waterfall>
  1. 使用 vuex 状态数据显示用户登录状态
import {
		mapState,
		mapActions
	} from 'vuex'
computed: {
	...mapState(['loginState', 'userInfo'])
},
methods: {
			...mapActions(['userLoginAction', 'userLogoutAction'])
}
  1. 用户删除一条动态更新瀑布流并通信 index 页面同步更新
// 选择删除一条动态
async openSheet(fid) {
		uni.showActionSheet({
			itemList: ["删除"],
			success: async res => {
										console.log('删除 id 为' + fid + '的动态')
				await this.$u.api.deleteFeed({
					id: fid
				})
				this.$refs.waterfall.remove(fid);
				// 触发 首页动态 同步删除事件
										uni.$emit('indexFeedRemove', fid)
				uni.showToast({
					title: "当前动态已删除",
											duration: 1000
				})
			}
		});
},
// index 页面
// 个人中心删除一条动态后,触发更新首页数据
uni.$on("indexFeedRemove", fid =>{
				this.$refs.waterfall.remove(fid);
})

4.4 用户设置页面基础功能实现

关键技术点:

  1. 使用 vuex 状态管理器 actions 方法快速退出
  2. 使用 uni.$on $emit 触发首页及个人中心页面用户退出后对应的数据更新
// index onload
uni.$on('indexUserLogout', ()=>{
		this.currentSwiperIndex = 0
		this.feedsList = []
		this.$refs.waterfall.clear()
		this.getFeedsList()
})
// me onload
// 用户退出后触发数据更新
uni.$on('meUserLogout', () => {
				console.log('触发了退出操作')
				this.feedsList = []
				this.avatar = ''
				this.bio = ''
				this.$refs.waterfall.clear()
})
uni.$emit('meUserLogout')
uni.$emit('indexUserLogout')

五、登陆态下业务功能实现

5.1 使用 mixins 实现动态点赞、取消点赞功能

关键技术点:

  1. 使用 mixins 构建复用的动态 点赞、取消点赞 事件方法
let feedMixin = {
	methods: {
		// 点赞或者取消点赞一条动态
		async clickLove(item) {
			// 判断当前登录状态
			if (!this.loginState) {
				this.$refs.login.openLogin()
				return
			}
			// 动态点赞
			if (item.has_like) {
				--item.like_count;
				item.has_like = false;
				await this.$u.api.unlikeThisFeed({
					id: item.id,
				});
				uni.showToast({
					title: "取消点赞",
					icon: "success",
					duration: 1000,
				});
			} else {
				++item.like_count;
				item.has_like = true;
				await this.$u.api.likeThisFeed({
					id: item.id,
				});
				uni.showToast({
					title: "点赞成功",
					icon: "success",
					duration: 1000,
				});
			}
			uni.$emit('indexFeedLoveChange',item)
			uni.$emit('myFeedLoveChange',item)
		}
	}
}
export default feedMixin
  1. 调用 mixins 实现点赞功能
// index \ me 两个页面
import feedMixin from '@/mixins/todoFeed.js'
mixins: [feedMixin]
  1. 点赞完成后通信相关页面更新显示状态
// me 页面 用户点赞一条动态后触发数据更新
uni.$on('myFeedLoveChange', item => {
	this.$refs.waterfall.modify(item.id, "like_count", item.like_count);
	this.$refs.waterfall.modify(item.id, "has_like", item.has_like);
})
// index 用户点赞一条动态后触发数据更新
uni.$on('indexFeedLoveChange', item => {
	this.$refs.waterfall.modify(item.id, "like_count", item.like_count);
	this.$refs.waterfall.modify(item.id, "has_like", item.has_like);
})

// minxins 中触发通信
uni.$emit('indexFeedLoveChange',item)
uni.$emit('myFeedLoveChange',item)

5.2 登陆状态下评论组件点赞、评论功能实现

关键技术点:

  1. 登陆插件引入及登陆状态判断
<!-- 登陆组件 -->
<login ref="login"></login>
// 判断当前登录状态
if (!this.loginState) {
	this.$refs.login.openLogin()
	return
}
  1. 正常实现资讯点赞功能
// 点赞逻辑操作
async sendLove(){
    // 判断当前登录状态
    if (!this.loginState) {
        this.$refs.login.openLogin()
        return
    }
    if (this.type === "feed") {
        this.clickLove(this.oneInfoClone)
    } else {
        // 动态点赞
        if (this.oneInfoClone.has_like) {
            --this.oneInfoClone.digg_count;
            this.oneInfoClone.has_like = false;
            await this.$u.api.unlikeThisNew({
                id: this.oneInfoClone.id,
            });
            uni.showToast({
                title: "取消点赞",
                icon: "success",
                duration: 1000,
            });
        } else {
            ++this.oneInfoClone.digg_count;
            this.oneInfoClone.has_like = true;
            await this.$u.api.likeThisNew({
                id: this.oneInfoClone.id,
            });
            uni.showToast({
                title: "点赞成功",
                icon: "success",
                duration: 1000,
            });
        }
    }
}
  1. 实现评论功能,注意:评论完成后修改页面数据不能直接修改 props 中的数据,通知个人中心评论数量增加
// 发送评论信息
async sendComment(){
    // 发送状态判定
    if (this.disableSendCommentTag) return
    this.disableSendCommentTag = true

    if (this.type === 'feed') {
        await this.$u.api.commentOneFeed({
            id: this.oneInfoClone.id,
            body: this.cinput
        });
        ++this.oneInfoClone.feed_comment_count
        // 通知 个人中心当前动态评论增加
        uni.$emit('myFeedCommentChange', this.oneInfoClone)
    } else {
        await this.$u.api.commentOneInfo({
            id: this.oneInfoClone.id,
            body: this.cinput
        });
        ++this.oneInfoClone.comment_count
    }
    uni.showToast({
        title: "评论成功",
        icon: "success",
        duration: 1000,
    });
    this.cinput = ''
    this.closeComment()
    this.getCommentsList()
}

5.3 UGC 动态发布功能实现

关键技术点:

  1. 公共组件 发布按钮 ,注意用户未登录状态下不显示这个按钮
<template>
	<view class="goto-share" v-if="loginState">
		<!-- 个人分享 选择器 -->
		<image class="share-img" src="@/static/carm.png" @tap.stop="gotoShare()" mode="aspectFit" />
	</view>
</template>
<script>
	import {
		mapState
	} from 'vuex'
	export default {
		data() {
			return {};
		},
		computed: {
			...mapState(['loginState'])
		},
		methods: {
			// 底部个人分享 弹窗相关操作
			gotoShare() {
				uni.navigateTo({
					url: "/subpages/share/share",
				})
			}
		}
	};
</script>
  1. 构建 share 页面用来专门实现动态上传能力
<view class="share">
		<!-- 内容输入框 -->
		<view class="uni-textarea">
			<textarea placeholder-style="color:#AAAAAA" placeholder="添加描述..." @input="bindTextAreaInput" />
			</view>

				<!-- 内容发布 -->
		<view class="pics">
			<view class="medias" v-for="( image, index ) in uploadPicsList" :key="index">
			  <image
				class="img"
				:src="image.path"
				:data-src="image"
				@tap="previewImage(index)"
				mode="aspectFill"
			  />
			  <u-icon name="close" class="iclose" color="#eee" size="20" @tap="removeImage(index)" />
			</view>

			<!-- 选择照片按钮 -->
			<view class="uploadBtn" @tap="chosePicsAndUpload" v-if="uploadPicsList.length < 9">
			  <u-icon name="plus" size="60" color="#aaa" />
			  <view class="text">选择照片</view>
			</view>

		</view>

		<view class="btns">
		  <u-button type="primary" size="default" @click="sendFeed" :disabled="uploadStatus">发布动态</u-button>
		</view>

</view>
  1. 封装 uploadFile 公共工具
import MD5 from "@/common/md5.js";
const uploadFile = async (path, size) => {
	let uploadUrl = 'http://47.115.83.135/api/v2'
	// 第一步 获取 hash 值
	let hash = await new Promise((resolve, reject) => {
		uni.getFileInfo({
			filePath: path,
			success: (result) => {
				resolve(result.digest);
			},
		});
	});

	// console.log(MD5.md5(hash))

	// 第二步 请求获取基础文件信息
	// let check = await uni.request({
	//  url: uploadUrl+'/files/uploaded/'+MD5.md5(hash),
	//  method: 'GET',
	//  header: {
	//    "content-type": "application/json",
	//    Accept: "application/json",
	//    Authorization: "Bearer " + uni.getStorageSync("token"),
	//  }
	// })
	// 如果返回的状态码是404,那说明没有上传过,继续后面的上传流程
	// 如果返回的是200,那就从body里取出 id,这个id就可以直接使用,后面的上传流程就可以终止了
	// console.log(check[1].statusCode)

	// 第三步 将文件以buffer流方式存入系统
	let ufile = await uni.uploadFile({
		url: uploadUrl + '/files',
		method: "POST",
		header: {
			'content-type': 'multipart/form-data',
			Authorization: "Bearer " + uni.getStorageSync("token"),
		},
		name: 'file',
		file: uni.getFileSystemManager().readFileSync(path),
		filePath: path
	});

	// console.log(JSON.parse(ufile[1].data))
	return JSON.parse(ufile[1].data)
};
export default uploadFile;

  1. 使用 primose.all 来确保多张照片能够顺利上传
let upStatusArr = [];
// 循环遍历上传多图
this.uploadPicsList.map((item) => {
			    upStatusArr.push(uploadFile(item.path, item.size));
});
let images = [];
(await Promise.all(upStatusArr)).map((item) => {
			    images.push({id : item.id});
});
let pres = await this.$u.api.postOneFeed({
			    feed_content: this.feedInfo,
			    feed_from: 5,
			    feed_mark: new Date().getTime(),
			    images
});
  1. 动态发布成功后通知 index me 页面更新
// 通知个人中心 动态更新了
uni.$emit("indexFeedsUpdate")
// 通知首页 动态更新了
uni.$emit("myFeedsUpdate");
// index 发布新的动态后,触发数据更新
uni.$on("indexFeedsUpdate", ()=>{
				this.currentSwiperIndex = 0
				this.feedsList = []
				this.$refs.waterfall.clear()
				this.getFeeds()
})

// me 用户发布一条动态后触发数据更新
uni.$on('myFeedsUpdate', ()=>{
				this.$refs.waterfall.clear()
				this.getInfos()
})

六、多端打包发布

6.1 微信小程序打包发布

6.2 H5 端相关兼容性开发及打包发布

关键调整节点:

  1. manifest.json H5 下出现的跨域问题要进行相关配置
"h5" : {
    "devServer" : {
        "port" : 8000, //端口
        "disableHostCheck" : true,
        "proxy" : {
            //使用代理
            "/api" : {
                "target": "http://47.115.83.135/api/v2",
                "changeOrigin" : true,
                "pathRewrite" : {
                    "^/api" : ""
                }
            }
        }
    }
}
  1. newinfo \ feedinfo 微信分享仅仅在微信下有效
// #ifdef MP-WEIXIN
// 微信条件下分享到朋友圈、群组
wx.showShareMenu({
	withShareTicket: true,
	menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
  1. comment 组件分享按钮仅仅在 微信小程序下展示,同步 css 调整
<!-- 转发次数统计 -->
<!-- #ifdef MP-WEIXIN -->
<button class="mbtn mhare" open-type="share">
		<image class="micon" src="/static/wx.png" mode="aspectFit" />
		<text class="mtext">分享</text>
</button>
<!-- #endif -->
.minput {
			background-color: #eee;
			height: 60upx;
			// #ifdef MP-WEIXIN
			width: 300upx;
			// #endif
			// #ifndef MP-WEIXIN
			width: 440upx;
			// #endif
}
  1. login 登陆组件获取用户信息逻辑调整
// #ifdef MP-WEIXIN
			wx.getSetting({
				success: res => {
					if (res.authSetting["scope.userInfo"]) {
						uni.getUserInfo({
							success: res => {
								// 如果用户授权了,则做两件事,第一件事
								this.form.login = res.userInfo.nickName
								this.form.name = res.userInfo.nickName
								this.form.avatar = res.userInfo.avatarUrl
								this.getUserInfoTag = false
							},
							fail: () => {
								console.log('用户未授权!')
							}
						})
					}
				}
	})
// #endif
// #ifndef MP-WEIXIN
				this.getUserInfoTag = false
// #endif
  1. http.api.js API 管理器 文件上传逻辑调整
// 文件上传操作
api.uploadFile = async file =>{
    let rfile = file
    // #ifdef MP-WEIXIN
    rfile = uni.getFileSystemManager().readFileSync(file.path)
    // #endif

    // 将文件写入后台系统系统
    let ufile = await uni.uploadFile({
        url: vm.$u.http.config.baseUrl + '/files',
        header: {
            Authorization: "Bearer " + uni.getStorageSync("token"),
        },
        name: 'file',
        file: rfile,
        filePath: file.path
    });
    console.log(JSON.parse(ufile[1].data))
    return JSON.parse(ufile[1].data)
}

PS: 可以使用托管工具,快速发布体验,尤其需要注意的是:开发的时候配置的代理服务器,在正式环境打包的时候是不会被调用的,所以要更改对应的配置信息

6.3 安卓云端体验版本打包发布

文章来源地址https://www.toymoban.com/news/detail-807633.html

到了这里,关于基于 UniAPP 社区论坛项目多端开发实战的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 轻型论坛社交圈子小程序源码/Uniapp多端整合私域流量运营型社群小程序交友聊天爱好交流圈子系统源码

    🔥🔥🔥 🔥🔥🔥 多客圈子系统支持文字发帖、语音贴、视频贴等,并可以创建语音聊天、在线聊天、语音房APP,语音直播app,后台PHP管理。uniapp开源代码可打包小程序,安卓app、苹果app、H5等。快速建立社区兴趣圈、语音直播、礼物、商城、充值、宝箱、陌生社交系统

    2024年02月06日
    浏览(48)
  • 基于MYSQL的论坛管理系统数据库设计项目实战

    说明:这是一个数据库课程设计实战项目(附带 代码+文档+视频讲解 ),如需 代码+文档+视频讲解 可以直接到文章最后获取。 项目背景 随着互联网行业的发展,各种论坛纷纷而来。在论坛系统中,可让用户注册成为论坛会员,取得发表言论的资格,同时也需要论坛信息管理

    2024年02月11日
    浏览(60)
  • Reddit NFT爆火,全球最大社区论坛成为Web3大规模应用前哨站

    这是白话区块链的第 1804 期原创  作者 | 火火 出品|白话区块链(ID:hellobtc) 据Dune Analytics最新数据显示,Reddit于Polygon网络发行的NFT系列Reddit Collectible Avatar销售总量已突破9万笔,在12月7日达到94,338笔,交易总额超1100万美元。 此外,当前Reddit Avatar NFT持有地址总量为4,079,

    2023年04月24日
    浏览(51)
  • 基于微服务+Java+Spring Cloud开发的建筑工地智慧平台源码 云平台多端项目源码

    建筑工地智慧平台源码,施工管理端、项目监管端、数据大屏端、移动APP端全套源码 技术架构:微服务+Java+Spring Cloud +Vue+UniApp +MySql+自主版权+实际应用案例+演示 建筑工地智慧平台已应用于线上巡查、质量管理、实名制管理、危大工程管理、运渣车管理、绿色施工、视频监控

    2024年02月07日
    浏览(55)
  • WebStorm开发uni-app ,用vue2实现手机APP(apk) + 微信小程序多端项目开发方案 webstorm开发的uniapp + hbuilderx进行app 小程序打包

    我们主要分析了如下小程序开发框架,主要包括: 框架 技术栈 案例 微信小程序 支付宝小程序 百度小程序 头条小程序 H5 App uni-app Vue 丰富 ⭕ ⭕️ ⭕️ ⭕ ⭕️ ⭕ Taro React 丰富 ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ wepy Vue 丰富 ⭕ ❌ ❌ ❌ ❌ ❌ mpvue Vue 丰富 ⭕ ❌ ❌ ❌ ⭕️ ❌  首先,就要排

    2024年02月11日
    浏览(75)
  • 基于Springboot的社区论坛系统(源代码+数据库)055

    部分代码地址 https://gitee.com/ynwynwyn/forum-public 前台: 话题列表,搜索话题,发布话题 通过标签筛选话题 个人设置:修改个人信息,查看发布话题记录,修改密码,修改头像 后台管理管理员登录: 话题列表的管理,话题可置顶、设置为精华话题, 评论管理 标签管理 用户管理

    2024年02月07日
    浏览(48)
  • jsp 新能源汽车论坛网Myeclipse开发mysql数据库web结构java编程计算机网页项目

    一、源码特点      JSP 新能源汽车论坛网是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql5.0,使用java语言开发。 java Web 新能源汽车论坛网 二

    2024年02月09日
    浏览(49)
  • 基于web3.0的社区教育网站开发教程/chatgpt

    社区教育网站是一个基于Web3.0和教育4.0理念的在线教育平台,它旨在为用户提供一个去中心化、开放、自主和协作的学习环境,让用户可以拥有和控制自己的数据和价值,通过智能合约和去中心化应用(DApp)实现与其他用户和服务的直接连接和交易,培养用户的全球公民技能

    2024年02月11日
    浏览(45)
  • 【Web3 系列开发教程——创建你的第一个 NFT(8)】如何开发一个成功的 NFT 项目 | NFT 社区建设技巧

    ⭐️ 本文首发自 前端修罗场(点击加入),是 一个由 资深开发者 独立运行 的专业技术社区 ,我专注 Web 技术、Web3、区块链、答疑解惑、面试辅导以及职业发展 。 博主创作的 《Web3 实战教程》(点击订阅) ,已经帮助许多开发者成功铸造 NFT,内容浅显易懂,能够快速对 W

    2024年02月05日
    浏览(64)
  • jsp 网络社区便利店系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

    一、源码特点      JSP 网络社区便利店系统是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql5.0,使用java语言开发。 jsp 网络社区便利店系统M

    2024年02月12日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包