当今社会,微信登录、QQ登录、抖音登录等等三方登录已经层出不穷,学会三方登录势在必行。
微信登录要认证开发者,必须为企业,个人不行,而且还要交300块钱。一听到钱,白嫖党的钱是不可能被赚的,不学微信登录。
QQ登录也要申请、微博登录也要申请。
还好Gitee给力,申请轻轻松松,谁都能轻松让Gitee作为第三方登录,此次我们就讲解Gitee来登录ry。其实其他的登录也是基本上一样的。
JustAuth能让我们第三方登录写少一些代码,它包装了国内外30多种三方登录。
学习JustAuth网站:
https://mp.weixin.qq.com/s?__biz=MzA3NDk3OTIwMg==&mid=2450633106&idx=1&sn=131e39d52347dffefbd4227b18b794bf&chksm=8892937fbfe51a69950cb0769e2b22d04217254b0e79cdcee4204aedb2007627ab6511b58355&token=29120304&lang=zh_CN#rd
https://justauth.wiki/guide/quickstart/how-to-use/#%E4%BD%BF%E7%94%A8%E6%AD%A5%E9%AA%A4
使用JustAuth总共分三步(这三步也适合于JustAuth支持的任何一个平台):
1、申请注册第三方平台的开发者账号。
我们找到gitee的设置,进入第三方应用,如下:
出来界面如下:
我现在是已经新建好了应用,大家是没有ruoyi-test。大家可以新建自己的应用:
应用主页随便填一个自己的应用页面即可。但是应用回调不能乱填,当我们gitee登录成功之后,gitee会自动跳转到应用回调地址,并且gitee会带上code,利用code可以得到所登录gitee用户信息。
2、创建第三方平台的应用,获取配置信息(accessKey, secretKey, redirectUri)。
上面我们已经创建了应用,自然有了这三个值。
3、使用该工具实现授权登陆。
利用工具先要引入依赖:
me.zhyd.oauth
JustAuth
1.16.5
依赖引入到核心框架(framework)下。
接下来改login.vue,如下:
...省略其他代码 :loading="loading"size="medium"type="primary"style="width:100%;"@click.native.prevent="handleLogin">登 录
登 录 中...
立即注册
以上,我们添加的代码是:
有了该代码,页面呈现的样子是:
关于图片LOGO下载地址请去随便找一个,或者群文件找找。然后我们看一下点击事件:
giteeLogin() {
PreLoginByGitee().then(res => {
Cookies.set("user-uuid", res.uuid)
window.location = res.authorizeUrl
})
},
以上是在methods中,我们看到直接请求了PreLoginByGitee:
export function PreLoginByGitee() {
return request({
url: '/PreLoginByGitee',
headers: {
isToken: false
},
method: 'get',
})
}
对应的后端接口:
@GetMapping("/PreLoginByGitee")public AjaxResult PreLoginByGitee() {AjaxResult ajax = AjaxResult.success();AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder().clientId("1712ae8e8105c0005da36339ed72c1a6aae86322fc64d431dadcaa275a14be45").clientSecret("87fb8f83efc04fcd85696d5461c80ce6f98c94845539f9612024e81302111a05").redirectUri("http://localhost/callback").build());String uuid = IdUtils.fastUUID();String authorizeUrl = authRequest.authorize(uuid);//存储ajax.put("authorizeUrl", authorizeUrl);ajax.put("uuid", uuid);return ajax;
}
以上的代码是生成跳转路径。生成一个gitee的路径,在该页面gitee只要登录完成,gitee程序会自动跳转到我们之前设置好的回调地址。这里我们发现我们一直拿着一个uuid在传来传去,还传去了前端,它有什么鸟用呢?
authRequest.authorize(uuid)用到了uuid,并且后面要执行:
authRequest.login(AuthCallback.builder().state(uuid).code(code).build());
要保证俩uuid为同一个,所以uuid才传来传去。
关于本篇文章,文字有点难以描述,请大家看视频。
上面window.location = res.authorizeUrl
让我们进入了如下界面:
只要我们登陆好gitee,gitee会自动跳转到我们的回调地址。此时回调到了前端的下面的路由:
我们还提前准备好了组件:
正在加载中...
import Cookies from "js-cookie";
export default {
name: "loginByGitee",
data() {
return {
loading: true
}
},
mounted() {
this.loading = true;
console.log("uuid", Cookies.get("user-uuid"))
const formBody = {
uuid: Cookies.get("user-uuid"),
code: this.$route.query.code
}
this.$store.dispatch("LoginByGitee", formBody).then(() => {
this.$router.push({path: this.redirect || "/"}).catch(() => {
});
}).catch(() => {
this.loading = false;
});
}
}
从上面可以看到,又带着了uuid,执行LoginByGitee方法,如下:
LoginByGitee({commit}, body) {
return new Promise((resolve, reject) => {
loginByGitee(body.code, body.uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
继续追代码:
export function loginByGitee(code, uuid) {
const data = {
code,
source: "Gitee",
uuid
}
return request({
url: '/loginByGitee',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
追到底了,调用的后端的:
@PostMapping("/loginByGitee")public AjaxResult loginByGitee(@RequestBody LoginByOtherSourceBody loginByOtherSourceBody) {AjaxResult ajax = AjaxResult.success();String token = loginService.loginByOtherSource(loginByOtherSourceBody.getCode(), loginByOtherSourceBody.getSource(), loginByOtherSourceBody.getUuid());ajax.put(Constants.TOKEN, token);return ajax;}
service层:
public String loginByOtherSource(String code, String source, String uuid) {//先到数据库查询这个人曾经有没有登录过,没有就注册// 创建授权requestAuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder().clientId("1712ae8e8105c0005da36339ed72c1a6aae86322fc64d431dadcaa275a14be45").clientSecret("87fb8f83efc04fcd85696d5461c80ce6f98c94845539f9612024e81302111a05").redirectUri("http://localhost/callback").build());AuthResponselogin = authRequest.login(AuthCallback.builder().state(uuid).code(code).build()); System.out.println(login);//先查询数据库有没有该用户AuthUser authUser = login.getData();SysUser sysUser = new SysUser();sysUser.setUserName(authUser.getUsername());sysUser.setSource(authUser.getSource());ListsysUsers = userService.selectUserListNoDataScope(sysUser); if (sysUsers.size() > 1) {throw new ServiceException("第三方登录异常,账号重叠");} else if (sysUsers.size() == 0) {//相当于注册sysUser.setNickName(authUser.getNickname());sysUser.setAvatar(authUser.getAvatar());sysUser.setEmail(authUser.getEmail());sysUser.setRemark(authUser.getRemark());userService.registerUserAndGetUserId(sysUser);AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.REGISTER,MessageUtils.message("user.register.success")));} else {sysUser = sysUsers.get(0);}AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));//注册成功或者是已经存在的用户LoginUser loginUser =new LoginUser(sysUser.getUserId(), sysUser.getDeptId(), sysUser, permissionService.getMenuPermission(sysUser));recordLoginInfo(loginUser.getUserId());// 生成tokenreturn tokenService.createToken(loginUser);
}
似乎如此就完事了。但实际上有很多细节我都没说。在视频里面说。
细节1:前端白名单放行:
const whiteList = ['/login', "/callback", '/auth-redirect', '/bind', '/register']
细节2:字段source。如下代码可以获得登录source:
AuthUser authUser = login.getData();
source表示登录平台,如微信登录,支付宝登录,因为要确定用户的唯一性。username在不同的平台可能会重复,但是username+source就不会重复了。
细节3:新写了一个查询方法,如下:
userService.selectUserListNoDataScope(sysUser);
@Overridepublic ListselectUserListNoDataScope(SysUser user) { return userMapper.selectUserList(user);
}
新重写一个查询的原因是原来的的方法有数据权限:
细节4:安全配置放行了若干接口:
.antMatchers("/login", "/register", "/captchaImage", "/loginByGitee", "/PreLoginByGitee").anonymous()
细节5:当我们第一次登录成功,什么权限都没有,需要admin设置一下权限,或者在注册的时候给新用户一个“普通角色”。
细节6:新登录用户,头像根本显示不了,改成如下就能显示了【文件user.js】:
let avatar;if (user.avatar == "" || user.avatar == null) {avatar = require("@/assets/images/profile.jpg")} else if (user.avatar.startsWith("http")) {avatar = user.avatar} else {avatar = process.env.VUE_APP_BASE_API + user.avatar;
}
其他细节不再赘述。
权限和第三方登录确实令人头疼,我们来学一点简单一点的。
另外,如果各位有属于自己的域名和ICP/IP备案,布置一个作业,自行实现第三方QQ登录。
我们所说的包名修改,是一次性修改ruoyi的全部包名,因为发现很多人有这样的需求,下载别人的代码,想要改成自己公司的包名,结果一改就各种出现问题,程序都起不来了,气死。
给大家介绍一款工具,下载它能一键修改:
https://gitee.com/lpf_project/common-tools
https://gitee.com/lpf_project/common-tools/releases/tag/V4-20220517
通过修改器可以一键修改包名和外层文件夹名字。这里文字不再赘述。请直接看视频演示。
发现许多情况下,若干同学们都想新建一个模块,然后不影响ry原先的代码,下面就演示步骤:
对准项目根文件右键新建选择模块:
然后新建模块,注意父模块要选ruoyi,名称随便取一个,但是要符合起名习惯,我现在取名字为wqj:
值得注意的是,新建的模块需要被admin模块所引入:
如上,这样就可以了,然而,发现ruoyi的其他依赖项都没有加版本号,它们都从父模块继承了版本!下面我们去父模块写好版本!:
写好之后,子模块就可以不写版本了。
左上角也出现了一个继承的标志。
根包的建立
因为RuoYiApplication在com.ruoyi包下,所以我们也需要手动建立com.ruoyi包,不然自己写的类无法注入Spring容器。
依赖
关于依赖,说的是什么呢?我新建的ruoyi-wqj可能或多或少依赖于ruoyi-framework或者ruoyi-common,当我们引入依赖的时候,一定不要造成循环依赖。
循环依赖会造成打包直接报错。循环依赖就是说A依赖B,B又依赖A。
当我们自己新建的模块需要依赖ruoyi-system和ruoyi-framework的时候,只需要引入ruoyi-framework即可,因为ruoyi-framework里面引入了ruoyi-system。但是再引也不会报错,因为没有循环依赖。
新模块竟然没有Spring,需要引入ruoyi-common:
当我们引入ruoyi-common后一切正常:
经过启动测试,一切正常!
这里为了让该接口可以匿名访问,我们可以通过一个注解完事:
然后可以访问了:
大家应该已经习惯我的教学套路,很多时候都是先使用,然后讲述原理。
上节课我们使用了注解@Anonymous,然后接口就可以直接被访问到了,不用token!不用token!不用token!。
我们一般知道,注解是给程序看的,给机器看的,当然也是给程序员看的。注解如果没有注解解析器(注解处理器,注解解释器),那么注解就没有什么作用。所以@Anonyous一定是在某个地方被干嘛干嘛了!
先来看一波@Anonyous的源码:
/*** 匿名访问不鉴权注解** @author ruoyi*/@Target({ ElementType.METHOD, ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Anonymous{
}
从源码可以看到,它可以放到类上,或者方法上。
那么我们就可以这样想:放到类上,该类所有方法都可以匿名访问;放到方法上,那么就该方法可以被匿名访问。
下面直接上注解解析器:
/*** 设置Anonymous注解允许匿名访问的url** @author ruoyi*/@Configurationpublic class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware{private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");private ApplicationContext applicationContext;private Listurls = new ArrayList<>(); public String ASTERISK = "*";@Overridepublic void afterPropertiesSet(){RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);Mapmap = mapping.getHandlerMethods(); map.keySet().forEach(info -> {HandlerMethod handlerMethod = map.get(info);// 获取方法上边的注解 替代path variable 为 *Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);Optional.ofNullable(method).ifPresent(anonymous -> info.getPatternsCondition().getPatterns().forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));// 获取类上边的注解, 替代path variable 为 *Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);Optional.ofNullable(controller).ifPresent(anonymous -> info.getPatternsCondition().getPatterns().forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));});}@Overridepublic void setApplicationContext(ApplicationContext context) throws BeansException{this.applicationContext = context;}public ListgetUrls() {return urls;}public void setUrls(Listurls) {this.urls = urls;}
}
从以上的代码我们可以发现,这个类实现了两个接口。
InitializingBean接口实现之后,初始化bean的时候会执行接口中的方法afterPropertiesSet。
ApplicationContextAware接口实现之后,可以重写setApplicationContext方法,该方法可以拿到Spring的上下文,相当于Spring容器在手,天下我有。
所以我们打一个断点,然后重新启动项目,断点就会卡住。然后直接debug讲述。
可以很容易发现,无非是先拿到一个包含所有映射和HandlerMethod的(只读)映射。
通过拿到映射后,获得每个映射的HandlerMethod,也就是获得处理它的方法。然后看看该方法有没有Anonymous.class注解,有的话保存成method变量,然后后续校验后加入到urls的ArrayList中保存起来供后续使用。
这里涉及到一个正则表达式,处理了特殊情况。当我们的url为 /{getget} 的时候,会被替换为/*,当我们的url为/{getget}/{abc}的时候,会被替换为/*/*。
正则表达式为:\{(.*?)\}
相关链接:https://segmentfault.com/q/1010000010680178。
后续在SecurityConfig中把访问权限改成了permitAll:
// 注解标记允许匿名访问的urlExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAl
讲了好多难的东西,接下来我们来轻松一点。
讲一下前端的通用方法。
所谓通用方法,就是随时能调用的方法。
例如:
this.$tab.openPage("用户管理", "/system/user");
this.$modal.msg("默认反馈");
this.$auth.hasPermi("system:user:add");
this.$auth.hasRole("admin");
this.$cache.local.set('key', 'local value')
this.$download.name(name);
为什么说这些方法可以随时调用呢?
首先,ry准备好了插件:
import tab from './tab'import auth from './auth'import cache from './cache'import modal from './modal'import download from './download'export default {install(Vue) {// 页签操作Vue.prototype.$tab = tab// 认证对象Vue.prototype.$auth = auth// 缓存对象Vue.prototype.$cache = cache// 模态框对象Vue.prototype.$modal = modal// 下载文件Vue.prototype.$download = download}
}
然后在main.js调用了安装插件方法:
...省略其他代码import plugins from './plugins' // plugins...省略其他代码Vue.use(plugins)
...省略其他代码
我们知道,Vue.use(plugins),会去自动调用plugins中的install方法,在install方法中,发现Vue.prototype上面装了一些东西。
到此我们明白了,Vue原型上挂载了,所以随时能访问到。
为什么原型挂载就能访问到呢?:
以上是尚硅谷的Vue视频曾提到的一个内置关系。如果不明白Vue原型挂载为啥能访问的只能自行去看视频了。
http://doc.ruoyi.vip/ruoyi-vue/document/qdsc.html#tab%E5%AF%B9%E8%B1%A1
下面我们亲自编码测试一下如何使用即可。
1、打开新页面
// 单纯打开新页面openTZGG() {this.$tab.openPage("打开的通知公告", "/system/notice");},// 打开页面之后,并且做一点其他事情。openTZGGAndWindows() {this.$tab.openPage("打开的通知公告并且弹窗", "/system/notice").then(() => {this.$modal.msg("我是弹窗")})
}
看看源码:
// 添加tab页签openPage(title, url, params) {var obj = { path: url, meta: { title: title } }store.dispatch('tagsView/addView', obj);return router.push({ path: url, query: params });
},
从上面的源码可以看出,做了两件事。
第一件事:
调用Vuex的tagsView/addView方法,并传参数obj。该方法是为了把新打开的页签添加到Vuex维护的页签数组visitedViews。
当添加成功,组件TagsView的计算属性visitedViews变化,页面重新渲染,页签自然多一个。
第二件事:
第二件事就是将路由做简单的跳转。
updateMyself() {//修改自己const obj = Object.assign({}, this.$route, {title: "自定义标题"})this.$tab.updatePage(obj);},updateTZGG() {// 修改别的const obj = Object.assign({}, {path: "/system/notice"}, {title: "自定义标题222"})this.$tab.updatePage(obj).then(() => {this.$modal.msg("修改页签完毕")})
},
//关闭当前并且打开新页面closeAndOpen() {const obj = {path: "/system/user"};this.$tab.closeOpenPage(obj);},//关闭当前并回到首页closeAndGoIndex() {this.$tab.closePage();},//关闭指定的页面closeSpecifyPage() {const obj = {path: "/system/user"};this.$tab.closePage(obj);
}
//刷新当前页签
refreshPage() {
this.$tab.refreshPage();
}
刷新页签相当于页面关闭,重新打开。
//关闭所有页签
closeAll() {
this.$tab.closeAllPage();
},
//关闭所有页签closeAll() {this.$tab.closeAllPage().then(res => {this.$tab.openPage("shouye", "/index")});
},
//关闭右侧closeRight() {this.$tab.closeRightPage();
},
//关闭其他closeOther() {this.$tab.closeOtherPage();
}
官方文档已经很齐备了:
http://doc.ruoyi.vip/ruoyi-vue/document/qdsc.html#modal%E5%AF%B9%E8%B1%A1
下面我们亲自编码测试一下如何使用即可。
提供成功、警告和错误等反馈信息
msg() {this.$modal.msg("默认反馈");// this.$modal.msgError("错误反馈");// this.$modal.msgSuccess("成功反馈");// this.$modal.msgWarning("警告反馈");
},
提供成功、警告和错误等提示信息
alert() {this.$modal.alert("默认提示");// this.$modal.alertError("错误提示");// this.$modal.alertSuccess("成功提示");// this.$modal.alertWarning("警告提示");
},
提供成功、警告和错误等通知信息
notify() {
this.$modal.notify("默认通知");
// this.$modal.notifyError("错误通知");
// this.$modal.notifySuccess("成功通知");
// this.$modal.notifyWarning("警告通知");
},
提供确认窗体信息
confirm() {this.$modal.confirm('你确定要确定吗').then(function () {}).then(() => {this.$modal.notify("点了确定")}).catch(() => {this.$modal.notify("点了取消")});
},
提供遮罩层信息
//遮罩层startLoading() {// 打开遮罩层this.$modal.loading("正在Loading,请稍后...");wait5Second().then(res => {console.log(res)this.$modal.closeLoading();})},//如若已经loading则只能程序自己调用stopLoading() {this.$modal.closeLoading();
}
该对象用于鉴权。
官方说明:
http://doc.ruoyi.vip/ruoyi-vue/document/qdsc.html#auth%E5%AF%B9%E8%B1%A1
验证用户权限
validMyPermission() {console.log(this.$auth.hasPermi("system:user:list"))
}
验证用户角色
validMyRole() {this.$modal.notify(this.$auth.hasRole("admin") + "")
}
验证用户角色的源码:
function authPermission(permission) {const all_permission = "*:*:*";const permissions = store.getters && store.getters.permissionsif (permission && permission.length > 0) {return permissions.some(v => {return all_permission === v || v === permission})} else {return false}
}
以上就是检查用户传入的权限到底有没有。
$cache看看就可以了,甚至笔者都不想演示。
$cache对象用于处理缓存。我们并不建议您直接使用sessionStorage或localStorage,因为项目的缓存策略可能发生变化,通过$cache对象做一层调用代理则是一个不错的选择。$cache提供session和local两种级别的缓存,如下:
// local 普通值this.$cache.local.set('key', 'local value')console.log(this.$cache.local.get('key')) // 输出'local value'// session 普通值this.$cache.session.set('key', 'session value')console.log(this.$cache.session.get('key')) // 输出'session value'// local JSON值this.$cache.local.setJSON('jsonKey', { localProp: 1 })console.log(this.$cache.local.getJSON('jsonKey')) // 输出'{localProp: 1}'// session JSON值this.$cache.session.setJSON('jsonKey', { sessionProp: 1 })console.log(this.$cache.session.getJSON('jsonKey')) // 输出'{sessionProp: 1}'// 删除值this.$cache.local.remove('key')
this.$cache.session.remove('key')
$download对象用于文件下载,它定义在plugins/download.js文件中,它有如下方法
根据名称下载download路径下的文件
downloadByName() {const name = "1.txt";const isDelete = true;// 默认下载方法this.$download.name(name);// 下载完成后是否删除文件// this.$download.name(name, isDelete);
}
以上的this.$download.name(name)方法在ry中它自己却没有用过。
那么如何正确使用它呢?:
testDownload(){//通过sheng_cheng_wen_jian方法去调用后端某一个接口,后端把文件保存在ruoyi/download/下面,// 然后后端返回文件名,然后前端收到文件名之后才去真正的下载。//然而既然要重新调用第二次下载接口,为什么不一次性解决呢?所以这个接口不是那么好用。sheng_cheng_wen_jian().then(res=>{this.$download.name(res.name)})
}
根据名称下载upload路径下的文件
我们可以看到,官网就发了下面这段代码:
const resource = "/profile/upload/2021/09/27/be756b96-c8b5-46c4-ab67-02e988973090.png";// 默认方法this.$download.resource(resource);
事实上,通过该方法可以下载到ruoyi/upload文件夹下面的若干文件。
我们观察到,作者的例子中带着日期什么的,其实这些数据都是存在数据库中的。
有时候我们要存用户的头像怎么办呢?我们就可以把用户头像的URI存到数据库中,然后通过该URI直接在前端展示,当然也可以通过resource方法下载下来。下面我们通过一个简单的案例来说明。
CREATE TABLE `ruoyi-vue`.`test` (`id` bigint NOT NULL COMMENT '主键',`uri` varchar(255) NULL COMMENT 'URI',PRIMARY KEY (`id`));-- 下面因为忘记主键自增,回来改的数据库。ALTER TABLE `ruoyi-vue`.`test`MODIFY COLUMN `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键' FIRST;
请看视频。
以上图片是我生成代码改的一些参数。
经过测试,页面正常,也能显示图片:
此时我们发现,network的请求响应就有一个路径!!!:
发现就在下面的路径中有我们要的图片:
http://localhost/dev-api/profile/upload/2022/09/08/favicon(1)_20220908150152A001.jpg
该路径为何能显示图片呢?
首先是dev-api走的前端代理,代理到了后端,并且削掉了dev-api的前缀。
找到后端后,后端静态资源是直接映射的。
@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry){/** 本地文件上传路径 */registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");/** swagger配置 */registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
}
以上代码是说:
如果收到 /profile/.....的请求,就去D:/ruoyi/uploadPath/下面去找对应的资源文件。
根据请求地址下载zip包
ry自己用过该方法,当我们生成代码的时候可以下载,对应的下载zip包:
/** 生成代码操作 */handleGenTable(row) {const tableNames = row.tableName || this.tableNames;if (tableNames == "") {this.$modal.msgError("请选择要生成的数据");return;}if(row.genType === "1") {genCode(row.tableName).then(response => {this.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath);});} else {this.$download.zip("/tool/gen/batchGenCode?tables=" + tableNames, "ruoyi");}
},
需要保证请求的地址返回的输出流是zip包。
更多文件下载操作
template:downloadByName 自定义文本保存1 自定义文本保存2 自定义文本保存3 自定义文本保存4 methods:hello() {// 自定义文本保存const blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});this.$download.saveAs(blob, "hello world.txt");},hello1() {// 自定义文件保存const file = new File(["Hello, world!"], "hello world.txt", {type: "text/plain;charset=utf-8"});this.$download.saveAs(file);},hello2() {// 自定义data数据保存const data = "中国共产党是世界第一大党!";const blob = new Blob([data], {type: 'text/plain;charset=utf-8'})this.$download.saveAs(blob, "2.txt")},hello3() {// 根据地址保存文件this.$download.saveAs("https://ruoyi.vip/images/logo.png", "logo.jpg");
}