最详细代码讲解微信开放数据域绘制排行榜(翻页和滑动列表)

论坛的文本编辑器贴代码有种淡淡的忧伤。大家将就看。。。

子域index.js代码如下:

import * as Consts from './consts'

const PAGE_SIZE = 6;
const ITEM_HEIGHT = 125;

const dataSorter = (gameDatas, field = Consts.OpenDataKeys.Grade) => {
    return gameDatas.sort((a, b) => {
        const kvDataA = a.KVDataList.find(kvData => kvData.key === Consts.OpenDataKeys.Grade);
        const kvDataB = b.KVDataList.find(kvData => kvData.key === Consts.OpenDataKeys.Grade);
        const gradeA = kvDataA ? parseInt(kvDataA.value || 0) : 0;
        const gradeB = kvDataB ? parseInt(kvDataB.value || 0) : 0;
        return gradeA > gradeB ? -1 : gradeA < gradeB ? 1 : 0;
    });
}

class RankListRenderer
{
    constructor()
    {
        this.totalPage = 0;
        this.currPage = 0;
        this.gameDatas = [];    //https://developers.weixin.qq.com/minigame/dev/document/open-api/data/UserGameData.html
        this.init();
    }

    init()
    {
        this.canvas = wx.getSharedCanvas();
        this.ctx = this.canvas.getContext('2d');
        this.ctx.imageSmoothingEnabled = true;
        this.ctx.imageSmoothingQuality = "high";
    }

    listen()
    {
        //msg -> {action, data}
        wx.onMessage(msg => {
            console.log("ranklist wx.onMessage", msg);
            switch(msg.action)
            {
                case Consts.DomainAction.FetchFriend:
                    this.fetchFriendData();
                    break;

                case Consts.DomainAction.FetchGroup:
                    if(!msg.data)
                    {
                        return;
                    }
                    this.fetchGroupData(msg.data);
                    break;

                case Consts.DomainAction.Paging:
                    if(!this.gameDatas.length)
                    {
                        return;
                    }
                    const delta = msg.data;
                    const newPage = this.currPage + delta;
                    if(newPage < 0)
                    {
                        console.log("已经是第一页了");
                        return;
                    }
                    if(newPage + 1 > this.totalPage)
                    {
                        console.log("没有更多了");
                        return;
                    }
                    this.currPage = newPage;
                    this.showPagedRanks(newPage);
                    break;

                default:
                    console.log(`未知消息类型:msg.action=${msg.action}`);
                    break;
            }
        });
    }

    fetchGroupData(shareTicket)
    {
        //取出群同玩成员数据
        wx.getGroupCloudStorage({
            shareTicket,
            keyList:[
                Consts.OpenDataKeys.Grade,
            ],
            success:res => {
                console.log("wx.getGroupCloudStorage success", res);
                const dataLen = res.data.length;
                this.gameDatas = dataSorter(res.data);
                this.currPage = 0;
                this.totalPage = Math.ceil(dataLen / PAGE_SIZE);
                if(dataLen)
                {
                    this.showPagedRanks(0);
                }
            },
            fail:res => {
                console.log("wx.getGroupCloudStorage fail", res);
            },
        });
    }

    fetchFriendData()
    {
        //取出所有好友数据
        wx.getFriendCloudStorage({
            keyList:[
                Consts.OpenDataKeys.Grade,
            ],
            success:res => {
                console.log("wx.getFriendCloudStorage success", res);
                const dataLen = res.data.length;
                this.gameDatas = dataSorter(res.data);
                this.currPage = 0;
                this.totalPage = Math.ceil(dataLen / PAGE_SIZE);
                if(dataLen)
                {
                    this.showPagedRanks(0);
                }
            },
            fail:res => {
                console.log("wx.getFriendCloudStorage fail", res);
            },
        });
    }

    showPagedRanks(page)
    {
        const pageStart = page * PAGE_SIZE;
        const pagedData = this.gameDatas.slice(pageStart, pageStart + PAGE_SIZE);
        const pageLen = pagedData.length;

        this.ctx.clearRect(0, 0, 1000, 1000);
        for(let i = 0, len = pagedData.length; i < len; i++)
        {
            this.drawRankItem(this.ctx, i, pageStart + i + 1, pagedData[i], pageLen);
        }
    }

    //canvas原点在左上角
    drawRankItem(ctx, index, rank, data, pageLen)
    {
        const avatarUrl = data.avatarUrl.substr(0, data.avatarUrl.length - 1) + "132";
        const nick = data.nickname.length <= 10 ? data.nickname : data.nickname.substr(0, 10) + "...";
        const kvData = data.KVDataList.find(kvData => kvData.key === Consts.OpenDataKeys.Grade);
        const grade = kvData ? kvData.value : 0;
        const itemGapY = ITEM_HEIGHT * index;
        //名次
        ctx.fillStyle = "#D8AD51";
        ctx.textAlign = "right";
        ctx.baseLine = "middle";
        ctx.font = "70px Helvetica";
        ctx.fillText(`${rank}`, 90, 80 + itemGapY);

        //头像
        const avatarImg = wx.createImage();
        avatarImg.src = avatarUrl;
        avatarImg.onload = () => {
            if(index + 1 > pageLen)
            {
                return;
            }
            ctx.drawImage(avatarImg, 120, 10 + itemGapY, 100, 100);
        };

        //名字
        ctx.fillStyle = "#777063";
        ctx.textAlign = "left";
        ctx.baseLine = "middle";
        ctx.font = "30px Helvetica";
        ctx.fillText(nick, 235, 80 + itemGapY);

        //分数
        ctx.fillStyle = "#777063";
        ctx.textAlign = "left";
        ctx.baseLine = "middle";
        ctx.font = "30px Helvetica";
        ctx.fillText(`${grade}分`, 620, 80 + itemGapY);

        //分隔线
        const lineImg = wx.createImage();
        lineImg.src = 'subdomain/images/llk_x.png';
        lineImg.onload = () => {
            if(index + 1 > pageLen)
            {
                return;
            }
            ctx.drawImage(lineImg, 14, 120 + itemGapY, 720, 1);
        };
    }
}

const rankList = new RankListRenderer();
rankList.listen();

主域代码排行榜脚本:

import * as Consts from "./consts"
import {appdata} from './appdata'
import {POP_UI_BASE} from './common/ui/pop_ui_base'
import {pop_mgr, UI_CONFIG} from "./common/ui/pop_mgr"
import {TimerMgr} from "./common/timer/timer_mgr"
import * as utils from "./common/util"
import * as wxapi from "./common/wxapi"
import * as Audio from "./common/audio/audioplayer"

const {ccclass, property} = cc._decorator;
@ccclass
export class RankView2 extends POP_UI_BASE {

    @property(cc.Sprite)
    img_head: cc.Sprite = null;

    @property(cc.Label)
    txt_name: cc.Label = null;

    @property(cc.Sprite)
    img_rank: cc.Sprite = null;

    timerId:number;
    isDirty:boolean;
    rankTexture:cc.Texture2D;
    rankSpriteFrame:cc.SpriteFrame;

    on_show(...params)
    {
        const appUserInfo = appdata.appUserInfo;
        const wxUserInfo = appdata.wxUserInfo;
        const avatarUrl = wxUserInfo.avatarUrl132;
        if(avatarUrl.length > 0)
        {
            this.txt_name.string = wxUserInfo.nickName;
            utils.load_external_img(this.img_head, avatarUrl, "png");
        }

        //只能在主域设置大小, 且要先于赋值到sprite才起作用
        const sharedCanvas = wxapi.wxOpenData.wxGetSharedCanvas();
        sharedCanvas.width = this.img_rank.node.width;
        sharedCanvas.height = this.img_rank.node.height;

        this.rankTexture = new cc.Texture2D();
        this.rankSpriteFrame = new cc.SpriteFrame();
        
        //拿好友排行榜
        this.isDirty = true;
        wxapi.wxOpenData.wxPostMessageToSubDomain({
            action:wxapi.WxDomainAction.FetchFriend,
        });
        this.timerId = TimerMgr.getInst().loop(0.1, utils.gen_handler(this.updateRankList, this));
    }

    on_hide() 
    {
        this.rankTexture = null;
        this.rankSpriteFrame = null;
        this.isDirty = false;
        TimerMgr.getInst().remove(this.timerId);
    }

    updateRankList()
    {
        if(!this.isDirty)
        {
            return;
        }
        const sharedCanvas = wxapi.wxOpenData.wxGetSharedCanvas();
        this.rankTexture.initWithElement(sharedCanvas);
        this.rankTexture.handleLoadedTexture();
        this.rankSpriteFrame.setTexture(this.rankTexture);
        this.img_rank.spriteFrame = this.rankSpriteFrame;
    }

    onTouchPageBtn(event, delta)
    {
        this.isDirty = true;
        wxapi.wxOpenData.wxPostMessageToSubDomain({
            action:wxapi.WxDomainAction.Paging,
            data:parseInt(delta),
        });
    }

    onTouchGroupRank()
    {
        wxapi.wxShare.share({
            title:"分享文本", 
            imageUrl:"shareimg/bg02.png",
            query:`openID=${appdata.appUserInfo.openid}`,
        }, utils.gen_handler((shareTickets:string[]) => {
            if(!shareTickets || !shareTickets.length)
            {
                console.log('本次分享无shareTicket');
                return;
            }
            const shareTicket = shareTickets[0];
            this.isDirty = true;
            wxapi.wxOpenData.wxPostMessageToSubDomain({
                action:wxapi.WxDomainAction.FetchGroup,
                data:shareTicket,
            });
        }));
    }
}

后续:
后多朋友问滑动表现如何做,我想了想,只要在主域监听子域节点滑动事件,将滑动的y值传入子域,在子域中重新绘制排行榜就行了。代码如下:
主域:

import * as Consts from "./consts"
import {appdata} from './appdata'
import {POP_UI_BASE} from './common/ui/pop_ui_base'
import {pop_mgr, UI_CONFIG} from "./common/ui/pop_mgr"
import {TimerMgr} from "./common/timer/timer_mgr"
import * as utils from "./common/util"
import * as wxapi from "./common/wxapi"
import * as Audio from "./common/audio/audioplayer"

const {ccclass, property} = cc._decorator;
@ccclass
export class RankView2 extends POP_UI_BASE {
    @property(cc.Sprite)
    img_rank: cc.Sprite = null;

    timerId:number;
    isDirty:boolean;
    rankTexture:cc.Texture2D;
    rankSpriteFrame:cc.SpriteFrame;

    on_show(...params)
    {
        //只能在主域设置大小, 且要先于赋值到sprite才起作用
        const sharedCanvas = wxapi.wxOpenData.wxGetSharedCanvas();
        sharedCanvas.width = this.img_rank.node.width;
        sharedCanvas.height = this.img_rank.node.height;

        this.rankTexture = new cc.Texture2D();
        this.rankSpriteFrame = new cc.SpriteFrame();
        
        //拿好友排行榜
        this.isDirty = true;
        wxapi.wxOpenData.wxPostMessageToSubDomain({
            action:wxapi.WxDomainAction.FetchFriend,
        });
        this.timerId = TimerMgr.getInst().loop(0.1, utils.gen_handler(this.updateRankList, this));
        this.img_rank.node.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
    }

    on_hide() 
    {
        this.rankTexture = null;
        this.rankSpriteFrame = null;
        this.isDirty = false;
        TimerMgr.getInst().remove(this.timerId);
        this.img_rank.node.off(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
    }

    onTouchMove(event:cc.Event.EventTouch)
    {
        const deltaY = event.getDeltaY();
        // console.log("rank touchmove:", deltaY);
        this.isDirty = true;
        wxapi.wxOpenData.wxPostMessageToSubDomain({
            action:wxapi.WxDomainAction.Paging,
            data:deltaY,
        });
    }

    updateRankList()
    {
        if(!this.isDirty)
        {
            return;
        }
        const sharedCanvas = wxapi.wxOpenData.wxGetSharedCanvas();
        this.rankTexture.initWithElement(sharedCanvas);
        this.rankTexture.handleLoadedTexture();
        this.rankSpriteFrame.setTexture(this.rankTexture);
        this.img_rank.spriteFrame = this.rankSpriteFrame;
    }

    onTouchGroupRank()
    {
        wxapi.wxShare.share({
            title:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 
            imageUrl:"shareimg/bg02.png",
            query:`openID=${appdata.appUserInfo.openid}`,
        }, utils.gen_handler((shareTickets:string[]) => {
            if(!shareTickets || !shareTickets.length)
            {
                console.log('本次分享无shareTicket');
                return;
            }
            const shareTicket = shareTickets[0];
            this.isDirty = true;
            wxapi.wxOpenData.wxPostMessageToSubDomain({
                action:wxapi.WxDomainAction.FetchGroup,
                data:shareTicket,
            });
        }));
    }
}

子域:

import * as Consts from './consts'

const PAGE_SIZE = 8;
const ITEM_WIDTH = 750;
const ITEM_HEIGHT = 125;

const dataSorter = (gameDatas, field = Consts.OpenDataKeys.Grade) => {
	return gameDatas.sort((a, b) => {
		const kvDataA = a.KVDataList.find(kvData => kvData.key === Consts.OpenDataKeys.Grade);
		const kvDataB = b.KVDataList.find(kvData => kvData.key === Consts.OpenDataKeys.Grade);
		const gradeA = kvDataA ? parseInt(kvDataA.value || 0) : 0;
		const gradeB = kvDataB ? parseInt(kvDataB.value || 0) : 0;
		return gradeA > gradeB ? -1 : gradeA < gradeB ? 1 : 0;
	});
}

class RankListRenderer {
	constructor() {
		this.offsetY = 0;
		this.maxOffsetY = 0;
		this.gameDatas = [];    //https://developers.weixin.qq.com/minigame/dev/document/open-api/data/UserGameData.html
		this.init();
	}

	init() {
		this.canvas = wx.getSharedCanvas();
		this.ctx = this.canvas.getContext('2d');
		this.ctx.imageSmoothingEnabled = true;
		this.ctx.imageSmoothingQuality = "high";
	}

	listen() {
		//msg -> {action, data}
		wx.onMessage(msg => {
			//console.log("ranklist wx.onMessage", msg);
			switch (msg.action) {
				case Consts.DomainAction.FetchFriend:
					this.fetchFriendData();
					break;

				case Consts.DomainAction.FetchGroup:
					if (!msg.data) {
						return;
					}
					this.fetchGroupData(msg.data);
					break;

				case Consts.DomainAction.Paging:
					if (!this.gameDatas.length) {
						return;
					}
					const deltaY = msg.data;
					const newOffsetY = this.offsetY + deltaY;
					if (newOffsetY < 0) {
						// console.log("前面没有更多了");
						return;
					}
					if (newOffsetY + PAGE_SIZE * ITEM_HEIGHT > this.maxOffsetY) {
						// console.log("后面没有更多了");
						return;
					}
					this.offsetY = newOffsetY;
					this.showRanks(newOffsetY);
					break;

				default:
					console.log(`未知消息类型:msg.action=${msg.action}`);
					break;
			}
		});
	}

	fetchGroupData(shareTicket) {
		//取出群同玩成员数据
		wx.getGroupCloudStorage({
			shareTicket,
			keyList: [
				Consts.OpenDataKeys.Grade,
			],
			success: res => {
				console.log("wx.getGroupCloudStorage success", res);
				const dataLen = res.data.length;
				this.gameDatas = dataSorter(res.data);
				this.offsetY = 0;
				this.maxOffsetY = dataLen * ITEM_HEIGHT;
				if (dataLen) {
					this.showRanks(0);
				}
			},
			fail: res => {
				console.log("wx.getGroupCloudStorage fail", res);
			},
		});
	}

	fetchFriendData() {
		//取出所有好友数据
		wx.getFriendCloudStorage({
			keyList: [
				Consts.OpenDataKeys.Grade,
			],
			success: res => {
				console.log("wx.getFriendCloudStorage success", res);
				const dataLen = res.data.length;
				this.gameDatas = dataSorter(res.data);
				this.offsetY = 0;
				this.maxOffsetY = dataLen * ITEM_HEIGHT;
				if (dataLen) {
					this.showRanks(0);
				}
			},
			fail: res => {
				console.log("wx.getFriendCloudStorage fail", res);
			},
		});
	}

	showRanks(offsetY) {
		const startY = offsetY % ITEM_HEIGHT;
		const startIndex = Math.floor(offsetY / ITEM_HEIGHT);
		const stopIndex = startIndex + PAGE_SIZE + (startY == 0 ? 0 : 1);
		const datas = this.gameDatas.slice(startIndex, stopIndex);

		this.ctx.clearRect(0, 0, 1000, 1000);
		for (let i = 0, len = datas.length; i < len; i++) {
			this.drawRankItem(this.ctx, i, startIndex + i + 1, datas[i], startY, this.offsetY);
		}
	}

	drawAvatar(ctx, avatarUrl, x, y, w, h, cb) {
		avatarUrl = avatarUrl.substr(0, avatarUrl.lastIndexOf('/')) + "/132";
		ctx.fillStyle = "#ffffff";
		ctx.fillRect(x - 5, y - 5, w + 10, h + 10);

		const avatarImg = wx.createImage();
		avatarImg.src = avatarUrl;
		avatarImg.onload = () => {
			cb(avatarImg);
		};
	}

	//canvas原点在左上角
	drawRankItem(ctx, index, rank, data, startY, prevOffsetY) {
		const nick = data.nickname.length <= 10 ? data.nickname : data.nickname.substr(0, 10) + "...";
		const kvData = data.KVDataList.find(kvData => kvData.key === Consts.OpenDataKeys.Grade);
		const grade = kvData ? kvData.value : 0;
		const itemGapY = ITEM_HEIGHT * index - startY;

		//背景颜色
		if (rank % 2 == 1) {
			ctx.fillStyle = "#FBF7E4";
			ctx.fillRect(0, itemGapY, ITEM_WIDTH, ITEM_HEIGHT);
		}

		//名次
		if (rank < 4) {
			const rankImg = wx.createImage();
			rankImg.src = `subdomain/images/llk_phb_icon${rank}.png`;
			rankImg.onload = () => {
				if(prevOffsetY == this.offsetY) {
					ctx.drawImage(rankImg, 55, 30 + itemGapY, 78, 82);
				}
			};
		} else {
			ctx.fillStyle = "#BDBDBD";
			ctx.textAlign = "right";
			ctx.baseLine = "middle";
			ctx.font = "50px Helvetica";
			ctx.fillText(`${rank}`, 100, 80 + itemGapY);
		}

		//头像
		const avatarX = 125;
		const avatarY = 25 + itemGapY;
		const avatarW = 80;
		const avatarH = 80;
		this.drawAvatar(ctx, data.avatarUrl, avatarX, avatarY, avatarW, avatarH, (avatarImg) => {
			if(prevOffsetY == this.offsetY) {
				ctx.drawImage(avatarImg, avatarX, avatarY, avatarW, avatarH);
			}
		})

		//名字
		ctx.fillStyle = "#777063";
		ctx.textAlign = "left";
		ctx.baseLine = "middle";
		ctx.font = "30px Helvetica";
		ctx.fillText(nick, 220, 80 + itemGapY);

		//分数
		ctx.fillStyle = "#777063";
		ctx.textAlign = "left";
		ctx.baseLine = "middle";
		ctx.font = "30px Helvetica";
		ctx.fillText(`${grade}分`, 620, 80 + itemGapY);
	}
}

const rankList = new RankListRenderer();
rankList.listen();
33赞

已使用 markdown 标记更新楼主代码样式

2赞

详细得过分了,不过支持一下

哈哈,非常谢谢。怎么文本编辑没提供一个插入代码的功能?

1赞

学习了,希望能用,哈哈

楼主你好,如果排行榜做成一页列表的形式,那么画布的大小要怎么确定?

你是说做成滑动的列表吗

是的。像跳一跳的那种排行榜

学习了:grin:

请问A分享游戏群排行榜到群里,群里B点开后进入游戏,并弹出该群排行榜要怎么做?
B要如何获取shareTicket

请问这是CocosCreator上的代码吗?我想把CocosCretaor上开发的微信小游戏加入排放榜,但是方向也没有。
请问
1)您的代码是写在index.js文件里的吗?我怎么没有这个文件?
2)“import * as Consts from ‘./consts’”这个consts是您自己定义的吗?
3)import * as utils from “./common/util”;import * as wxapi from “./common/wxapi”;。。。这些common里的倒入是不是腾讯专门提供的,然后放在自己的项目文件夹里吗?

不好意思,初学者,发现demo project很少,您的比较接近我的需要,还请您有时间的时候赐教。谢谢。要是能提供demo project下载,那就更好了。再谢!

你说的这些是我自己写的,项目里用到的。我这里已经把子域获取好友数据,主域/子域绘制排行榜列表,主域/子域通信都写出来了。你参考着做就行,不用管其它东西是干什么的。
index.js要你自己新建的,你自己去看看官方开放数据域文档了解下基本步骤吧。

难道不是数据项数*项高吗?

common里的东西我已经上传到githubcocos-creator-h5-wxapi

谢谢您的详细耐心的解答和上传的文件。我马上学习!

正在学习中,发现下面几个倒入出错。这两个也是您自己写的吗?介意分享吗?谢谢!
import * as utils from “…/util”
import {TimerMgr} from “…/timer/timer_mgr”
import {POP_UI_BASE} from ‘./common/ui/pop_ui_base’
import {appdata} from ‘./appdata’

TimerMgr.getInst().loop(0.1, utils.gen_handler(this.updateRankList, this));0.1秒刷新一次,会卡么,我之前放在update里面,滑动刷新时,有些机型,会卡

我没有做滑动,是做的翻页按钮。如果你要做滑动的话,要注意不要每次new cc.Texture2D和cc.SpriteFrame,要复用一个对象。

代码也在我github上cocos_creator_proj_base

1赞

果断得mark一下