Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/1.8.0-uniapp

 Conflicts:
	yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/UserController.java
	yudao-ui-admin/src/components/ImageUpload/index.vue
This commit is contained in:
YunaiV
2022-05-29 11:05:33 +08:00
276 changed files with 13777 additions and 5274 deletions

View File

@ -1,4 +1,6 @@
import request from '@/utils/request'
import {getRefreshToken} from "@/utils/auth";
import service from "@/utils/request";
// 登录方法
export function login(username, password, code, uuid) {
@ -26,7 +28,7 @@ export function getInfo() {
// 退出方法
export function logout() {
return request({
url: '/system/logout',
url: '/system/auth/logout',
method: 'post'
})
}
@ -99,3 +101,48 @@ export function smsLogin(mobile, code) {
}
})
}
// 刷新访问令牌
export function refreshToken() {
return service({
url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken(),
method: 'post'
})
}
// ========== OAUTH 2.0 相关 ==========
export function getAuthorize(clientId) {
return request({
url: '/system/oauth2/authorize?clientId=' + clientId,
method: 'get'
})
}
export function authorize(responseType, clientId, redirectUri, state,
autoApprove, checkedScopes, uncheckedScopes) {
// 构建 scopes
const scopes = {};
for (const scope of checkedScopes) {
scopes[scope] = true;
}
for (const scope of uncheckedScopes) {
scopes[scope] = false;
}
// 发起请求
return service({
url: '/system/oauth2/authorize',
headers:{
'Content-type': 'application/x-www-form-urlencoded',
},
params: {
response_type: responseType,
client_id: clientId,
redirect_uri: redirectUri,
state: state,
auto_approve: autoApprove,
scope: JSON.stringify(scopes)
},
method: 'post'
})
}

View File

@ -0,0 +1,44 @@
import request from '@/utils/request'
// 创建 OAuth2 客户端
export function createOAuth2Client(data) {
return request({
url: '/system/oauth2-client/create',
method: 'post',
data: data
})
}
// 更新 OAuth2 客户端
export function updateOAuth2Client(data) {
return request({
url: '/system/oauth2-client/update',
method: 'put',
data: data
})
}
// 删除 OAuth2 客户端
export function deleteOAuth2Client(id) {
return request({
url: '/system/oauth2-client/delete?id=' + id,
method: 'delete'
})
}
// 获得 OAuth2 客户端
export function getOAuth2Client(id) {
return request({
url: '/system/oauth2-client/get?id=' + id,
method: 'get'
})
}
// 获得 OAuth2 客户端分页
export function getOAuth2ClientPage(query) {
return request({
url: '/system/oauth2-client/page',
method: 'get',
params: query
})
}

View File

@ -0,0 +1,18 @@
import request from '@/utils/request'
// 获得访问令牌分页
export function getAccessTokenPage(query) {
return request({
url: '/system/oauth2-token/page',
method: 'get',
params: query
})
}
// 删除访问令牌
export function deleteAccessToken(accessToken) {
return request({
url: '/system/oauth2-token/delete?accessToken=' + accessToken,
method: 'delete'
})
}

View File

@ -1,18 +0,0 @@
import request from '@/utils/request'
// 查询在线用户列表
export function list(query) {
return request({
url: '/system/user-session/page',
method: 'get',
params: query
})
}
// 强退用户
export function forceLogout(tokenId) {
return request({
url: '/system/user-session/delete?id=' + tokenId,
method: 'delete'
})
}

View File

@ -1,7 +1,7 @@
<template>
<div>
<el-upload
:action="uploadUrl"
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
@ -10,7 +10,7 @@
:headers="headers"
style="display: none"
ref="upload"
v-if="this.type == 'url'"
v-if="this.type === 'url'"
>
</el-upload>
<div class="editor" ref="editor" :style="styles"></div>
@ -22,7 +22,7 @@ import Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { getToken } from "@/utils/auth";
import { getAccessToken } from "@/utils/auth";
export default {
name: "Editor",
@ -60,10 +60,8 @@ export default {
},
data() {
return {
uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken()
},
uploadFileUrl: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
headers: { Authorization: "Bearer " + getAccessToken() }, // 设置上传的请求头部
Quill: null,
currentValue: "",
options: {
@ -126,7 +124,7 @@ export default {
const editor = this.$refs.editor;
this.Quill = new Quill(editor, this.options);
// 如果设置了上传地址则自定义图片上传事件
if (this.type == 'url') {
if (this.type === 'url') {
let toolbar = this.Quill.getModule("toolbar");
toolbar.addHandler("image", (value) => {
this.uploadType = "image";
@ -172,11 +170,13 @@ export default {
// 获取富文本组件实例
let quill = this.Quill;
// 如果上传成功
if (res.code == 200) {
// edit by 芋道源码
if (res.code === 200 || res.code === 0) {
// 获取光标所在位置
let length = quill.getSelection().index;
// 插入图片 res.url为服务器返回的图片地址
quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName);
// edit by 芋道源码
quill.insertEmbed(length, "image", res.data);
// 调整光标到最后
quill.setSelection(length + 1);
} else {

View File

@ -28,7 +28,7 @@
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<el-link :href="`${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
@ -40,7 +40,7 @@
</template>
<script>
import { getToken } from "@/utils/auth";
import { getAccessToken } from "@/utils/auth";
export default {
name: "FileUpload",
@ -72,11 +72,8 @@ export default {
return {
number: 0,
uploadList: [],
baseUrl: process.env.VUE_APP_BASE_API,
uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
uploadFileUrl: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
headers: { Authorization: "Bearer " + getAccessToken() }, // 设置上传的请求头部
fileList: [],
};
},
@ -121,8 +118,7 @@ export default {
}
const isTypeOk = this.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
return !!(fileExtension && fileExtension.indexOf(type) > -1);
});
if (!isTypeOk) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
@ -152,7 +148,8 @@ export default {
},
// 上传成功回调
handleUploadSuccess(res) {
this.uploadList.push({ name: res.fileName, url: res.fileName });
// edit by 芋道源码
this.uploadList.push({ name: res.data, url: res.data });
if (this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
@ -181,7 +178,7 @@ export default {
for (let i in list) {
strs += list[i].url + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
}
}
};

View File

@ -1,21 +1,21 @@
<template>
<div class="component-upload-image">
<el-upload
multiple
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
name="file"
:on-remove="handleRemove"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}"
multiple
:action="uploadFileUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
name="file"
:on-remove="handleRemove"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}"
>
<i class="el-icon-plus"></i>
</el-upload>
@ -29,21 +29,21 @@
</div>
<el-dialog
:visible.sync="dialogVisible"
title="预览"
width="800"
append-to-body
:visible.sync="dialogVisible"
title="预览"
width="800"
append-to-body
>
<img
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
/>
</el-dialog>
</div>
</template>
<script>
import {getToken} from "@/utils/auth";
import { getAccessToken } from "@/utils/auth";
export default {
props: {
@ -76,12 +76,8 @@ export default {
dialogImageUrl: "",
dialogVisible: false,
hideUpload: false,
// todo /infra/file/upload 返回的都是带 host 的可访问地址, baseUrl 有点没必要
baseUrl: process.env.VUE_APP_BASE_API,
uploadImgUrl: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
uploadFileUrl: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
headers: { Authorization: "Bearer " + getAccessToken() }, // 设置上传的请求头部
fileList: []
};
},
@ -94,12 +90,8 @@ export default {
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
// 不带有 http 的路径, 才拼接 baseUrl.
if (item.indexOf("http") === -1) {
item = {name: this.baseUrl + item, url: this.baseUrl + item};
} else {
item = {name: item, url: item};
}
// edit by 芋道源码
item = { name: item, url: item };
}
return item;
});
@ -129,7 +121,8 @@ export default {
},
// 上传成功回调
handleUploadSuccess(res) {
this.uploadList.push({name: res.data.fileName, url: res.data.fileUrl});
// edit by 芋道源码
this.uploadList.push({ name: res.data, url: res.data });
if (this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
@ -188,10 +181,9 @@ export default {
let strs = "";
separator = separator || ",";
for (let i in list) {
// 由于 infra-file 返回带有 host , 不需要替换 baseUrl // .replace(this.baseUrl, "")
strs += list[i].url + separator;
strs += list[i].url.replace(this.baseUrl, "") + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
}
}
};

View File

@ -1,68 +0,0 @@
<template>
<div class="component-upload-image">
<el-upload
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:on-error="handleUploadError"
name="file"
:show-file-list="false"
:headers="headers"
style="display: inline-block; vertical-align: top"
>
<img v-if="value" :src="value" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
export default {
components: {},
data() {
return {
uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
};
},
props: {
value: {
type: String,
default: "",
},
},
methods: {
handleUploadSuccess(res) {
this.$emit("input", res.url);
this.loading.close();
},
handleBeforeUpload() {
this.loading = this.$loading({
lock: true,
text: "上传中",
background: "rgba(0, 0, 0, 0.7)",
});
},
handleUploadError() {
this.$message({
type: "error",
message: "上传失败",
});
this.loading.close();
},
},
watch: {},
};
</script>
<style scoped lang="scss">
.avatar {
width: 100%;
height: 100%;
}
</style>

View File

@ -499,7 +499,8 @@ export const selectComponents = [
__slot__: {
'list-type': true
},
action: 'https://jsonplaceholder.typicode.com/posts/',
// action: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
action: '/infra/file/upload', // 请求地址
disabled: false,
accept: '',
name: 'file',

View File

@ -1,6 +1,7 @@
<script>
import { deepClone } from '@/utils/index'
import render from '@/components/render/render.js'
import {getAccessToken} from "@/utils/auth";
const ruleTrigger = {
'el-input': 'blur',
@ -79,10 +80,51 @@ function formBtns(h) {
}
function renderFormItem(h, elementList) {
const that = this
const data = this[this.formConf.formModel]
// const formRef = that.$refs[that.formConf.formRef] // 这里直接添加有问题,此时还找不到表单 $refs
return elementList.map(scheme => {
const config = scheme.__config__
const layout = layouts[config.layout]
// edit by 芋道源码,解决 el-upload 上传的问题
// 参考 https://github.com/JakHuang/form-generator/blob/master/src/components/parser/example/Index.vue 实现
const vModel = scheme.__vModel__
const val = data[vModel]
if (scheme.__config__.tag === 'el-upload') {
// 回显图片
scheme['file-list'] = (val || []).map(url => ({ name: url, url }))
// 上传地址 + 请求头
scheme.action = process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload"
scheme.headers = { Authorization: "Bearer " + getAccessToken() }
// 注意 on-success 不能绑定箭头函数!!!
scheme['on-success'] = function (response, file, fileList) {
if (response.code !== 0) {
return;
}
// 添加到 data 中
const prev = data[vModel] || []
this.$set(data, vModel, [
...prev,
response.data
])
// 发起表单校验
that.$refs[that.formConf.formRef].validateField(vModel)
}
// 注意 on-remove 不能绑定箭头函数!!!
scheme['on-remove'] = function (file, fileList) {
// 移除从 data 中
const prev = data[vModel] || []
const index = prev.indexOf(file.response.data)
if (index === -1) {
return
}
prev.splice(index, 1) // 直接移除即可,无需重复 set因为 array 是引用
// 发起表单校验
that.$refs[that.formConf.formRef].validateField(vModel)
}
}
if (layout) {
return layout.call(this, h, scheme)
}

View File

@ -152,31 +152,24 @@ export default {
})
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
this.$tab.refreshPage(view);
},
closeSelectedTag(view) {
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeRightTags() {
this.$store.dispatch('tagsView/delRightTags', this.selectedTag).then(visitedViews => {
this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeLeftTags() {
this.$store.dispatch('tagsView/delLeftTags', this.selectedTag).then(visitedViews => {
this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
@ -184,12 +177,12 @@ export default {
},
closeOthersTags() {
this.$router.push(this.selectedTag).catch(()=>{});
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === this.$route.path)) {
return
}

View File

@ -69,8 +69,9 @@ import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
import Tinymce from '@/components/tinymce/index.vue'
Vue.component('tinymce', Tinymce)
import '@/icons'
import axios from 'axios'
Vue.prototype.$axios = axios
import request from "@/utils/request" // 实现 form generator 使用自己定义的 axios request 对象
console.log(request)
Vue.prototype.$axios = request
import '@/styles/index.scss'
/**

View File

@ -3,7 +3,7 @@ import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { getAccessToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
@ -13,7 +13,7 @@ const whiteList = ['/login', '/social-login', '/auth-redirect', '/bind', '/regi
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
if (getAccessToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {

View File

@ -42,6 +42,11 @@ export const constantRoutes = [
component: (resolve) => require(['@/views/login'], resolve),
hidden: true
},
{
path: '/sso',
component: (resolve) => require(['@/views/sso'], resolve),
hidden: true
},
{
path: '/social-login',
component: (resolve) => require(['@/views/socialLogin'], resolve),

View File

@ -2,6 +2,7 @@ import { constantRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView';
import { toCamelCase } from "@/utils";
const permission = {
state: {
@ -56,6 +57,8 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
icon: route.icon,
noCache: !route.keepAlive,
}
// 路由地址转首字母大写驼峰作为路由名称适配keepAlive
route.name = toCamelCase(route.path, true)
route.hidden = !route.visible
// 处理 component 属性
if (route.children) { // 父节点

View File

@ -63,7 +63,7 @@ const mutations = {
}
}
},
DEL_RIGHT_VIEWS: (state, view) => {
const index = state.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
@ -79,6 +79,23 @@ const mutations = {
}
return false
})
},
DEL_LEFT_VIEWS: (state, view) => {
const index = state.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
state.visitedViews = state.visitedViews.filter((item, idx) => {
if (idx >= index || (item.meta && item.meta.affix)) {
return true
}
const i = state.cachedViews.indexOf(item.name)
if (i > -1) {
state.cachedViews.splice(i, 1)
}
return false
})
}
}
@ -172,7 +189,14 @@ const actions = {
commit('DEL_RIGHT_VIEWS', view)
resolve([...state.visitedViews])
})
}
},
delLeftTags({ commit }, view) {
return new Promise(resolve => {
commit('DEL_LEFT_VIEWS', view)
resolve([...state.visitedViews])
})
},
}
export default {

View File

@ -1,9 +1,8 @@
import {login, logout, getInfo, socialQuickLogin, socialBindLogin, smsLogin} from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import {getAccessToken, setToken, removeToken, getRefreshToken} from '@/utils/auth'
const user = {
state: {
token: getToken(),
id: 0, // 用户编号
name: '',
avatar: '',
@ -15,9 +14,6 @@ const user = {
SET_ID: (state, id) => {
state.id = id
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
@ -42,8 +38,8 @@ const user = {
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
res = res.data;
setToken(res.token)
commit('SET_TOKEN', res.token)
// 设置 token
setToken(res)
resolve()
}).catch(error => {
reject(error)
@ -59,8 +55,8 @@ const user = {
return new Promise((resolve, reject) => {
socialQuickLogin(type, code, state).then(res => {
res = res.data;
setToken(res.token)
commit('SET_TOKEN', res.token)
// 设置 token
setToken(res)
resolve()
}).catch(error => {
reject(error)
@ -78,8 +74,8 @@ const user = {
return new Promise((resolve, reject) => {
socialBindLogin(type, code, state, username, password).then(res => {
res = res.data;
setToken(res.token)
commit('SET_TOKEN', res.token)
// 设置 token
setToken(res)
resolve()
}).catch(error => {
reject(error)
@ -93,8 +89,8 @@ const user = {
return new Promise((resolve, reject) => {
smsLogin(mobile,mobileCode).then(res => {
res = res.data;
setToken(res.token)
commit('SET_TOKEN', res.token)
// 设置 token
setToken(res)
resolve()
}).catch(error => {
reject(error)
@ -142,7 +138,6 @@ const user = {
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
@ -151,15 +146,6 @@ const user = {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}

View File

@ -1,15 +1,96 @@
import Cookies from 'js-cookie'
import {decrypt, encrypt} from "@/utils/jsencrypt";
const TokenKey = 'Admin-Token'
const AccessTokenKey = 'ACCESS_TOKEN'
const RefreshTokenKey = 'REFRESH_TOKEN'
export function getToken() {
return Cookies.get(TokenKey)
// ========== Token 相关 ==========
export function getAccessToken() {
return localStorage.getItem(AccessTokenKey)
}
export function getRefreshToken() {
return localStorage.getItem(RefreshTokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
localStorage.setItem(AccessTokenKey, token.accessToken)
localStorage.setItem(RefreshTokenKey, token.refreshToken)
}
export function removeToken() {
return Cookies.remove(TokenKey)
localStorage.removeItem(AccessTokenKey)
localStorage.removeItem(RefreshTokenKey)
}
// ========== 账号相关 ==========
const UsernameKey = 'USERNAME'
const PasswordKey = 'PASSWORD'
const RememberMeKey = 'REMEMBER_ME'
export function getUsername() {
return localStorage.getItem(UsernameKey)
}
export function setUsername(username) {
localStorage.setItem(UsernameKey, username)
}
export function removeUsername() {
localStorage.removeItem(UsernameKey)
}
export function getPassword() {
const password = localStorage.getItem(PasswordKey)
return password ? decrypt(password) : undefined
}
export function setPassword(password) {
localStorage.setItem(PasswordKey, encrypt(password))
}
export function removePassword() {
localStorage.removeItem(PasswordKey)
}
export function getRememberMe() {
return localStorage.getItem(RememberMeKey) === 'true'
}
export function setRememberMe(rememberMe) {
localStorage.setItem(RememberMeKey, rememberMe)
}
export function removeRememberMe() {
localStorage.removeItem(RememberMeKey)
}
// ========== 租户相关 ==========
const TenantIdKey = 'TENANT_ID'
const TenantNameKey = 'TENANT_NAME'
export function getTenantName() {
return localStorage.getItem(TenantNameKey)
}
export function setTenantName(username) {
localStorage.setItem(TenantNameKey, username)
}
export function removeTenantName() {
localStorage.removeItem(TenantNameKey)
}
export function getTenantId() {
return localStorage.getItem(TenantIdKey)
}
export function setTenantId(username) {
localStorage.setItem(TenantIdKey, username)
}
export function removeTenantId() {
localStorage.removeItem(TenantIdKey)
}

View File

@ -75,13 +75,13 @@ export const SystemUserSocialTypeEnum = {
title: "钉钉",
type: 20,
source: "dingtalk",
img: "https://cdn.jsdelivr.net/gh/justauth/justauth-oauth-logo@1.11/dingtalk.png",
img: "https://s1.ax1x.com/2022/05/22/OzMDRs.png",
},
WECHAT_ENTERPRISE: {
title: "企业微信",
type: 30,
source: "wechat_enterprise",
img: "https://cdn.jsdelivr.net/gh/justauth/justauth-oauth-logo@1.11/wechat_enterprise.png",
img: "https://s1.ax1x.com/2022/05/22/OzMrzn.png",
}
}

View File

@ -23,6 +23,7 @@ export const DICT_TYPE = {
SYSTEM_SMS_SEND_STATUS: 'system_sms_send_status',
SYSTEM_SMS_RECEIVE_STATUS: 'system_sms_receive_status',
SYSTEM_ERROR_CODE_TYPE: 'system_error_code_type',
SYSTEM_OAUTH2_GRANT_TYPE: 'system_oauth2_grant_type',
// ========== INFRA 模块 ==========
INFRA_BOOLEAN_STRING: 'infra_boolean_string',

View File

@ -427,3 +427,15 @@ export function isNumberStr(str) {
return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
}
// -转驼峰
export function toCamelCase(str, upperCaseFirst) {
str = (str || '').toLowerCase().replace(/-(.)/g, function (match, group1) {
return group1.toUpperCase();
});
if (upperCaseFirst && str) {
str = str.charAt(0).toUpperCase() + str.slice(1);
}
return str;
}

View File

@ -1,13 +1,24 @@
import axios from 'axios'
import { Notification, MessageBox, Message } from 'element-ui'
import {Message, MessageBox, Notification} from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import {getAccessToken, getRefreshToken, getTenantId, setToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import Cookies from "js-cookie";
import {getPath, getTenantEnable} from "@/utils/ruoyi";
import {refreshToken} from "@/api/login";
// 需要忽略的提示。忽略后,自动 Promise.reject('error')
const ignoreMsgs = [
"无效的刷新令牌", // 刷新令牌被删除时,不用提示
"刷新令牌已过期" // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401无法跳转到登出界面
]
// 是否显示重新登录
export let isRelogin = { show: false };
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
// 请求队列
let requestList = []
// 是否正在刷新中
let isRefreshToken = false
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
@ -15,18 +26,20 @@ const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API + '/admin-api/', // 此处的 /admin-api/ 地址,原因是后端的基础路径为 /admin-api/
// 超时
timeout: 30000
timeout: 30000,
// 禁用 Cookie 等信息
withCredentials: false,
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
if (getAccessToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// 设置租户
if (getTenantEnable()) {
const tenantId = Cookies.get('tenantId');
const tenantId = getTenantId();
if (tenantId) {
config.headers['tenant-id'] = tenantId;
}
@ -60,66 +73,84 @@ service.interceptors.request.use(config => {
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = getPath('/index');
})
}).catch(() => {
isRelogin.show = false;
});
service.interceptors.response.use(async res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = res.data.msg || errorCode[code] || errorCode['default']
if (ignoreMsgs.indexOf(msg) !== -1) { // 如果是忽略的错误码,直接返回 msg 异常
return Promise.reject(msg)
} else if (code === 401) {
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true;
// 1. 如果获取不到刷新令牌,则只能执行登出操作
if (!getRefreshToken()) {
return handleAuthorized();
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({
message: msg,
type: 'error'
// 2. 进行刷新访问令牌
try {
const refreshTokenRes = await refreshToken()
// 2.1 刷新成功,则回放队列的请求 + 当前请求
setToken(refreshTokenRes.data)
requestList.forEach(cb => cb())
return service(res.config)
} catch (e) {// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach(cb => cb())
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized();
} finally {
requestList = []
isRefreshToken = false
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise(resolve => {
requestList.push(() => {
res.config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(service(res.config))
})
})
return Promise.reject(new Error(msg))
} else if (code === 901) {
Message({
type: 'error',
duration: 0,
dangerouslyUseHTMLString: true,
message: '<div>演示模式,无法进行写操作</div>'
+ '<div> &nbsp; </div>'
+ '<div>参考 https://doc.iocoder.cn/ 教程</div>'
+ '<div> &nbsp; </div>'
+ '<div>5 分钟搭建本地环境</div>',
})
return Promise.reject(new Error(msg))
} else if (code !== 200) {
}
} else if (code === 500) {
Message({
message: msg,
type: 'error'
})
return Promise.reject(new Error(msg))
} else if (code === 901) {
Message({
type: 'error',
duration: 0,
dangerouslyUseHTMLString: true,
message: '<div>演示模式,无法进行写操作</div>'
+ '<div> &nbsp; </div>'
+ '<div>参考 https://doc.iocoder.cn/ 教程</div>'
+ '<div> &nbsp; </div>'
+ '<div>5 分钟搭建本地环境</div>',
})
return Promise.reject(new Error(msg))
} else if (code !== 200) {
if (msg === '无效的刷新令牌') { // hard coding忽略这个提示直接登出
console.log(msg)
} else {
Notification.error({
title: msg
})
return Promise.reject('error')
} else {
return res.data
}
},
error => {
return Promise.reject('error')
} else {
return res.data
}
}, error => {
console.log('err' + error)
let { message } = error;
let {message} = error;
if (message === "Network Error") {
message = "后端接口连接异常";
}
else if (message.includes("timeout")) {
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
}
else if (message.includes("Request failed with status code")) {
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({
@ -133,9 +164,29 @@ service.interceptors.response.use(res => {
export function getBaseHeader() {
return {
'Authorization': "Bearer " + getToken(),
'tenant-id': Cookies.get('tenantId'),
'Authorization': "Bearer " + getAccessToken(),
'tenant-id': getTenantId(),
}
}
function handleAuthorized() {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = getPath('/index');
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
}
export default service

View File

@ -429,7 +429,7 @@ export default {
this.$modal.confirm('是否删除该流程!!').then(function() {
deleteModel(row.id).then(response => {
that.getList();
that.msgSuccess("删除成功");
that.$modal.msgSuccess("删除成功");
})
}).catch(() => {});
},
@ -439,7 +439,7 @@ export default {
this.$modal.confirm('是否部署该流程!!').then(function() {
deployModel(row.id).then(response => {
that.getList();
that.msgSuccess("部署成功");
that.$modal.msgSuccess("部署成功");
})
}).catch(() => {});
},

View File

@ -55,10 +55,9 @@
<script>
import {getProcessDefinitionBpmnXML, getProcessDefinitionList} from "@/api/bpm/definition";
import {DICT_TYPE, getDictDatas} from "@/utils/dict";
import {getForm} from "@/api/bpm/form";
import {decodeFields} from "@/utils/formGenerator";
import Parser from '@/components/parser/Parser'
import {createProcessInstance, getMyProcessInstancePage} from "@/api/bpm/processInstance";
import {createProcessInstance} from "@/api/bpm/processInstance";
// 流程实例的发起
export default {

View File

@ -58,7 +58,7 @@
</el-table-column>
<el-table-column label="当前审批任务" align="center" prop="tasks">
<template slot-scope="scope">
<el-button v-for="task in scope.row.tasks" type="text" @click="handleFormDetail(task.id)">
<el-button v-for="task in scope.row.tasks" :key="task.id" type="text" @click="handleFormDetail(task.id)">
<span>{{ task.name }}</span>
</el-button>
</template>

View File

@ -12,7 +12,7 @@
</el-table-column>
<el-table-column label="规则范围" align="center" prop="options" width="440px">
<template slot-scope="scope">
<el-tag size="medium" v-if="scope.row.options" v-for="option in scope.row.options">
<el-tag size="medium" v-if="scope.row.options" :key="option" v-for="option in scope.row.options">
{{ getAssignRuleOptionName(scope.row.type, option) }}
</el-tag>
</template>

View File

@ -77,7 +77,7 @@
<script>
import { deleteFile, getFilePage } from "@/api/infra/file";
import {getToken} from "@/utils/auth";
import {getAccessToken} from "@/utils/auth";
export default {
name: "File",
@ -108,7 +108,7 @@ export default {
title: "", // 弹出层标题
isUploading: false, // 是否禁用上传
url: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
headers: { Authorization: "Bearer " + getToken() }, // 设置上传的请求头部
headers: { Authorization: "Bearer " + getAccessToken() }, // 设置上传的请求头部
data: {} // 上传的额外数据用于文件名
},
};

View File

@ -107,9 +107,17 @@
import {getCodeImg, sendSmsCode, socialAuthRedirect} from "@/api/login";
import {getTenantIdByName} from "@/api/system/tenant";
import Cookies from "js-cookie";
import {decrypt, encrypt} from '@/utils/jsencrypt'
import {SystemUserSocialTypeEnum} from "@/utils/constants";
import {getTenantEnable} from "@/utils/ruoyi";
import {
getPassword,
getRememberMe, getTenantName,
getUsername,
removePassword, removeRememberMe, removeTenantName,
removeUsername,
setPassword, setRememberMe, setTenantId, setTenantName,
setUsername
} from "@/utils/auth";
export default {
name: "Login",
@ -161,7 +169,7 @@ export default {
const tenantId = res.data;
if (tenantId && tenantId >= 0) {
// 设置租户
Cookies.set("tenantId", tenantId);
setTenantId(tenantId)
callback();
} else {
callback('租户不存在');
@ -172,8 +180,6 @@ export default {
}
]
},
loading: false,
redirect: undefined,
// 枚举
@ -213,21 +219,16 @@ export default {
});
},
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe')
const tenantName = Cookies.get('tenantName');
const mobile = Cookies.get('mobile');
const mobileCode = Cookies.get('mobileCode');
const loginType = Cookies.get('loginType');
const username = getUsername();
const password = getPassword();
const rememberMe = getRememberMe();
const tenantName = getTenantName();
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
tenantName: tenantName === undefined ? this.loginForm.tenantName : tenantName,
mobile: mobile === undefined ? this.loginForm.mobile : mobile,
mobileCode: mobileCode === undefined ? this.loginForm.mobileCode : mobileCode,
loginType: loginType === undefined ? this.loginForm.loginType : loginType,
...this.loginForm,
username: username ? username : this.loginForm.username,
password: password ? password : this.loginForm.password,
rememberMe: rememberMe ? getRememberMe() : false,
tenantName: tenantName ? tenantName : this.loginForm.tenantName,
};
},
handleLogin() {
@ -236,18 +237,18 @@ export default {
this.loading = true;
// 设置 Cookie
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, {expires: 30});
Cookies.set("password", encrypt(this.loginForm.password), {expires: 30});
Cookies.set('rememberMe', this.loginForm.rememberMe, {expires: 30});
Cookies.set('tenantName', this.loginForm.tenantName, {expires: 30});
setUsername(this.loginForm.username)
setPassword(this.loginForm.password)
setRememberMe(this.loginForm.rememberMe)
setTenantName(this.loginForm.tenantName)
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
Cookies.remove('tenantName');
removeUsername()
removePassword()
removeRememberMe()
removeTenantName()
}
// 发起登陆
console.log("发起登录", this.loginForm);
// console.log("发起登录", this.loginForm);
this.$store.dispatch(this.loginForm.loginType === "sms" ? "SmsLogin" : "Login", this.loginForm).then(() => {
this.$router.push({path: this.redirect || "/"}).catch(() => {
});

View File

@ -74,7 +74,7 @@
<!-- <el-table-column label="商户名称" align="center" prop="merchantName" width="120"/>-->
<!-- <el-table-column label="应用名称" align="center" prop="appName" width="120"/>-->
<el-table-column label="支付渠道" align="center" width="130">
<template v-slot="scope">
<template slot-scope="scope">
<el-popover trigger="hover" placement="top">
<p>商户名称: {{ scope.row.merchantName }}</p>
<p>应用名称: {{ scope.row.appName }}</p>
@ -88,7 +88,7 @@
<!-- <el-table-column label="交易订单号" align="center" prop="tradeNo" width="140"/>-->
<!-- <el-table-column label="商户订单编号" align="center" prop="merchantOrderId" width="140"/>-->
<el-table-column label="商户订单号" align="left" width="230">
<template v-slot="scope">
<template slot-scope="scope">
<p class="order-font">
<el-tag size="mini">退款</el-tag>
{{ scope.row.merchantRefundNo }}
@ -100,7 +100,7 @@
</template>
</el-table-column>
<el-table-column label="支付订单号" align="center" prop="merchantRefundNo" width="250">
<template v-slot="scope">
<template slot-scope="scope">
<p class="order-font">
<el-tag size="mini">交易</el-tag>
{{ scope.row.tradeNo }}
@ -112,7 +112,7 @@
</template>
</el-table-column>
<el-table-column label="支付金额(元)" align="center" prop="payAmount" width="100">
<template v-slot="scope" class="">
<template slot-scope="scope" class="">
{{ parseFloat(scope.row.payAmount / 100).toFixed(2) }}
</template>
</el-table-column>
@ -122,17 +122,17 @@
</template>
</el-table-column>
<el-table-column label="退款类型" align="center" prop="type" width="80">
<template v-slot="scope">
<template slot-scope="scope">
<dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="退款状态" align="center" prop="status">
<template v-slot="scope">
<template slot-scope="scope">
<dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="回调状态" align="center" prop="notifyStatus">
<template v-slot="scope">
<template slot-scope="scope">
<dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="scope.row.notifyStatus" />
</template>
</el-table-column>
@ -183,7 +183,7 @@
<el-tag class="tag-purple" size="mini">{{ parseFloat(refundDetail.refundAmount / 100).toFixed(2) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="退款类型">
<template v-slot="scope">
<template slot-scope="scope">
<dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="refundDetail.type" />
</template>
</el-descriptions-item>

View File

@ -19,7 +19,7 @@
</el-tab-pane>
</el-tabs>
<div>
<el-form ref="loginForm" :model="loginForm" :rules="LoginRules" class="login-form">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<!-- 账号密码登录 -->
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
@ -65,16 +65,28 @@
<script>
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
import {
getPassword, getRememberMe,
getUsername,
removePassword,
removeUsername,
setPassword,
setRememberMe,
setUsername
} from "@/utils/auth";
import {getCodeImg} from "@/api/login";
export default {
name: "ThirdLogin",
data() {
return {
codeUrl: "",
captchaEnable: true,
loginForm: {
loginType: "uname",
username: "admin",
password: "admin123",
rememberMe: false, // TODO 芋艿:后面看情况,去掉这块
rememberMe: false,
},
loginRules: {
username: [
@ -104,6 +116,7 @@ export default {
this.getCookie();
// 重定向地址
this.redirect = this.$route.query.redirect;
this.getCode();
// 社交登录相关
this.type = this.$route.query.type;
this.code = this.$route.query.code;
@ -119,16 +132,30 @@ export default {
});
},
methods: {
getCode() {
// 只有开启的状态,才加载验证码。默认开启
if (!this.captchaEnable) {
return;
}
// 请求远程,获得验证码
getCodeImg().then(res => {
res = res.data;
this.captchaEnable = res.enable;
if (this.captchaEnable) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
},
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe')
const loginType = Cookies.get('loginType');
const username = getUsername();
const password = getPassword();
const rememberMe = getRememberMe();
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
loginType: loginType === undefined ? this.loginForm.loginType : loginType,
username: username ? username : this.loginForm.username,
password: password ? password : this.loginForm.password,
rememberMe: rememberMe ? getRememberMe() : false,
loginType: this.loginForm.loginType,
};
},
handleLogin() {
@ -136,11 +163,12 @@ export default {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
setUsername(this.loginForm.username)
setPassword(this.loginForm.password)
setRememberMe(this.loginForm.rememberMe)
} else {
Cookies.remove("username");
Cookies.remove("password");
removeUsername()
removePassword()
}
this.$store.dispatch("SocialLogin2", {
code: this.code,

View File

@ -0,0 +1,236 @@
<template>
<div class="container">
<div class="logo"></div>
<!-- 登录区域 -->
<div class="content">
<!-- 配图 -->
<div class="pic"></div>
<!-- 表单 -->
<div class="field">
<!-- [移动端]标题 -->
<h2 class="mobile-title">
<h3 class="title">芋道后台管理系统</h3>
</h2>
<!-- 表单 -->
<div class="form-cont">
<el-tabs class="form" style=" float:none;" value="uname">
<el-tab-pane :label="'三方授权(' + client.name + ')'" name="uname">
</el-tab-pane>
</el-tabs>
<div>
<el-form ref="loginForm" :model="loginForm" :rules="LoginRules" class="login-form">
<el-form-item prop="tenantName" v-if="tenantEnable">
<el-input v-model="loginForm.tenantName" type="text" auto-complete="off" placeholder='租户'>
<svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<!-- 授权范围的选择 -->
此第三方应用请求获得以下权限
<el-form-item prop="scopes">
<el-checkbox-group v-model="loginForm.scopes">
<el-checkbox v-for="scope in params.scopes" :label="scope" :key="scope"
style="display: block; margin-bottom: -10px;">{{formatScope(scope)}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 下方的登录按钮 -->
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:60%;"
@click.native.prevent="handleAuthorize(true)">
<span v-if="!loading">同意授权</span>
<span v-else> 中...</span>
</el-button>
<el-button size="medium" style="width:36%"
@click.native.prevent="handleAuthorize(false)">拒绝</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
<!-- footer -->
<div class="footer">
Copyright © 2020-2022 iocoder.cn All Rights Reserved.
</div>
</div>
</template>
<script>
import {getTenantIdByName} from "@/api/system/tenant";
import {getTenantEnable} from "@/utils/ruoyi";
import {authorize, getAuthorize} from "@/api/login";
import {getTenantName, setTenantId} from "@/utils/auth";
export default {
name: "Login",
data() {
return {
tenantEnable: true,
loginForm: {
tenantName: "芋道源码",
scopes: [], // 已选中的 scope 数组
},
params: { // URL 上的 client_id、scope 等参数
responseType: undefined,
clientId: undefined,
redirectUri: undefined,
state: undefined,
scopes: [], // 优先从 query 参数获取;如果未传递,从后端获取
},
client: { // 客户端信息
name: '',
logo: '',
},
LoginRules: {
tenantName: [
{required: true, trigger: "blur", message: "租户不能为空"},
{
validator: (rule, value, callback) => {
// debugger
getTenantIdByName(value).then(res => {
const tenantId = res.data;
if (tenantId && tenantId >= 0) {
// 设置租户
setTenantId(tenantId)
callback();
} else {
callback('租户不存在');
}
});
},
trigger: 'blur'
}
]
},
loading: false
};
},
created() {
// 租户开关
this.tenantEnable = getTenantEnable();
this.getCookie();
// 解析参数
// 例如说【自动授权不通过】client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
// 例如说【自动授权通过】client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
this.params.responseType = this.$route.query.response_type
this.params.clientId = this.$route.query.client_id
this.params.redirectUri = this.$route.query.redirect_uri
this.params.state = this.$route.query.state
if (this.$route.query.scope) {
this.params.scopes = this.$route.query.scope.split(' ')
}
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
if (this.params.scopes.length > 0) {
this.doAuthorize(true, this.params.scopes, []).then(res => {
const href = res.data
if (!href) {
console.log('自动授权未通过!')
return;
}
location.href = href
})
}
// 获取授权页的基本信息
getAuthorize(this.params.clientId).then(res => {
this.client = res.data.client
// 解析 scope
let scopes
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes
if (this.params.scopes.length > 0) {
scopes = []
for (const scope of res.data.scopes) {
if (this.params.scopes.indexOf(scope.key) >= 0) {
scopes.push(scope)
}
}
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
} else {
scopes = res.data.scopes
for (const scope of scopes) {
this.params.scopes.push(scope.key)
}
}
// 生成已选中的 checkedScopes
for (const scope of scopes) {
if (scope.value) {
this.loginForm.scopes.push(scope.key)
}
}
})
},
methods: {
getCookie() {
const tenantName = getTenantName();
this.loginForm = {
...this.loginForm,
tenantName: tenantName ? tenantName : this.loginForm.tenantName,
};
},
handleAuthorize(approved) {
this.$refs.loginForm.validate(valid => {
if (!valid) {
return
}
this.loading = true
// 计算 checkedScopes + uncheckedScopes
let checkedScopes;
let uncheckedScopes;
if (approved) { // 同意授权,按照用户的选择
checkedScopes = this.loginForm.scopes
uncheckedScopes = this.params.scopes.filter(item => checkedScopes.indexOf(item) === -1)
} else { // 拒绝,则都是取消
checkedScopes = []
uncheckedScopes = this.params.scopes
}
// 提交授权的请求
this.doAuthorize(false, checkedScopes, uncheckedScopes).then(res => {
const href = res.data
if (!href) {
return;
}
location.href = href
}).finally(() => {
this.loading = false
})
})
},
doAuthorize(autoApprove, checkedScopes, uncheckedScopes) {
return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state,
autoApprove, checkedScopes, uncheckedScopes)
},
formatScope(scope) {
// 格式化 scope 授权范围,方便用户理解。
// 这里仅仅是一个 demo可以考虑录入到字典数据中例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
switch (scope) {
case 'user.read': return '访问你的个人信息'
case 'user.write': return '修改你的个人信息'
default: return scope
}
}
}
};
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/login.scss";
.oauth-login {
display: flex;
align-items: cen;
cursor:pointer;
}
.oauth-login-item {
display: flex;
align-items: center;
margin-right: 10px;
}
.oauth-login-item img {
height: 25px;
width: 25px;
}
.oauth-login-item span:hover {
text-decoration: underline red;
color: red;
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="应用名" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入应用名" clearable @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:oauth2-client:create']">新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="客户端编号" align="center" prop="clientId" />
<el-table-column label="客户端密钥" align="center" prop="secret" />
<el-table-column label="应用名" align="center" prop="name" />
<el-table-column label="应用图标" align="center" prop="logo">
<template slot-scope="scope">
<img width="40px" height="40px" :src="scope.row.logo">
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="访问令牌的有效期" align="center" prop="accessTokenValiditySeconds">
<template slot-scope="scope">{{ scope.row.accessTokenValiditySeconds }} </template>
</el-table-column>
<el-table-column label="刷新令牌的有效期" align="center" prop="refreshTokenValiditySeconds">
<template slot-scope="scope">{{ scope.row.refreshTokenValiditySeconds }} </template>
</el-table-column>
<el-table-column label="授权类型" align="center" prop="authorizedGrantTypes">
<template slot-scope="scope">
<el-tag :disable-transitions="true" :key="index" v-for="(authorizedGrantType, index) in scope.row.authorizedGrantTypes" :index="index">
{{ authorizedGrantType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:oauth2-client:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:oauth2-client:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="160px">
<el-form-item label="客户端编号" prop="secret">
<el-input v-model="form.clientId" placeholder="请输入客户端编号" />
</el-form-item>
<el-form-item label="客户端密钥" prop="secret">
<el-input v-model="form.secret" placeholder="请输入客户端密钥" />
</el-form-item>
<el-form-item label="应用名" prop="name">
<el-input v-model="form.name" placeholder="请输入应用名" />
</el-form-item>
<el-form-item label="应用图标">
<imageUpload v-model="form.logo" :limit="1"/>
</el-form-item>
<el-form-item label="应用描述">
<el-input type="textarea" v-model="form.description" placeholder="请输入应用名" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)"
:key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="访问令牌的有效期" prop="accessTokenValiditySeconds">
<el-input-number v-model="form.accessTokenValiditySeconds" placeholder="单位:秒" />
</el-form-item>
<el-form-item label="刷新令牌的有效期" prop="refreshTokenValiditySeconds">
<el-input-number v-model="form.refreshTokenValiditySeconds" placeholder="单位:秒" />
</el-form-item>
<el-form-item label="授权类型" prop="authorizedGrantTypes">
<el-select v-model="form.authorizedGrantTypes" multiple filterable placeholder="请输入授权类型" style="width: 500px" >
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="授权范围" prop="scopes">
<el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
<el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
</el-select>
</el-form-item>
<el-form-item label="自动授权范围" prop="autoApproveScopes">
<el-select v-model="form.autoApproveScopes" multiple filterable placeholder="请输入授权范围" style="width: 500px" >
<el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
</el-select>
</el-form-item>
<el-form-item label="可重定向的 URI 地址" prop="redirectUris">
<el-select v-model="form.redirectUris" multiple filterable allow-create placeholder="请输入可重定向的 URI 地址" style="width: 500px" >
<el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
</el-select>
</el-form-item>
<el-form-item label="权限" prop="authorities">
<el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
<el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
</el-select>
</el-form-item>
<el-form-item label="资源" prop="resourceIds">
<el-select v-model="form.resourceIds" multiple filterable allow-create placeholder="请输入资源" style="width: 500px" >
<el-option v-for="resourceId in form.resourceIds" :key="resourceId" :label="resourceId" :value="resourceId"/>
</el-select>
</el-form-item>
<el-form-item label="附加信息" prop="additionalInformation">
<el-input type="textarea" v-model="form.additionalInformation" placeholder="请输入附加信息JSON 格式数据" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { createOAuth2Client, updateOAuth2Client, deleteOAuth2Client, getOAuth2Client, getOAuth2ClientPage } from "@/api/system/oauth2/oauth2Client";
import ImageUpload from '@/components/ImageUpload';
import Editor from '@/components/Editor';
import {CommonStatusEnum} from "@/utils/constants";
import FileUpload from "@/components/FileUpload";
export default {
name: "OAuth2Client",
components: {
FileUpload,
ImageUpload,
Editor,
},
data() {
return {
// 遮罩层
loading: true,
// 导出遮罩层
exportLoading: false,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// OAuth2 客户端列表
list: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNo: 1,
pageSize: 10,
name: null,
status: null,
},
// 表单参数
form: {},
// 表单校验
rules: {
clientId: [{ required: true, message: "客户端编号不能为空", trigger: "blur" }],
secret: [{ required: true, message: "客户端密钥不能为空", trigger: "blur" }],
name: [{ required: true, message: "应用名不能为空", trigger: "blur" }],
logo: [{ required: true, message: "应用图标不能为空", trigger: "blur" }],
status: [{ required: true, message: "状态不能为空", trigger: "blur" }],
accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
}
};
},
created() {
this.getList();
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
// 处理查询参数
let params = {...this.queryParams};
// 执行查询
getOAuth2ClientPage(params).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
id: undefined,
clientId: undefined,
secret: undefined,
name: undefined,
logo: undefined,
description: undefined,
status: CommonStatusEnum.ENABLE,
accessTokenValiditySeconds: 30 * 60,
refreshTokenValiditySeconds: 30 * 24 * 60,
redirectUris: [],
authorizedGrantTypes: [],
scopes: [],
autoApproveScopes: [],
authorities: [],
resourceIds: [],
additionalInformation: undefined,
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加 OAuth2 客户端";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id;
getOAuth2Client(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改 OAuth2 客户端";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (!valid) {
return;
}
// 修改的提交
if (this.form.id != null) {
updateOAuth2Client(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
return;
}
// 添加的提交
createOAuth2Client(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
});
},
/** 删除按钮操作 */
handleDelete(row) {
const id = row.id;
this.$modal.confirm('是否确认删除客户端编号为"' + row.clientId + '"的数据项?').then(function() {
return deleteOAuth2Client(id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
}
};
</script>

View File

@ -3,11 +3,14 @@
<doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="登录地址" prop="userIp">
<el-input v-model="queryParams.userIp" placeholder="请输入登录地址" clearable @keyup.enter.native="handleQuery"/>
<el-form-item label="用户编号" prop="userId">
<el-input v-model="queryParams.userId" placeholder="请输入用户编号" clearable @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="用户名称" prop="username">
<el-input v-model="queryParams.username" placeholder="请输入用户名称" clearable @keyup.enter.native="handleQuery"/>
<el-form-item label="用户类型" prop="userType">
<el-select v-model="queryParams.userType" placeholder="请选择用户类型" clearable>
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.USER_TYPE)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
@ -16,20 +19,28 @@
</el-form>
<el-table v-loading="loading" :data="list" style="width: 100%;">
<el-table-column label="会话编号" align="center" prop="id" width="300" />
<el-table-column label="登录名称" align="center" prop="username" width="100" />
<el-table-column label="部门名称" align="center" prop="deptName" width="100" />
<el-table-column label="登录地址" align="center" prop="userIp" width="100" />
<el-table-column label="userAgent" align="center" prop="userAgent" :show-overflow-tooltip="true" />
<el-table-column label="登录时间" align="center" prop="createTime" width="180">
<el-table-column label="访问令牌" align="center" prop="accessToken" width="300" />
<el-table-column label="刷新令牌" align="center" prop="refreshToken" width="300" />
<el-table-column label="用户编号" align="center" prop="userId" />
<el-table-column label="用户类型" align="center" prop="userType" width="100">
<template slot-scope="scope">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType"/>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="过期时间" align="center" prop="expiresTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.expiresTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleForceLogout(scope.row)"
v-hasPermi="['system:user-session:delete']">强退</el-button>
v-hasPermi="['system:oauth2-token:delete']">强退</el-button>
</template>
</el-table-column>
</el-table>
@ -40,10 +51,10 @@
</template>
<script>
import { list, forceLogout } from "@/api/system/session";
import { getAccessTokenPage, deleteAccessToken } from "@/api/system/oauth2/oauth2Token";
export default {
name: "Online",
name: "OAuth2Token",
data() {
return {
//
@ -56,8 +67,8 @@ export default {
queryParams: {
pageNo: 1,
pageSize: 10,
userIp: undefined,
username: undefined
userId: undefined,
userType: undefined
}
};
},
@ -68,7 +79,7 @@ export default {
/** 查询登录日志列表 */
getList() {
this.loading = true;
list(this.queryParams).then(response => {
getAccessTokenPage(this.queryParams).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
@ -86,8 +97,8 @@ export default {
},
/** 强退按钮操作 */
handleForceLogout(row) {
this.$modal.confirm('是否确认强退名称为"' + row.username + '"的数据项?').then(function() {
return forceLogout(row.id);
this.$modal.confirm('是否确认强退令牌为"' + row.accessToken + '"的数据项?').then(function() {
return deleteAccessToken(row.accessToken);
}).then(() => {
this.getList();
this.$modal.msgSuccess("强退成功");

View File

@ -54,7 +54,7 @@
<el-table-column label="描述" align="center" prop="description"/>
<el-table-column label="标签" align="center" prop="tags">
<template slot-scope="scope">
<el-tag :disable-transitions="true" v-for="(tag, index) in scope.row.tags" :index="index">
<el-tag :disable-transitions="true" :key="index" v-for="(tag, index) in scope.row.tags" :index="index">
{{ tag }}
</el-tag>
</template>

View File

@ -147,7 +147,7 @@
<el-form-item label="手机号" prop="mobile">
<el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item v-for="param in sendSmsForm.params" :label="'参数 {' + param + '}'" :prop="'templateParams.' + param">
<el-form-item v-for="param in sendSmsForm.params" :key="param" :label="'参数 {' + param + '}'" :prop="'templateParams.' + param">
<el-input v-model="sendSmsForm.templateParams[param]" :placeholder="'请输入 ' + param + ' 参数'" />
</el-form-item>
</el-form>

View File

@ -111,7 +111,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<treeselect v-model="form.deptId" :options="deptOptions" :show-count="true"
<treeselect v-model="form.deptId" :options="deptOptions" :show-count="true" :clearable="false"
placeholder="请选择归属部门" :normalizer="normalizer"/>
</el-form-item>
</el-col>
@ -237,7 +237,7 @@ import {
resetUserPwd,
updateUser
} from "@/api/system/user";
import {getToken} from "@/utils/auth";
import {getAccessToken} from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
@ -304,7 +304,7 @@ export default {
// 设置上传的请求头部
headers: getBaseHeader(),
// 上传的地址
url: process.env.VUE_APP_BASE_API + '/admin-api/' + "/system/user/import"
url: process.env.VUE_APP_BASE_API + '/admin-api/system/user/import'
},
// 查询参数
queryParams: {