目錄
前言
交互流程說明圖
我的任務
登錄授權(login)
首頁(tababr分析)
房間準備區(preparing)
便簽編輯區
最終方案選擇(房主權限)
會議報告頁面(report)
前言
今年4月份機緣巧合和團隊成員參加了2019 年高校微信小程序開發大賽,經過長時間的“頭腦風暴”,我們最終將主題定為“頭腦風暴”類——《brain頭腦智序》(喜歡的話到我們的github上star一下呀(*^▽^*))
我們開發這款項目的初衷是:
創造出一款針對“高效思維發散、打破思維定勢,優化思維產出”而設計的功能性實用且獨特,操作流程簡明扼要,界面簡潔而又新穎的小程序。
前期我們討論出來的項目必須要有的特點是:
- 腦暴前:房主管理房間,成員們等待聽令
- 腦暴時:匿名“發言”且多流程選擇
- 腦暴后:會議結果報告生成圖及自定義導出保存
考慮到我們的項目是需要接近上線的,我們用到的后臺及數據庫就采用了基于微信自帶的云開發——看官方解釋:云開發為開發者提供完整的原生云端支持和微信服務支持,弱化后端和運維概念,無需搭建服務器,使用平臺提供的 API 進行核心業務開發,即可實現快速上線和迭代,同時這一能力,同開發者已經使用的云服務相互兼容,并不互斥。
簡而言之,就是無需購買自己的服務器、部署后端服務這些,云開發提供一站式后臺服務,直接在前臺利用api和云函數來操作各種后臺服務比如數據庫增刪改查以及文件存儲管理等(在新建項目的時候勾選創建 “云開發 QuickStart 項目”即可。當然空間大小也是有限制的,超了需要支付一定的金額),雖然速度上是慢了點,但是特容易上手,后臺小白如我也能操作,非常推薦!
關于云開發還需要提醒一下
- 云開發需要指定環境,一般都是有一個測試環境,一個發布環境,開發的時候選擇測試環境即可。
- Node.js云函數需要上傳并部署到云端才行,否則這些云函數都不起作用。
交互流程說明圖
先撒一張交互流程說明圖(這是前期設計的,后期開發的時候考慮到一些問題,我們并沒有嚴格按照此圖設計開發項目,會有一些小改動)
項目開發主要是我和另一個小伙伴一起操刀的
我的任務
我的任務主要是
- 制作主頁、自定義中間凸起tabbar?
- 利用微信云開發制作房間頁面,實時更新房間信息、房主邀請成員加入或踢出成員、準備狀態等
- 會議報告頁面生成以及利用canvas導出自定義報告截圖并保存?
?而此次開發主要是要用到數據庫來存儲信息,數據庫預覽如下,分為集合和記錄,他們的關系是一對多的關系,即一個集合里包含有多條記錄,每條記錄都是擁有相同屬性的json對象。之后會專門說清楚如何操作數據庫。
?好的,接下來開始說明開發思路了
登錄授權(login)
我們的項目肯定是需要用戶的身份信息的,比如openid(用戶唯一標識)、頭像、昵稱等,所以需要一個登錄授權頁面才行
?云開發提供了login云函數可以方便快捷的獲取用戶的openid,那么在獲取用戶數據思路如下(注意:由于進入我們小程序的方式有兩條:1、正常點擊小程序進入首頁? ?2、點擊房主分享的鏈接直接進入房間等待區。所以我們需要在用戶進入小程序前捕獲他原本需要進入的頁面path)
- 檢測用戶是否登錄過,有就直接進入path指定的url
- 沒有的話則進入登錄授權頁面,登錄后將獲取到的數據分別保留到全局app.js和本地緩存中
- 在后臺users集合中插入一條新用戶相關信息的記錄
- 進入指定path
所以先在app.js的onLaunch函數中插入如下數據
let userInfo = wx.getStorageSync('userInfo');let selfOpenId = wx.getStorageSync('selfOpenId');if (!selfOpenId || !userInfo.avatarUrl) {wx.reLaunch({url: 'pages/login/login?redirect_url=' + encodeURIComponent(`/${redirect_url}`),})return}
在登陸界面中,核心代碼如下:
// 調用云函數wx.cloud.callFunction({name: 'login',data: {},success: res => {let openid = res.result.openid;//存儲用戶代碼app.globalData.selfOpenId = openid;wx.setStorageSync('userInfo', userInfo);//本地緩存用戶信息wx.setStorageSync('selfOpenId', openid);wx.hideLoading();that.setData({loading: false});//先查詢是否有此用戶記錄app.onQuery('users', { openid: openid }, { nickName: true }).then(res => {let data = res.data;if (data.length === 0) {//沒有則在users集合中新建一條記錄//多次會用到數據庫操作,建議直接封裝代碼到app.jsapp.onAdd('users', {//建立用戶的基本信息屬性avatarUrl: app.globalData.userInfo.avatarUrl,//頭像hisRoom: [],//用于歷史紀錄,查詢進去過的房間號nickName: app.globalData.userInfo.nickName,//昵稱star: [],userInfo: {},openid: app.globalData.selfOpenId}).then(()=>{console.log('插入用戶數據成功')})}else{console.log('用戶已有數據')}//開始跳轉頁面if (that.data.redirect_url) {console.log('跳轉開始')wx.redirectTo({url: that.data.redirect_url})} else {wx.redirectTo({//如果沒有指定redirect_url,默認跳轉到首頁url: 'pages/index/index'///!!!!})return }}) },fail: err => {console.error('[云函數] [login] 調用失敗', err)}})
?登陸完畢后,進入首頁啦
首頁(tababr分析)
首頁如下,簡潔吧嘻嘻(為了方便自己……一下參與者只有我……,但是實測過幾人參與也是能運行的!當然如果各位又遇到什么問題歡迎評論區狂砸我!)
首頁沒什么技術難點,主要還是tabbar如果需要中間有凸起效果的話是不能使用微信自帶的tabbar(小程序規定tabbar要老實點……)
所以我們需要自定義一個tabbar組件(其實我現在還沒實現組件化……時間問題,過段時間能透會氣的時候會優化一下這里)
在需要tababr的頁面注入這段代碼
<view class="tab-bar"><view wx:for="{{list}}" wx:key="index" class="tab-bar-item bar{{index}}" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab"><image wx:if="{{index!=1}}" class="image" src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></image><image wx:if="{{index==1}}" class="image {{selected === index ? 'barson2':'barson1'}}" src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></image><view class="view" style="color: {{selected === index ? selectedColor : color}};">{{item.text}}</view></view>
</view>
js中list數據為:
//需要的數據 selected: 0,color: "#000000",//正常顏色selectedColor: "#4880ff",//高亮顏色list: [{iconPath: "../../icon/index.png",//正常圖標selectedIconPath: "../../icon/indexChecked.png",//高亮圖標text: "首頁"//文字顯示}, {iconPath: "../../icon/arrowbg.png",selectedIconPath: "../../icon/arrowbg.png",}, {iconPath: "../../icon/mine.png",selectedIconPath: "../../icon/mineChecked.png",text: "我的"}]//需要的函數switchTab: function (e) {const data = e.currentTarget.dataset //獲取到DOM元素上的自定義屬性const url = data.path
//在for循環中,每個tab上的index屬性和它for循環的index掛鉤,這樣
//在點擊的時候就知道用戶點了哪個元素if (data.index === 1) {
//點擊中間跳轉到創建房間頁面,由于創建頁面已經有一個退出房間的大叉叉圖標,
//所以不希望這里還能有返回頁面的按鈕,所以用redirectTowx.redirectTo({ url: '/pages/buildRooming/buildRooming' });this.setData({selected: 1 //selected與tabbar里for循環的index對應,如果兩者相同則證明該tab被點擊了
//需要高亮!})} else {this.setData({selected: data.index})}}
由于我們需要實現當用戶點擊某個tab的時候需要切換高亮的圖片和文字,那么我們一開始在js中利用list數組儲存tabbar需要的可以被遍歷使用的數據,包括:普通icon、高亮item和顯示文字。其次我們還需要的數據請看上面的代碼及上面的解釋。這里需要提示一下的是:
我將首頁頁面和我的頁面一起寫到一個wxml里,原因是tabbar他不是全局的,需要插入到每個需要他的頁面里,首頁和我的都需要tabbar,這樣一來有個問題,當點擊他們兩個tab相互切換頁面的時候tabbar會有加載延時,會閃一下,特別是網速慢的時候,可以很明顯的看出tabbar消失又出現了這樣一個問題。所以為了解決這個閃爍,況且這兩個頁面的內容也不多,不復雜1,就把他倆并在一起了。
至于中間的凸起圖標
思路是這樣的:
背景圖標用白色菱形圖片代替,注意設置樣式(在.bar1 .image里),普通和被點擊之間是通過類barson1與barson2來切換樣式的,這兩類的公共樣式都是利用偽元素::before和::after畫出兩條矩形然后讓其一旋轉90度,唯一不同的是點擊后圖標整體需要旋轉45度,以及顏色要變成紅色。當然給個過渡效果會更佳。
進入房間步驟,沒什么技術難度
?房間準備區(preparing)
這里需要考慮到的點有
- 房間分兩個視角:普通成員——點擊準備按鈕切換準備狀態,此時該成員頭像也會變亮表示進入準備狀態;房主——房間第一位置且有專門的房主圖標表示,可以踢別人、在所有人都準備完畢才可以按開始討論按鈕并決定討論時長,點擊確認后所有人進入自己的編輯區點擊便簽開始寫下自己的想法,房主的步驟是不可逆的。
- 房間實時性,所有的人都能實時、同步地獲取到房間的所有情況,包括誰進來了,誰被踢走了,誰在準備狀態了……
- 最后一個位置始終都是分享按鈕
- 如果成員過多要采用分頁狀態,設置左右滑動查看成員
- 所有人可自行離開房間,如果房主選擇離開,那么按順位繼承房主名號(這里要注意如果只有房主一個人的情況下離開)
- 如果房主已經處于設置討論時長的頁面,那么剩下的成員的準備按鈕要失效,不可再取消準備狀態
- 如果新成員點擊分享鏈接加進來要先轉到登錄頁面,要儲存users的信息
- 如果房間已經開始在討論階段了,如果又有人點擊分享鏈接加進來要給出相應提示,并返回主頁。
?創建的房間room集合的每條記錄所需屬性為:
/* 插入數據 */app.onAdd('rooms',{title: inputValue.text,//房間討論主題roomNum: String(inputValue.roomNum),//房號roomMaster: { //房主信息openid: selfOpenId, avatarUrl: app.globalData.userInfo.avatarUrl, nickName: app.globalData.userInfo.nickName },readyArr: [],//準備好的成員openid,當長度===roomates.length-1時表示只有房主沒準備了roommates: [{ //保存房間所有人的信息openid:selfOpenId, avatarUrl: app.globalData.userInfo.avatarUrl, nickName: app.globalData.userInfo.nickName , ready: false }],allset: false,//對應readArr,當成員都準備好的時候,值為true,房主可以點擊開始討論按鈕inMeeting: false,//是否在討論中,如果是則其他人無法再進入房間preparingTime: 2,//房主設置的準備時長,用于閱讀腦暴規則meetingTime: 10,//討論時長again:false,//輪數是否為第二次開始,第一輪需要設置準備時長,二輪開始則不需要validPlan:[],//保存討論結束后的篩選出來的有效建議startTime:0,//記錄討論開始時間戳totalTime:'',//保存此次討論總時長date:'',//記錄討論日期hasRank:false,hasPersonal:false,reportAgain:false,goReport:false,goSelect:false})
分頁的話用swiper組件即可,
data: {index: 0,join: 1,//點擊鏈接會傳入這個屬性,1表示是鏈接進來的,0表示房主allset: false,//表示房主已設置好時間,全部成員可以開始進入討論頁面了inputMsg: {//保存上一頁面傳來的房間信息roomNum: 0,text: '',},buttonText: '',//針對是房主還是成員切換按鈕文本dotsWidth: 0,//分頁指示點長度currentSwiper: 0,//分頁現在userInfo: [],//保存房間成員信息userInfoSwiper: [],//二維數組,里面的數組表示每8人信息組成一個arr,構成一頁,便于分頁顯示allTime: true//同步數據停止信號,如果為fasle則表示停止實時請求響應的請求},
<swiper current="{{currentSwiper}}" bindchange="swiperChange"><block wx:for="{{userInfoSwiper}}" wx:key="" wx:for-index="outerIndex" wx:for-item="outerItem">//每頁成員布局<swiper-item class='peopleList' bindtap='bindDelete'>//outerItem保存著內部arr,每個arr有8個或者8個一下的成員數據對象<block wx:for="{{outerItem}}" wx:key="" wx:for-index="innerIndex" wx:for-item="innerItem"><view class="peopleItem" id="{{innerIndex}}"><view class="imgPart" data-parentIndex="{{outerIndex}}" data-index="{{innerIndex}}"><image src='{{innerItem.avatarUrl}}' class="headImg" />//設置遮罩,房主不需要遮罩<view wx:if="{{!innerItem.ready && innerIndex != 0}}" class="map"></view>//設置房主圖標<view wx:if="{{ innerIndex ===0 && outerIndex === 0 }}" class="houseHolder" style="background:url('https://dmt-web-1257360276.cos.ap-guangzhou.myqcloud.com/%E5%A4%B4%E8%84%91%E6%99%BA%E5%BA%8F/roomowner.png') no-repeat ;background-size: 100%;"></view>//設置刪除圖標,不是房主則看不到此圖標<image wx:if="{{join===0}}" class="{{ outerIndex === 0 ? (innerIndex === 0 ? 'hidden' : 'delete' ): 'delete'}}" id="delete{{outerIndex*8+innerIndex}}" src="../../icon/delete.png" /></view><view class="nickName">{{innerItem.nickName}}</view></view></block>//結束循環//保留最后一頁的最后位置一定是分享按鈕<button wx:if="{{outerIndex===userInfoSwiper.length-1}}" class="invite" open-type='share' style="background:url('https://dmt-web-1257360276.cos.ap-guangzhou.myqcloud.com/%E5%A4%B4%E8%84%91%E6%99%BA%E5%BA%8F/join.png');background-size:100%"></button></swiper-item></block>
</swiper>
源代碼上都有注釋相信大家可以看得懂的,這里需要提一下如何做到同步數據,其實一開始我們是想要用websocket,它是一種建立在 TCP 協議之上的協議,它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話,屬于服務器推送技術的一種。現在的實時聊天技術都是可以基于這種協議來實現,但是由于我們還有學業上的作業要完成,留給我們項目的時間不多,而且我們是沒有怎么部署后端服務的,使用的是微信云開發提供的一站式后臺服務,不是自己的服務器操作起來確實有點麻煩,我們沒能及時實現它,后來就直接簡單粗暴的使用setTimeout函數模擬setInterval來不斷請求后臺數據庫信息來更新房間信息……
關于其他的呢,只要你把思路理清,需要哪些流程和步驟以及一些臨界條件,把需求一條條列出來(可看我上面列出的需要考慮的點)就可以輕松解決啦
便簽編輯區
開始討論(房主設置時長——所有人跳轉到閱讀討論規則——進入自定義編輯區,可利用便簽輸入自己的想法,可自定義便簽顏色):
選擇多輪討論(討論時間一到便跳轉到“意見”瀏覽區,所有成員的建議都集中在此,可點贊別人的建議(獲贊最多的成員獲“點贊王”稱號,可顯示在報告中)——隨后成員等待房主操作,如果房主選擇“再次討論”,則房主需要再次設置討論時長,所有人會再次進入便簽編輯區,此時可點擊“回顧排行榜”查看上一輪所有“建議”)
?
最終方案選擇(房主權限)
以下為房主視角,討論完畢房主進入選擇最終方案頁面,每輪點贊數由高到低排序,房主勾選出這幾輪中得出的最終方案,點擊確認按鈕后,所有成員集體跳轉到會議報告頁面
?
?
?會議報告頁面(report)
會議報告頁面:報告默認會顯示有效方案數、具體方案以及本次獲得贊最多的人,成員可以勾選所有排行榜上的每輪記錄或者自己發過的記錄實現自定義導出
?
導出報告頁面:導出后便可以自由查看自己想看的記錄
?
選擇“生成”按鈕會將會議報告生成圖片,保存到本地相冊,方便隨時查看
?好的我來具體說說會議報告頁面需要注意的點:
導出頁面的標題我限制了最多2行顯示,多余的用…顯示,核心代碼如下:
let title = that.canvasWordBreak(300 * ratio, 16 * ratio, that.data.title);//限制在兩行以內顯示canvasWordBreak: function (maxWidth, fontSize, text) {//切割文字const maxNum = maxWidth / fontSize//每行最多顯示幾個字const textLength = text.length;//title字符串的長度let textRowArr = []let tmp = 0;let line = 2;//你需要顯示的最多行數while (line--) {textRowArr.push(text.substr(tmp, maxNum))tmp += maxNumif (tmp >= textLength) {//原文的字數以及小于每次累加的最大字數return textRowArr}if (line === 0) {//console.log(textRowArr[1][0],'line')let length = textRowArr[1].length;textRowArr[1] = textRowArr[1].substr(0, length - 1);//將最后一個字符變為...textRowArr[1] += '…';return textRowArr}}},
canvas繪圖因為需要具體的px數值,而我們的項目是需要動態導出需要的部分,有變化的部分包括標題的行數和排行榜或者個人記錄是否要導出。所以這里頁要注意不同屏幕尺寸大小,這里以iphone6尺寸為標準,設置比例 ratio =?windowWidth?/?375 即可
標題的話我先假設有2行,測試一行標題大概需要的高度差為?lineCha?=?22?*?Number(ratio),那么后續只需要判斷行數與高度差之間的關系即可,這里我算出的是
title.forEach((item, index) => {//由于主題可能會很長,拆分成幾個數組,一個數組一行顯示//if (index === 1) lineCha = 0;//由于一開始我是設置了2行文字顯示,所以下面的所有高度都是基于此的,//那么如果title是1行的話lineChaNum默認為1,即減去lineCha//如果是2行,則lineChaNum為0,2行以上則lineChaNum為1,即整體高度加lineCha,以此類推,本項目限制了最多只能2行顯示if (index >= 1) { lineChaNum = -1 * (index - 1) }context.setFontSize(16 * ratio);context.setFillStyle("#000000");context.fillText(item, 70 * ratio, height);height += 20 * ratio;
})
繪制圖片微信這邊的要求是要先將網絡路徑轉換成臨時路徑
//繪制canvas生成圖
//初始化圖片臨時路徑
that.getImgTempPath('https://dmt-web-1257360276.cos.ap-guangzhou.myqcloud.com/%E5%A4%B4%E8%84%91%E6%99%BA%E5%BA%8F/stared.png', 'star')
that.getImgTempPath('https://dmt-web-1257360276.cos.ap-guangzhou.myqcloud.com/%E5%A4%B4%E8%84%91%E6%99%BA%E5%BA%8F/circle.png', 'circle')that.getImgTempPath('https://dmt-web-1257360276.cos.ap-guangzhou.myqcloud.com/%E5%A4%B4%E8%84%91%E6%99%BA%E5%BA%8F/zanKing.png', 'kingCircle')//將網絡圖片轉成臨時路徑
getImgTempPath: function (url, data) {let that = this;wx.downloadFile({url: url, //success: function (res) {// 只要服務器有響應數據,就會把響應內容寫入文件并進入 success 回調,業務需要自行判斷是否下載到了想要的內容if (res.statusCode === 200) {//console.log(res.tempFilePath, "reererererer")that.setData({[data]: res.tempFilePath //動態生成屬性})}}})
},//將臨時路徑賦值給CanvasContext.drawImage()即可,具體參數還請移步微信官方文檔
至于排行榜和個人記錄,我分別用rankH和rankP變量來初始化他們的高度,由于是自定義選擇導出,可能會出現這些情況
- if:如果有排行榜,那么需要先用rankH記錄它應該處于clientTop的距離,我這里是:rankH?=?450?*?ratio?-?lineCha?*?lineChaNum算式是可以不固定的,高度自行測試確定,看著舒服即可(就是這么隨便哈哈哈)再利用排行榜的數組數據遍歷循環繪制出每行數據,每行高度都要累加到rankH才能繪制出下一行,總之繪制的高度的表達式都要有rankH才行
- else:如果沒有排行榜,則rankH就不需要累加了,我測試的時候rankH的值還需要再調整一下:rankH?=?380?*?ratio?-?lineCha?*?lineChaNum//如果不需要排行榜,則記錄此值為繪制下一部分的起始高度
- if:如果有個人紀錄,那么他的起始高度為rankP?=?rankH?+?70?*?ratio;//承接排行榜的高度數值并調整成個人紀錄需要的高度,之后的累加是和排行榜一樣的套路了
- else:如果沒有個人記錄,那么rankP?=?rankH,之后部分的繪制再依據rankP的數值調整即可。也就是說排行榜和個人紀錄都各自需要一個變量(rankH和rankP)來連接彼此(關系式),最后歸為一個變量(rankP)來處理,數值有什么變化,最后的變量也會相應更改,下面的部分的高度都依據rankP這個變量的改動而自我調節
具體還請看項目代碼,都有解釋的
最后展示生成的圖片
如果要想展示圖片,要先將canvas繪制到wxml里,然后再在js中利用canvas的canvasToTempFilePath接口繪制出圖片的臨時路徑
<!-- canvas繪圖區 -->
<view class='imagePathBox' hidden="{{maskHidden == false}}" id="imagePathBox" bindtap="hideCanvas"><image src="{{imagePath}}" class='shengcheng' mode="aspectFit" id="canvasImg"></image><view class="btnGroup"><button class='baocun' bindtap='saveImg'>保存</button><button class='cancel' bindtap='cancel'>取消</button></view>
</view>
<view hidden="{{maskHidden == false}}" class="mask"></view>
//先將canvas繪制于此,并隱藏起來
<view class="canvas-box"><canvas style="width: {{375*ratio}}px;height: {{canvasHeight*ratio}}px;position:fixed;top:99999px;" canvas-id="mycanvas" />
</view>
//把當前畫布指定區域的內容導出生成指定大小的圖片,需要延遲一會,繪制期間耗時
setTimeout(function () {wx.canvasToTempFilePath({canvasId: 'mycanvas',success: function (res) {var tempFilePath = res.tempFilePath;//生成文件的臨時路徑that.setData({imagePath: tempFilePath,//插入到image標簽的src可顯示canvasHidden: true});},fail: function (res) {console.log(res);}});
}, 300);
這里圖片高度其實大約就等于rankP的數值,因為方案那塊的繪制全部都是基于rankP累加的,因為繪制是基于左上角標準來繪制的,所以這里需要再加些數值,讓底部有些留白,我這里是:canvasHeight:?rankP?+?30?*?ratio
保存圖片的核心代碼為:
//點擊保存到相冊saveImg: function () {var that = thiswx.saveImageToPhotosAlbum({filePath: that.data.imagePath,//圖片的臨時路徑success(res) {wx.showModal({content: '圖片已保存到相冊!',showCancel: false,confirmText: '好的',confirmColor: '#333',success: function (res) {if (res.confirm) {console.log('用戶點擊確定');/* 該隱藏的隱藏 */that.setData({maskHidden: false})}}, fail: function (res) {console.log(11111)that.setData({maskHidden: false})}})}})},
至此!解說終于完畢啦,(擦擦汗……
說實話此次開發的時間比較短,很多代碼都急需優化才行,哭泣,找個寬裕的時間來開干!!
收獲還是蠻大的自己感覺,特別是開發一個要上線的項目,跟自己隨便搞搞的測試項目區別是超級大的,要努力加油呀!
好的不多說了,逃
如果有什么問題歡迎砸評論!ε=ε=ε=┏(゜ロ゜;)┛