前端wx-jssdk的使用及企微和微信下分享等功能自定义处理

一、前端wx-jssdk的使用

wx-jssdk使用需要微信公众平台内进行设置(进入公众号设置的“功能设置”里填写“JS接口安全域名“的操作)及后端配合才能真正使用,下文是讲述前端如何使用。
wx-jssdk的接口文档

1、导包

在main.ts中(以vite+react项目中为例),为什么在main.ts中进行使用呢?其实在index.html文件中调用也是可以的,因为我要用到一些公共方法,并且保证要完成加载在线wx-jssdk包完后,再进行挂载等步,实现异步变同步处理更方便,就放在main.ts中。放在index.html 并保证异步变同步完全加载完wx-jssdk包就行,即:支持使用 AMD/CMD 标准模块加载方法加载

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './app';
import { getEnv,WECHAT_ENV  } from '@/utils';
//import '@/utils/setup.ts';
//import '@/assets/css/reset.less';
//import '@/assets/css/base.less';
//import '@/assets/font/iconfont.css';const script = document.createElement('script');
// @ts-ignore
script.crossorigin = 'anonymous';
const sdkVerion = getEnv() === WECHAT_ENV.qyWechat ? 'jweixin-1.2.0.js' : 'jweixin-1.6.0.js';
// 开发环境和线上环境的wxsdk的路径有所区别
// script.src = process.env.NODE_ENV === 'development' ? `./public/lib/${sdkVerion}` : `./lib/${sdkVerion}`;
script.src = `https://res.wx.qq.com/open/js/${sdkVerion}`;
script.onerror = () => {console.log('qy-wx-sdk:loadError');
};script.onload = () => {console.log({ wx });// 必须等wxsdk 加载完之后,才能渲染页面ReactDOM.render(<BrowserRouter basename={`/${import.meta.env.VITE_APP_NAME}`}><App /></BrowserRouter>,document.getElementById('root'));
};document.head.appendChild(script);

2、使用

wx-jssdk提供了企微和微信环境下的一些方法进行使用:(wx-jssdk的接口文档、企业微信-wxjssdk)

  • wx.config(obj):config接口注入权限验证配置
  • wx.ready():config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
  • wx.error():通过error接口处理失败验证
  • wx.checkJsApi({ jsApiList: [],success(res) {console.log(res);},}):判断当前客户端版本是否支持指定JS接口,jsApiList: 需要检测的JS接口列表,所有JS接口列表,见JS接口列表;企业微信-wxjssdk
  • wx.agentConfig():
    config注入的是企业的身份与权限,而agentConfig注入的是应用的身份与权限。尤其是当调用者为第三方服务商时,通过config无法准确区分出调用者是哪个第三方应用,而在部分场景下,又必须严谨区分出第三方应用的身份,此时即需要通过agentConfig来注入应用的身份信息。企业微信-wxjssdk内有详细讲解
  • wx.invoke():获取进入H5页面的入口环境,企业微信-wxjssdk

封装的getTicket

import ajax from '@/utils/ajax';
import { ConfigOptions, JSApiList } from 'wx-jssdk';
import { getEnv,WECHAT_ENV  } from '@/utils';// 普通签名
export const getTicket = async (jsApiList, callback, isShowError = true, maxRequestCount = 3) => {// 开发+真机调试模式,要初始化JS_SDK if (process.env.NODE_ENV === 'development' && !process.env.WX_JS_SDK_ENABLED) return;let url = '';// 如果是 iOS 设备,则使用第一次进入App时的 URL 去请求 wxConfig,不然的话会导致 iOS 中分享的链接描述信息或者图标不对if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent) && env === 2) {url = encodeURIComponent(localStorage.getItem('entryUrl'));// 有保存在localstorage中的页面路径} else {// console.log('签名地址=======================', window.location.href)url = encodeURIComponent(window.location.href);}const params = {appId: newAppId || appId,corpId: env === 2 ? authCorpId : currentCorpId,wechatType: env,jsUrl: url,agentId,};console.log('getTicket', `path=${location.pathname}  url=${decodeURIComponent(params.jsUrl)}`);try {const res = await ajax('getTicket', params, isShowError); // 后端定义的接口  这里的ajax是自定义的const { retdata = {} } = res;const { appId, noncestr, signature, timestamp, nextUpdateTime } = retdata;const obj = {debug: false, // 是否开启调试模式appId, // appidtimestamp, // 时间戳nonceStr: noncestr, // 随机字符串signature, // 签名jsApiList,openTagList: ['wx-open-launch-weapp'],};if (env === 1) {obj.beta = true;obj.appId = currentCorpId;}wx.config(obj);wx.ready(function () {if (typeof callback === 'function') callback(jsApiList);});wx.error((res) => {console.log('%c zjs wx.error res:', 'color: #0e93e0;background: #aaefe5;', res);if (typeof callback === 'function' && maxRequestCount > 0) callback(new Error('error'));});} catch (e) {console.log('%c zjs getTicket err:', 'color: #0e93e0;background: #aaefe5;', e);setTimeout(() => {maxRequestCount > 0 && getTicket(jsApiList, callback, isShowError, --maxRequestCount);}, 1000);}
};// 企业微信下一些自建应用签名
export const agentConfig = async (jsApiList: JSApiList[],callback: Function,shareUrl = '',isShowError = true,maxRequestCount = 3
) => {getTicket([...jsApiList, 'agentConfig'],async (error: string) => {if (error === 'error') {// 过期或者签名错误 重新获取setTimeout(() => {maxRequestCount > 0 && agentConfig(jsApiList, callback, shareUrl, isShowError, --maxRequestCount);}, 1000);} else {const params = {type: WECHAT_ENV.qyWechat,jsUrl: window.location.href,};const res = await ajax({ api: 'getTicket', params });const { retdata = {} } = res;const { corpId: corpid, noncestr: nonceStr, agentid, signature, timestamp }: any = retdata;wx.checkJsApi({jsApiList: ['agentConfig'],success(res) {console.log(res);},});const obj = {corpid, // 必填,企业微信的corpid,必须与当前登录的企业一致agentid, // 必填,企业微信的应用idtimestamp, // 必填,生成签名的时间戳nonceStr, // 必填,生成签名的随机串signature, // 必填,签名,见附录1jsApiList,// openTagList: ['wx-open-launch-weapp'],success: (res: ILooseStrObj) => {console.log('agentConfig ok', res);if (typeof callback === 'function') callback(jsApiList);},fail(res: ILooseStrObj) {console.log('agentConfig fail', res);if (typeof callback === 'function') callback('error');},};console.log('agentConfig obj', obj);wx.agentConfig(obj);}},shareUrl);
};

上面公用的一些方法

export const WECHAT_ENV = {qyWechat: 1, // 企业微信wechat: 2, // 微信
};// 判断当前是微信环境还是企业微信环境
export const getEnv = () => {const ua = window.navigator.userAgent.toLowerCase();// eslint-disable-next-lineif (Boolean(ua.match(/MicroMessenger/i)) && Boolean(ua.match(/wxwork/i))) {// 企业微信// console.log('企业微信环境-1')return WECHAT_ENV.qyWechat;// eslint-disable-next-line} else if (Boolean(ua.match(/micromessenger/i))) {// 微信// console.log('微信环境-2')return WECHAT_ENV.wechat;}
};// 判断是pc端还是移动端
export const isPC = !/Android|webOS|iPhone|iPod|BlackBerry|SymbianOS|Windows Phone/i.test(navigator.userAgent);/*** [changeSearch description]* @param  {[type]} oldName 需要修改的search字段* @param  {[type]} newStr 替换的新串* @param  {[type]} url 当前要替换的link地址  不传默认是  window.location.search* @return {[type]}         [description]*/
export const changeSearch = (oldName: string, newStr: string, url: string) => {const jsonObj: any = searchToJson(url || window.location.search);if (!oldName) {return url;}jsonObj[oldName] = newStr;const linkUrl = url.split('?')[0] + jsonToSearch(jsonObj);return linkUrl;
};/** 将url的search部分转化为json*  @param url:String -- url地址*  @param codeURI:Boolean -- 是否解码*/
export const searchToJson = (url = window.location.href, codeURI = false) => {let setUrl: string = url || '';const search = setUrl.split('?');let result = {};search.forEach((item, index) => {if (index !== 0) {result = item.split('&').reduce((obj, item) => {const arr = item.split('=');return { ...obj, [arr[0]]: codeURI ? decodeURIComponent(arr[1]) : arr[1] };}, result);}});return result;
};// 获取url参数
export const getUrlQueryString = (search: string, name: string) => {const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');const r = search.substr(1).match(reg);if (r !== null) {return r[2];}return '';
};

注意bug

背景:在使用到这个ssdk过程中(我使用js-weixin的包是1.2.0版本),我遇到了一个兼容Android和ios的问题,就是在调用jssdk无论是ios还是Android都可以调用,但是当在企业微信中使用到分享相关的api时,在ios是无法调用,在Android是没有问题的,
处理方案:更改调用的js-weixin包,将js-weixin包改为1.0.0的包 ,两个包地址是不一样的:
https://res.wx.qq.com/wwopen/js/jsapi/jweixin-1.0.0.js
https://res.wx.qq.com/open/js/jweixin-1.2.0.js

二、 处理系统分享出去自定标题、描述、背景及相关系统功能禁用等

场景一:使用系统携带的分享等功能,但自定义分享的标题、描述等内容

在使用系统带的分享功能时,需要自定义分享出去的背景、背景图、描述及分享的链接时,可以将下面的shareFuc导入,初始化(放componentDidMount或useEffect中)携带对应的参数即可。

 const shareObj = {title: '客户认证',desc: '客户自己完成认证',imgUrl: '',linkUrl: ``,//分享的链接,可携带一些参数};useEffect(() => {shareFuc(shareObj);// 下面封装已给出}, [userId]);

场景二:弃用系统自带的分享,自己写弹框,触发分享API

出现这个场景,一方面可能可客户需要,另一方面,可能是分享要进行埋点,记录分享出去的次数。
实现:写一个弹框(如下),将下面每个分享或转发调用相应的API

  1. 转发:shareAppMessage,微信好友:shareWechatMessage,微信朋友圈:shareTimeline,这几个直接使用wx.invoke即可
// 微信好友const forwardWeChat = () => {const shareConfig = {title, // 分享标题desc, // 分享描述link: "linkUrl", // 分享链接imgUrl: "imgUrl" , // 分享封面};console.log('企微-微信好友 shareConfig', shareConfig);wx.invoke('shareWechatMessage', shareConfig, (res: any) => {console.log('企微分享微信好友-分享回调', res);if (res.err_msg === 'shareWechatMessage:ok') {//}});};
  1. 企微朋友圈:shareToExternalMoments,群发客户:shareToExternalContact;群发客户群:shareToExternalChat,比微信下多了agentConfig处理,是因为这三个api需要配置客户联系功能与版本
import { JSApiList } from 'wx-jssdk';
import { agentConfig } from '@/utils/getTicket';// 上面有封装// 群发客户群const groupShareGroup = async (e: any) => {const arrAgent: JSApiList[] = ['shareToExternalChat'];agentConfig(arrAgent,() => {wxInvokeShare('shareToExternalChat', {title, // 分享标题desc,link: "linkUrl", // 分享链接imgUrl:"imgUrl", // 分享封面});},linkUrl);};
  • 注意
    问题:在我处理分享的时候踩到了一个坑,就是设置分享的imgUrl参数的时候写的是相对路径(…/…/…/images/common/shareIconImg.png),分享出去的图标无法显示
    原因:官方似乎没有给出解释,我觉得是不是图片是本地内,分享出去后,显示的这个内容并没有加载整个项目代码,只是作为参数传过去,由于是图片是相对路径,这样导致加载不到图片,
    处理方案:使用base64方法得到图片作为imgUrl参数

场景三:禁用个人微信或企微右上角分享等功能

见下列方法:

  • 企微:hiddenWxQyShareOption,使用API:wx.hideOptionMenu(), wx.showMenuItems(
  • 微信:hiddenWxShareOption ,使用:WeixinJSBridge.call(‘hideToolbar’); WeixinJSBridge.call(‘hideOptionMenu’);

函数封装shareFuc、企微和个人微信系统分享等功能禁用封装

// @ts-nocheck
import globalData from '@/config/globalData';
import keyDict from '@/config/keyDict';
import point from '@/config/point';
import { changeSearch, getEnv, searchToJson, getUrlQueryString, isPC, WECHAT_ENV  } from '@/utils';//上面公用方法
import ajax from '@/utils/ajax';
import { getTicket } from '@/utils/getTicket';// 上面定义封装处理
import { JSApiList } from 'wx-jssdk';interface IShareProps {title: string;desc: string;linkUrl: string;imgUrl: string;userId: string;cb?: () => void;
}interface IShareFunProps {shareObj: IShareProps;jsApiList: JSApiList[];maxRequestCount: number;
}/*** [shareFuc description]* @param  {[type]}  shareObj { title: 标题, desc: 描述, linkUrl: 分享地址(会对地址做拼接,默认不传为当前url), imgUrl: 分享icon, staffId: 经理id,拼接给分享地址}* @param  {Array}   [jsApiList=[微信api]]* @return {Promise}                [description]* @param  {type}   maxRequestCount 最大失败请求次数,默认 为 3次*/export const shareFuc = async (shareObj: IShareProps, jsApiList = [], maxRequestCount = 3) => {// console.log('shareObj===》', shareObj);const defaultJsApi: JSApiList[] = ['onMenuShareAppMessage', 'onMenuShareTimeline', 'getLocation', 'previewImage'];// 不支持在PC端微信使用js-sdk功能if (isPC) return;const ua = navigator.userAgent.match(/MicroMessenger\/([\d\\.]+)/i);const lowerWeChat = ua ? ua[1] < '6.7.2' : true;// 微信if (getEnv() === 2) {defaultJsApi.push('updateAppMessageShareData', 'updateTimelineShareData');}getTicket([...defaultJsApi, ...jsApiList],async (error: string) => {if (error === 'error') {// 过期或者签名错误   重新获取setTimeout(() => {if (maxRequestCount > 0) {maxRequestCount = --maxRequestCount;shareFuc(shareObj, jsApiList, maxRequestCount);}}, 1000);} else {// 企业微信(微信)下隐藏部分不需要的菜单功能--如分享到同事吧,收藏,转发,微信,朋友圈if (getEnv() === 2) {wx.hideOptionMenu();wx.showMenuItems({menuList: ['menuItem:copyUrl', // 复制链接],// wx.hideMenuItems({//   menuList: [//     // 'menuItem:setFont', // 字体//     // 'menuItem:openWithSafari', // Safari//     // 'menuItem:share:email', // 邮件//     // 'menuItem:openWithQQBrowser', // QQBrowser//     // 'menuItem:share:appMessage', // 转发//     // 'menuItem:share:timeline', // 朋友圈//     // 'menuItem:share:wechat', // 微信//   ], // 要隐藏的菜单项// });});}const { title = '', desc = '', imgUrl = '', linkUrl, cb } = shareObj;// 个人微信处理if (getEnv() === 2) {// 分享给朋友if (wx.updateAppMessageShareData && !lowerWeChat) {wx.updateAppMessageShareData({title, // 分享标题desc, // 分享描述link: linkUrl, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致imgUrl, // 分享图标success: () => {console.log('微信分享给朋友,新Api,没有成功回调');},});} else {wx.onMenuShareAppMessage({title, // 分享标题desc, // 分享描述link: linkUrl, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致imgUrl, // 分享图标type: undefined, // 分享类型,music、video或link,不填默认为linkdataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空success: () => {// 用户点击了分享后执行的回调函数console.log('微信分享给朋友,旧Api');},});}// 分享给朋友圈if (wx.updateTimelineShareData && !lowerWeChat) {wx.updateTimelineShareData({title, // 分享标题link: linkUrl, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致imgUrl, // 分享图标success: () => {// 设置成功console.log('微信分享给朋友圈,新Api');},});} else {wx.onMenuShareTimeline({title, // 分享标题link: linkUrl, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致imgUrl, // 分享图标success: () => {// 设置成功console.log('微信分享给朋友圈,旧Api');},});}} else {// 企业微信wx.onMenuShareAppMessage({title, // 分享标题desc, // 分享描述link: linkUrl, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致imgUrl, // 分享图标success: () => {},cancel: () => {},});wx.onMenuShareTimeline({title, // 分享标题link: linkUrl, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致imgUrl, // 分享图标success: () => {// 设置成功console.log('企业微信分享给朋友圈,旧Api');},cancel: () => {// 用户取消分享后执行的回调函数console.log('企业微信分享给朋友圈,旧Api,cancel');},});}// eslint-disable-next-line @typescript-eslint/prefer-optional-chaincb && cb();}},shareObj.linkUrl,false,maxRequestCount);
};/*** 禁止企业微信右上角分享(新版本企微是下方)* **/
let wxConfigTimer: number;
export const hiddenWxQyShareOption = () => {if (getEnv() === WECHAT_ENV.qyWechat) {wxConfigTimer && clearTimeout(wxConfigTimer);wxConfigTimer = window.setTimeout(() => {getTicket([], () => {console.log('企业微信环境屏蔽右上角分享');wx.hideOptionMenu();wx.showMenuItems({menuList: ['menuItem:copyUrl'], //保留复制链接});});}, 200);}
};/** 禁止微信右上角分享按钮 */
export const hiddenWxShareOption = () => {if (getEnv() === WECHAT_ENV.wechat) {console.log('禁用微信右上角分享和状态栏');if (typeof WeixinJSBridge === 'undefined') {// 这个可以禁用安卓系统的右上角分享     (只针对微信端)document.addEventListener('WeixinJSBridgeReady',function () {WeixinJSBridge.call('hideToolbar');WeixinJSBridge.call('hideOptionMenu');},false);} else {// 这个可以禁用ios系统的右上角分享      (只针对微信端)WeixinJSBridge.call('hideToolbar');WeixinJSBridge.call('hideOptionMenu');}}
};

JS接口列表

type JSApiList =| 'agentConfig'| 'updateAppMessageShareData'| 'updateTimelineShareData'| 'onMenuShareTimeline'| 'onMenuShareAppMessage'| 'onMenuShareQQ'| 'onMenuShareWeibo'| 'onMenuShareQZone'| 'startRecord'| 'stopRecord'| 'onVoiceRecordEnd'| 'playVoice'| 'pauseVoice'| 'stopVoice'| 'onVoicePlayEnd'| 'uploadVoice'| 'downloadVoice'| 'chooseImage'| 'previewImage'| 'uploadImage'| 'downloadImage'| 'translateVoice'| 'getNetworkType'| 'openLocation'| 'getLocation'| 'hideOptionMenu'| 'showOptionMenu'| 'hideMenuItems'| 'showMenuItems'| 'hideAllNonBaseMenuItem'| 'showAllNonBaseMenuItem'| 'closeWindow'| 'scanQRCode'| 'chooseWXPay'| 'openProductSpecificView'| 'addCard'| 'chooseCard'| 'openCard'| 'shareToExternalContact'| 'shareToExternalChat'| 'selectExternalContact'| 'navigateToAddCustomer';

前端wx-jssdk的使用及企微和微信下分享等功能自定义处理相关推荐

  1. 企微获客助手是什么?企微即将上线“获客助手”功能

    前言 3月22日企业微信在上海召开的私域代运营服务商会议,企微即将上线"获客助手"功能,即公司将自己的企业微信变成一个短链接,放置在微信内/外,如网页.APP.公众号.短信等. 客 ...

  2. 微信JS-SDK获取signature签名以及config配置(微信转发分享页面需要)

    Js代码 wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在p ...

  3. 企微有客户流失提醒功能吗?如何设置?

    企业微信和微信分为两个APP,也可以添加好友,例如去商场购物,加导购微信,或者关注公众号领取优惠券的时候,不出意外添加的都是企业微信,那么如果后续不需要了删除了对方,他们会收到提醒吗? 一般情况下,微 ...

  4. 企微SCRM软件圈量营销玩法拆解,私域用户裂变推荐这2种

    在企业私域"流量"及"留量"增长的过程中,企业微信已然成为了不可或缺的工具,2012年的公众号.2018年的抖音被错过了,那么我们就要好好珍惜如今的企微私域! ...

  5. 【前端企微开发流程】-企业微信-创建应用-开发-调试-发布

    创建应用 概述 在企业微信中,企业应用分为基础应用.第三方应用及自建应用 基础应用:企业微信本身自带的应用 第三方应用:由第三方服务商提供的办公应用套件,管理员可根据企业办公需要选择安装 自建应用:根 ...

  6. 【企业微信】JS-SDK引入实现向企微群聊发送图文消息

    一.需求 企业自建应用商城需要实现分享商品链接到群聊.(标题/描述/图片/点击跳转商品链接H5详情页),API(打开已有群聊并发送信息)可实现 企微文档 二.引入Js-sdk 方式一:链接引入 < ...

  7. 企微 - 使用jsdk的 openDefaultBrowser 实现打开默认浏览器

      今天应项目需求,要使用 企业微信的jsdk 写一个demo实现一进入页面就跳转默认浏览器的功能,一开始我直接使用的是wx.config来实现这个需求的,但是发现并不能实现这样的功能,跟大佬找了很多 ...

  8. 微信,企微静默登录授权

    这里写自定义目录标题 欢迎使用Markdown编辑器 新的改变 功能快捷键 合理的创建标题,有助于目录的生成 如何改变文本的样式 插入链接与图片 如何插入一段漂亮的代码片 生成一个适合你的列表 创建一 ...

  9. 前端低代码平台腾讯云微搭使用文档

    腾讯云微搭 调研报告 之前作者有写过一个同类低代码平台调研报告 H5-Dooring 点击查看,这次我们去尝试使用腾讯系低代码平台,文中也会增加两者之间的差异对比和使用体验上的区别. 1. 简介 1. ...

最新文章

  1. MySQL之查看数据库编码
  2. Pytorch框架的深度学习优化算法集(优化中的挑战)
  3. Spring MVC 概述
  4. java中servletcontext_Java中的ServletContext对象
  5. 书评:卓有成效的ThoughtWorks程序员的45个习惯
  6. Android富文本处理
  7. 阿里云前端周刊 - 第 14 期
  8. 后端技术:Java 泛型 T,E,K,V的含义,看完本文你就明白了?
  9. InVEST model中生境质量
  10. java 数据库 模板_JAVA操作数据库的模板方法
  11. JVM总结(一):概念----(无节操转载,潜心学习)
  12. 关于使用OpenXml向Excel插入数据的一点总结
  13. web server linux,GitHub - markparticle/WebServer: C++ Linux WebServer服务器
  14. 数字电路设计之数字电路工程师面试集锦
  15. 关于SOLIDWORKS缩略图预览失败的解决方案
  16. Latex声调(一声、二声、三声等)
  17. python基本写法_Python的表达式写法
  18. 爱存在用计算机怎么弹,qiukepingvf
  19. 无交互作用的正交设计
  20. Docker——安装和启动

热门文章

  1. 【字节青训营】微服务架构原理核心服务治理与具体实践
  2. Mac virtualBox 微软官方Win7镜像下载安装全过程
  3. 郭光灿院士--奇妙的量子世界笔记1(量子世界和经典世界区联系)
  4. ArcGIS制图之桂林山水
  5. 水库/河道水位监测系统产品介绍
  6. 第二次创客沙龙暨平台联合探讨会PPT-18.5.12
  7. linux映射关系文件,Linux磁盘映射DM
  8. HTML三个常用代码-空格,标签,回车
  9. MPB:西湖大学鞠峰组微生物群落胞内胞外吸附胞外游离水环境DNA的分离提取
  10. 新-新古典综合给出的正统答案-中国视角下的宏观经济