微信小程序三种授权登录以及授权登录流程讲解

🎉🎉欢迎来到我的CSDN主页!🎉🎉

🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚

🌟推荐给大家我的专栏《微信小程序开发实战》。🎯🎯

👉点击这里,就可以查看我的主页啦!👇👇

Java方文山的个人主页

🎁如果感觉还不错的话请给我点赞吧!🎁🎁

💖期待你的加入,一起学习,一起进步!💖💖

请添加图片描述

一、微信授权登录流程

小程序登录

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

👇👇图解👇👇

步骤流程:

1.小程序调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器

2.开发者服务器以appid+appsecret+code换取 用户唯一标识openid和会话密钥session_key

3.开发者服务器根据用户标识来生成自定义登录态用于后续业务逻辑中前后端交互时识别用户身份

4.客户端保存后端生成的自定义登录态,并在下一次发送请求的时候带上这个自定义登录态。

注意:临时登录凭证code只能使用一次

二、微信用户授权

下面我以两种方式的代码来给大家论证一下微信用户授权登录的流程,第一种不需要用户确认即可完成用户授权登录在开发中并不是那么的安全,第二种则是需要用户确认方可进行授权登录。

前期准备工作

wxml代码展示

<!--pages/index/index.wxml-->
<view><button wx:if="{{canIUseGetUserProfile}}" type="primary" class="wx-login-btn" bindtap="getUserProfile">微信直接登录1</button><button wx:else open-type="getUserInfo" type="primary" class="wx-login-btn" bindgetuserinfo="wxLogin">微信直接登录2</button><image mode="scaleToFill" src="{{userInfo.avatarUrl}}" /><text>昵称:{{userInfo.nickName}}</text>
</view>

1.wx.login

JS代码展示

 wxLogin: function(e) {debuggerconsole.log('wxLogin')console.log(e.detail.userInfo);this.setData({userInfo: e.detail.userInfo})if (e.detail.userInfo == undefined) {app.globalData.hasLogin = false;util.showErrorToast('微信登录失败');return;}

效果展示:

看得出确实是走的这个方法并获取用户信息的,我们继续看一下另一个方法。

2.wx.getUserProfile

JS代码展示

  getUserProfile(e) {console.log('getUserProfile')// 推荐使用 wx.getUserProfile 获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认// 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗wx.getUserProfile({desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写success: (res) => {console.log(res);this.setData({userInfo: res.userInfo,hasUserInfo: true})}})}

效果展示: 

3.用户授权完成后端交互 

我们使用用户授权后将用户信息保存到数据库方便下次用户发送请求的时候做身份认证。

封装的代码

utils/util.js

function formatTime(date) {var year = date.getFullYear()var month = date.getMonth() + 1var day = date.getDate()var hour = date.getHours()var minute = date.getMinutes()var second = date.getSeconds()return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}function formatNumber(n) {n = n.toString()return n[1] ? n : '0' + n
}/*** 封封微信的的request*/
function request(url, data = {}, method = "GET") {return new Promise(function (resolve, reject) {wx.request({url: url,data: data,method: method,timeout:3000,header: {'Content-Type': 'application/json','X-OA-Token': wx.getStorageSync('token')},success: function (res) {if (res.statusCode == 200) {if (res.data.errno == 501) {// 清除登录相关内容try {wx.removeStorageSync('userInfo');wx.removeStorageSync('token');} catch (e) {// Do something when catch error}// 切换到登录页面wx.navigateTo({url: '/pages/auth/login/login'});} else {resolve(res.data);}} else {reject(res.errMsg);}},fail: function (err) {reject(err)}})});
}function redirect(url) {//判断页面是否需要登录if (false) {wx.redirectTo({url: '/pages/auth/login/login'});return false;} else {wx.redirectTo({url: url});}
}function showErrorToast(msg) {wx.showToast({title: msg,image: '/static/images/icon_error.png'})
}function jhxLoadShow(message) {if (wx.showLoading) { // 基础库 1.1.0 微信6.5.6版本开始支持,低版本需做兼容处理wx.showLoading({title: message,mask: true});} else { // 低版本采用Toast兼容处理并将时间设为20秒以免自动消失wx.showToast({title: message,icon: 'loading',mask: true,duration: 20000});}
}function jhxLoadHide() {if (wx.hideLoading) { // 基础库 1.1.0 微信6.5.6版本开始支持,低版本需做兼容处理wx.hideLoading();} else {wx.hideToast();}
}module.exports = {formatTime,request,redirect,showErrorToast,jhxLoadShow,jhxLoadHide
}

config/api.js 

// 以下是业务服务器API地址// 本机开发API地址
var WxApiRoot = 'http://localhost:8080/oapro/wx/';
// 测试环境部署api地址
// var WxApiRoot = 'http://192.168.191.1:8080/oapro/wx/';
// 线上平台api地址
//var WxApiRoot = 'https://www.oa-mini.com/demo/wx/';module.exports = {IndexUrl: WxApiRoot + 'home/index', //首页数据接口SwiperImgs: WxApiRoot+'swiperImgs',MettingInfos: WxApiRoot+'meeting/list',AuthLoginByWeixin: WxApiRoot + 'auth/login_by_weixin', //微信登录UserIndex: WxApiRoot + 'user/index', //个人页面用户相关信息AuthLogout: WxApiRoot + 'auth/logout', //账号登出AuthBindPhone: WxApiRoot + 'auth/bindPhone' //绑定微信手机号
};

JS代码

// pages/auth/login/login.js
var util = require('../../../utils/util.js');
var user = require('../../../utils/user.js');
const app = getApp();
Page({/*** 页面的初始数据*/data: {canIUseGetUserProfile: false, // 用于向前兼容lock:false},onLoad: function(options) {// 页面初始化 options为页面跳转所带来的参数// 页面渲染完成if (wx.getUserProfile) {this.setData({canIUseGetUserProfile: true})}//console.log('login.onLoad.canIUseGetUserProfile='+this.data.canIUseGetUserProfile)},/*** 生命周期函数--监听页面初次渲染完成*/onReady() {},/*** 生命周期函数--监听页面显示*/onShow() {},getUserProfile(e) {console.log('getUserProfile');// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认// 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗wx.getUserProfile({desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写success: (res) => {//console.log(res);user.checkLogin().catch(() => {user.loginByWeixin(res.userInfo).then(res => {app.globalData.hasLogin = true;wx.navigateBack({delta: 1})}).catch((err) => {app.globalData.hasLogin = false;if(err.errMsg=="request:fail timeout"){util.showErrorToast('微信登录超时');}else{util.showErrorToast('微信登录失败');}this.setData({lock:false})});});},fail: (res) => {app.globalData.hasLogin = false;console.log(res);util.showErrorToast('微信登录失败');}});},wxLogin: function(e) {console.log('wxLogin');if (e.detail.userInfo == undefined) {app.globalData.hasLogin = false;util.showErrorToast('微信登录失败');return;}user.checkLogin().catch(() => {user.loginByWeixin(e.detail.userInfo).then(res => {app.globalData.hasLogin = true;wx.navigateBack({delta: 1})}).catch((err) => {app.globalData.hasLogin = false;if(err.errMsg=="request:fail timeout"){util.showErrorToast('微信登录超时');}else{util.showErrorToast('微信登录失败');}});});},accountLogin() {console.log('开发中....')}})

WXML代码

<view class="container"><view class="login-box"><button wx:if="{{canIUseGetUserProfile}}" type="primary" class="wx-login-btn" bindtap="getUserProfile">微信直接登录</button><button wx:else open-type="getUserInfo" type="primary" class="wx-login-btn" bindgetuserinfo="wxLogin">微信直接登录</button><button type="primary" class="account-login-btn" bindtap="accountLogin">账号登录</button></view>
</view>

 后端配置appid+appsecret与数据库连接

后端代码

 /*** 微信登录** @param wxLoginInfo*            请求内容,{ code: xxx, userInfo: xxx }* @param request*            请求对象* @return 登录结果*/@PostMapping("login_by_weixin")public Object loginByWeixin(@RequestBody WxLoginInfo wxLoginInfo, HttpServletRequest request) {//客户端需携带code与userInfo信息String code = wxLoginInfo.getCode();UserInfo userInfo = wxLoginInfo.getUserInfo();if (code == null || userInfo == null) {return ResponseUtil.badArgument();}//调用微信sdk获取openId及sessionKeyString sessionKey = null;String openId = null;try {long beginTime = System.currentTimeMillis();//WxMaJscode2SessionResult result = this.wxService.getUserService().getSessionInfo(code);
//            Thread.sleep(6000);long endTime = System.currentTimeMillis();log.info("响应时间:{}",(endTime-beginTime));sessionKey = result.getSessionKey();//session idopenId = result.getOpenid();//用户唯一标识 OpenID} catch (Exception e) {e.printStackTrace();}if (sessionKey == null || openId == null) {log.error("微信登录,调用官方接口失败:{}", code);return ResponseUtil.fail();}else{log.info("openId={},sessionKey={}",openId,sessionKey);}//根据openId查询wx_user表//如果不存在,初始化wx_user,并保存到数据库中//如果存在,更新最后登录时间WxUser user = userService.queryByOid(openId);if (user == null) {user = new WxUser();user.setUsername(openId);user.setPassword(openId);user.setWeixinOpenid(openId);user.setAvatar(userInfo.getAvatarUrl());user.setNickname(userInfo.getNickName());user.setGender(userInfo.getGender());user.setUserLevel((byte) 0);user.setStatus((byte) 0);user.setLastLoginTime(new Date());user.setLastLoginIp(IpUtil.client(request));user.setShareUserId(1);userService.add(user);} else {user.setLastLoginTime(new Date());user.setLastLoginIp(IpUtil.client(request));if (userService.updateById(user) == 0) {log.error("修改失败:{}", user);return ResponseUtil.updatedDataFailed();}}// tokenUserToken userToken = null;try {userToken = UserTokenManager.generateToken(user.getId());} catch (Exception e) {log.error("微信登录失败,生成token失败:{}", user.getId());e.printStackTrace();return ResponseUtil.fail();}userToken.setSessionKey(sessionKey);log.info("SessionKey={}",UserTokenManager.getSessionKey(user.getId()));Map<Object, Object> result = new HashMap<Object, Object>();result.put("token", userToken.getToken());result.put("tokenExpire", userToken.getExpireTime().toString());userInfo.setUserId(user.getId());if (!StringUtils.isEmpty(user.getMobile())) {// 手机号存在则设置userInfo.setPhone(user.getMobile());}try {DateFormat df = new SimpleDateFormat("yyyy-MM-dd");String registerDate = df.format(user.getAddTime() != null ? user.getAddTime() : new Date());userInfo.setRegisterDate(registerDate);userInfo.setStatus(user.getStatus());userInfo.setUserLevel(user.getUserLevel());// 用户层级userInfo.setUserLevelDesc(UserTypeEnum.getInstance(user.getUserLevel()).getDesc());// 用户层级描述} catch (Exception e) {log.error("微信登录:设置用户指定信息出错:"+e.getMessage());e.printStackTrace();}result.put("userInfo", userInfo);log.info("【请求结束】微信登录,响应结果:{}", JSONObject.toJSONString(result));return ResponseUtil.ok(result);}

 数据库展示

效果演示

三、微信手机号授权

手机授权原理也是如此只不过调用的接口不同罢了。

JS代码

var util = require('../../../utils/util.js');
var api = require('../../../config/api.js');
var user = require('../../../utils/user.js');
var app = getApp();
Page({/*** 页面的初始数据*/data: {userInfo: {},hasLogin: false,userSharedUrl: ''},/*** 生命周期函数--监听页面加载*/onLoad: function (options) {},onShow: function () {let that = this;//获取用户的登录信息let userInfo = wx.getStorageSync('userInfo');this.setData({userInfo: userInfo,hasLogin: true});},getPhoneNumber: function (e) {console.log(e);let that = this;if (e.detail.errMsg !== "getPhoneNumber:ok") {// 拒绝授权return;}if (!this.data.hasLogin) {wx.showToast({title: '绑定失败:请先登录',icon: 'none',duration: 2000});return;}util.request(api.AuthBindPhone, {iv: e.detail.iv,encryptedData: e.detail.encryptedData}, 'POST').then(function (res) {if (res.errno === 0) {let userInfo = wx.getStorageSync('userInfo');userInfo.phone = res.data.phone;//设置手机号码wx.setStorageSync('userInfo', userInfo);that.setData({userInfo: userInfo,hasLogin: true});wx.showToast({title: '绑定手机号码成功',icon: 'success',duration: 2000});}});},exitLogin: function () {wx.showModal({title: '',confirmColor: '#b4282d',content: '退出登录?',success: function (res) {if (!res.confirm) {return;}util.request(api.AuthLogout, {}, 'POST');app.globalData.hasLogin = false;wx.removeStorageSync('token');wx.removeStorageSync('userInfo');wx.reLaunch({url: '/pages/index/index'});}})}
})

WXML代码

<!--pages/ucenter/user/user.wxml-->
<form bindsubmit="formSubmit"><view class='personal-data'><view class='list'><view class='item acea-row row-between-wrapper'><view>头像</view><view class='pictrue'><image src='{{userInfo.avatarUrl}}'></image></view></view><view class='item acea-row row-between-wrapper'><view>名字</view><view class='input'><input type='text' disabled='true' name='nickname' value='{{userInfo.nickName}}'></input></view></view><view class='item acea-row row-between-wrapper'><view>手机号码</view><button name='phone' class='phoneW' value='{{userInfo.phone}}' wx:if="{{!userInfo.phone}}" bindgetphonenumber="getPhoneNumber" hover-class='none' open-type='getPhoneNumber'>点击获取</button><view class='input acea-row row-between-wrapper' wx:else><input type='text' disabled='true' name='phone' value='{{userInfo.phone}}' class='id'></input><text class='iconfont icon-suozi'></text></view></view><view class='item acea-row row-between-wrapper'><view>ID号</view><view class='input acea-row row-between-wrapper'><input type='text' value='1000{{userInfo.userId}}' disabled='true' class='id'></input><text class='iconfont icon-suozi'></text></view></view></view><button class='modifyBnt' bindtap="exitLogin">退 出</button></view>
</form>

后端代码

 /*** 绑定手机号码** @param userId* @param body* @return*/@PostMapping("bindPhone")public Object bindPhone(@LoginUser Integer userId, @RequestBody String body) {log.info("【请求开始】绑定手机号码,请求参数,body:{}", body);String sessionKey = UserTokenManager.getSessionKey(userId);String encryptedData = JacksonUtil.parseString(body, "encryptedData");String iv = JacksonUtil.parseString(body, "iv");WxMaPhoneNumberInfo phoneNumberInfo = null;try {phoneNumberInfo = this.wxService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv);} catch (Exception e) {log.error("绑定手机号码失败,获取微信绑定的手机号码出错:{}", body);e.printStackTrace();return ResponseUtil.fail();}String phone = phoneNumberInfo.getPhoneNumber();WxUser user = userService.selectByPrimaryKey(userId);user.setMobile(phone);if (userService.updateById(user) == 0) {log.error("绑定手机号码,更新用户信息出错,id:{}", user.getId());return ResponseUtil.updatedDataFailed();}Map<Object, Object> data = new HashMap<Object, Object>();data.put("phone", phone);log.info("【请求结束】绑定手机号码,响应结果:{}", JSONObject.toJSONString(data));return ResponseUtil.ok(data);}

效果展示

四、注销登录

注销登录的前端代码同上,后端代码如下:

后端代码

 /*** 注销登录*/@PostMapping("logout")public Object logout(@LoginUser Integer userId) {log.info("【请求开始】注销登录,请求参数,userId:{}", userId);if (userId == null) {return ResponseUtil.unlogin();}try {UserTokenManager.removeToken(userId);} catch (Exception e) {log.error("注销登录出错:userId:{}", userId);e.printStackTrace();return ResponseUtil.fail();}log.info("【请求结束】注销登录成功!");return ResponseUtil.ok();}

效果演示

 请添加图片描述

到这里我的分享就结束了,欢迎到评论区探讨交流!!

💖如果觉得有用的话还请点个赞吧 💖

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/113330.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小县城蔬菜配送小程序制作全攻略

随着互联网的普及和人们对生活品质要求的提高&#xff0c;越来越多的小县城开始开发蔬菜配送小程序&#xff0c;以满足当地居民对新鲜蔬菜的需求。制作一个小县城蔬菜配送小程序&#xff0c;需要经过以下步骤&#xff1a; 步骤一&#xff1a;登录乔拓云平台 首先&#xff0c;打…

揭开 Amazon Bedrock 的神秘面纱 | 基础篇

在 2023 年 4 月&#xff0c;亚马逊云科技曾宣布将 Amazon Bedrock 纳入使用生成式人工智能进行构建的新工具集。Amazon Bedrock 是一项完全托管的服务&#xff0c;提供各种来自领先 AI 公司&#xff08;包括 AI21 Labs、Anthropic、Cohere、Stability AI 和 Amazon 等&#xf…

Android音视频开发之基础知识

一、视频文件 1、视频格式 常见格式&#xff1a;mp4、mkv、flv 封装的数据&#xff1a;音频码流、视频码流 常用工具&#xff1a; [FFmpeg下载]:https://ffmpeg.org/download.html 下载、安装并配置环境变量 ffmpeg.exe 视频编解码 ffplay.exe 播放器库 ffprobe.exe 音视频分…

git(部分)

1、git三个区域&#xff1a;工作区&#xff0c;暂存区&#xff0c;版本库 2、git文件状态&#xff1a;未跟踪&#xff0c;已跟踪&#xff08;新添加&#xff0c;未修改&#xff0c;已修改&#xff09; 如何查看暂存区和工作区文件状态&#xff1a;git status -s 3、查看版本记…

mysql MVC jsp实现表分页

mysql是轻量级数据库 在三层架构中实现简单的分页 在数据库sql编程中需要编写sql语句 SELECT * FROM sys.student limit 5,5; limit x,y x是开始节点&#xff0c;y是开始节点后的需要显示的长度。 在jdbc编程中需要给出x和y 一般是页数*页码&#xff0c;显示的长度。 代…

【(数据结构)—— 基于单链表实现通讯录】

&#xff08;数据结构&#xff09;—— 基于单链表实现通讯录 一.通讯录的功能介绍1.基于单链表实现通讯录(1). 知识要求(2). 功能要求 二.通讯录的代码实现1.通讯录的底层结构(单链表)(1).思路展示(2).底层代码实现(单链表)1.单链表头文件 —— &#xff08;函数的定义&#x…

【分类讨论】CF1747D

Problem - D - Codeforces 题意 思路 一看这个做法一定就是分类讨论 先判无解 显然&#xff0c;如果区间异或和不是0一定无解 如果区间内全是0&#xff0c;答案一定是0 之后怎么讨论 注意到需要讨论区间长度 如果长度是奇数&#xff0c;那么直接操作即可&#xff0c;答…

Top 10 数据恢复工具,可从iPhone 和 iPad 恢复数据

您是否正在寻找最好的 iPad 恢复软件&#xff0c;但不知道哪个选项最好&#xff1f;没有什么可担心的。本文将为您提供有关根据文件类型、设备兼容性和数据丢失原因等因素选择合适的 iPad 恢复软件的提示。此外&#xff0c;前 10 名提到的恢复软件是安全可靠的。 第 1 部分、iP…

ArmSoM-W3之RK3588硬编解码MPP环境配置

1. 简介 瑞芯微提供的媒体处理软件平台&#xff08;Media Process Platform&#xff0c;简称 MPP&#xff09;是适用于瑞芯微芯片系列的 通用媒体处理软件平台。该平台对应用软件屏蔽了芯片相关的复杂底层处理&#xff0c;其目的是为了屏蔽不 同芯片的差异&#xff0c;为使用者…

基于Java+Springboot+Vue前后端分离的商品库存销售管理系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 当今社会&#xff0c;…

Python--循环中的两大关键词 break 与 continue

在Python循环中&#xff0c;经常会遇到两个常见的关键词&#xff1a;break 与 continue break&#xff1a;代表终止整个循环结构 continue&#xff1a;代表中止当前本次循环&#xff0c;继续下一次循环 break&#xff1a; 英 /breɪk/ v. 打破&#xff0c;打碎&#xff0c…

看《软能力》的读后感

最近在图书大夏看了一本书的一部分&#xff0c;书名是《软能力》。本人看到了几个有意思的观点。一是接一些兼职项目。 二是分享自己的技术&#xff0c;让同事能干自己的工作&#xff0c;让自己的工作变得别人也能干&#xff0c;才有机会让自己的职位提升。 三是让手动操作变…

使用imx 8m 测试matter协议功能

参考网址&#xff1a; https://github.com/nxp-imx/meta-matter 请使用Ubuntu-20.04。18.04python版本太老 注意repo会出现此报错&#xff0c;可以无视&#xff1a; git checkout imx_matter_2023_q3 这一步在这个目录下操作 项目交流、学习、开发&#xff0c;欢迎私信。

【RNA biology】RNA的多功能性与早期生命进化

文章目录 RNARNA plays core functions in Central Dogma of BiologyrRNAsnRNA RNA worldReplication催化作用感知环境变化并作出响应 来自Manolis Kellis教授&#xff08;MIT计算生物学主任&#xff09;的课 油管链接&#xff1a;6.047/6.878 Lecture 7 - RNA folding, RNA wo…

TCP和UDP的原理及其区别(三次握手、四次挥手)

TCP和UDP都是在传输层上工作的协议&#xff0c;用于在网络中传输数据。 1、TCP和UDP之间的区别 TCP和UDP的主要区别在于它们提供的服务和特性。TCP提供可靠的、有序的、基于连接的数据传输&#xff0c;适用于对数据完整性和可靠性要求较高的应用&#xff08;邮件、短信&#xf…

Java多线程编程

文章目录 Java多线程编程线程概念线程常用方法线程创建线程优先级终止线程等待线程线程状态线程安全synchronizedvolatilewait和notify方法ThreadLocal的使用标准库线程安全类 Java多线程编程 线程概念 线程概念&#xff1a; 一个线程就是一个 “执行流”. 每个线程之间都可…

Springboot-案例 增删改查二

准备 前端程序、后端工程&#xff08;web/mybatis/mysql/lombok&#xff09;、数据库 开发规范 GET&#xff1a;查询 POST&#xff1a;新增 PUT&#xff1a;修改 DELETE&#xff1a;删除 Result.java Data NoArgsConstructor AllArgsConstructor public class Result {pri…

偶数科技发布实时湖仓数据平台Skylab 5.3版本

近日&#xff0c; 偶数发布了最新的实时湖仓数据平台 Skylab 5.3 版本。Skylab包含七大产品&#xff0c;分别为云原生分布式数据库 OushuDB、数据分析与应用平台 Kepler、数据资产管理平台 Orbit、自动化机器学习平台 LittleBoy、数据工厂 Wasp、数据开发与调度平台 Flow、系统…

鸿蒙HarmonyOS应用开发:扫描仪文件扫描

华为鸿蒙HarmonyOS已经发展到4.0&#xff0c;使用ArkTS作为开发语言。这篇文章结合Dynamsoft Service开发一个简单的鸿蒙应用&#xff0c;用来获取办公室里连接PC的扫描仪(惠普&#xff0c;富士通&#xff0c;爱普生&#xff0c;等)&#xff0c;把文档扫描到手机里。 准备工作…

DevExpress WPF Pivot Grid组件,可轻松实现多维数据分析!(二)

在上文中&#xff08;点击这里回顾>>&#xff09;我们主要为大家介绍了DevExpress WPF Pivot Grid组件的超快速枢轴分析功能、Microsoft分析服务等&#xff0c;本文将继续介绍图表透视数据的处理、MVVM支持等。欢迎持续关注我们&#xff0c;探索更多新功能哦~ P.S&#…