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

This commit is contained in:
YunaiV
2022-05-26 19:19:48 +08:00
406 changed files with 24646 additions and 16109 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'
})
}
@ -75,3 +77,72 @@ export function socialBindLogin(type, code, state, username, password) {
}
})
}
// 获取登录验证码
export function sendSmsCode(mobile, scene) {
return request({
url: '/system/auth/send-sms-code',
method: 'post',
data: {
mobile,
scene
}
})
}
// 短信验证码登录
export function smsLogin(mobile, code) {
return request({
url: '/system/auth/sms-login',
method: 'post',
data: {
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'
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
yudao-ui-admin/src/assets/images/profile.jpg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,387 @@
/* ===== PC DESIGN ===== */
$W: 1000;
$H: 1920;
$picW: 438;
$picH: 560;
$formW: 320;
$tabW: $formW / 2;
$rowH: 56;
$buttonH: 50;
// container
$containerBgColor: #e6ebf2;
$containerBgImage: '../assets/images/bg.png';
// container-logo
$logoWidth: 417px;
$logoHeight: 64px;
$logoImage: '../assets/logo/login-logo.png';
// container-content
$contentWidth: round($W / $H * 100) * 1vw;
$contentHeight: round($picH / $W * 100) / 100 * $contentWidth;
$contentBgColor: #ffffff;
// container-content-pic
$picWidth: round($picW / $H * 100) * 1vw;
$picHeight: inherit;
$picImage: '../assets/images/pic.png';
// container-content-field
$fieldWidth: $contentWidth - $picWidth;
$fieldHeight: inherit;
// container-content-field-form
$formWidth: $formW * 1px;
$tabWidth: $tabW * 1px;
$rowHeight: $rowH * 1px;
$buttonHeight: $buttonH * 1px;
// - - - - - 页面基础设置
.container {
.login-code {
width: 33%;
height: 38px;
float: right;
img {
cursor: pointer;
width:100%;max-width:100px; height:auto;
vertical-align: middle;
}
}
// 元素
width: inherit;
height: inherit;
min-width: 1080px;
min-height: 620px;
background-color: $containerBgColor;
background-image: url($containerBgImage);
background-size: cover;
// 定位
position: relative;
display: flex;
justify-content: center;
align-items: center;
// 文字
font-size: 14px;
font-family: Microsoft YaHei;
font-weight: 400;
.logo {
// 元素
width: $logoWidth;
height: $logoHeight;
background-image: url($logoImage);
background-size: contain;
// 定位
position: absolute;
top: 50px;
left: 50%;
margin-left: -$logoWidth/2;
}
.content {
// 元素
width: $contentWidth;
height: $contentHeight;
background-color: #ffffff;
box-shadow: 0px 16px 40px rgba(0, 0, 0, 0.07);
border-radius: 20px;
// 定位
position: relative;
.pic {
// 元素
width: $picWidth;
height: $picHeight;
background-image: url($picImage);
background-repeat: no-repeat;
background-size: cover;
border-radius: 20px 0 0 20px;
// 定位
position: absolute;
top: 0;
left: 0;
}
.field {
width: $fieldWidth;
height: $fieldHeight;
// 定位
position: absolute;
top: 0;
left: $picWidth;
display:flex;
justify-content: center;
align-items: center;
.pc-title{ width: 100%; clear: both;}
.mobile-title,
.mobile-switch {
display: none;
}
.form {
box-sizing: border-box;
width: $formWidth;
// - - - tab
:deep(.el-tabs__content) {
padding: 20px 0 0;
}
:deep(.el-tabs__item) {
// 元素
width: $tabWidth;
height: $rowHeight;
padding: 0;
// 文字
line-height: $rowHeight;
color: #666666;
}
:deep(.el-tabs__item.is-active) {
font-weight: bold;
color: #2F53EB;
}
:deep(.el-tabs__active-bar) {
height: 3px;
border-radius: 2px;
}
// - - - input
:deep(.el-input__inner) {
// 元素
width: 100%;
height: $rowHeight;
background: #f5f5f5;
border: 0;
border-radius: 28px;
// 文字
text-align: center;
line-height: 19px;
color: #262626;
}
.code:deep(.el-input__inner) {
padding: 0 24px;
// 文字
text-align: left;
}
:deep(.el-input__inner::-webkit-input-placeholder) { /* WebKit browsers */
font-weight: 400;
color: #8C8C8C;
}
:deep(.el-input__inner:-moz-placeholder) { /* Mozilla Firefox 4 to 18 */
font-weight: 400;
color: #8C8C8C;
}
:deep(.el-input__inner::-moz-placeholder) { /* Mozilla Firefox 19+ */
font-weight: 400;
color: #8C8C8C;
opacity:1;
}
:deep(.el-input__inner:-ms-input-placeholder) { /* Internet Explorer 10+ */
font-weight: 400;
color: #8C8C8C !important;
}
:deep(.el-form-item) {
position: relative;
.button-code {
// 元素
height: $rowHeight;
box-sizing: border-box;
// 定位
position: absolute;
top: 0;
right: 20px;
z-index: 1;
// 文字
line-height: 20px;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
color: #2F53EB;
span {
padding-left: 15px;
border-left: 2px solid #D9D9D9;
}
}
}
:deep(.el-form-item__error) {
padding-left: 24px;
}
.button {
width: 100%;
height: $buttonHeight;
background: rgba(24, 144, 255, 0.2);
border: 0;
border-radius: 24px;
margin-bottom: 20px;
// 文字
line-height: 26px;
font-size: 20px;
color: #FFFFFF;
}
.button-active {
background: #2F53EB;
box-shadow: 0px 2px 8px rgba(0, 80, 184, 0.2);
}
}
}
}
.footer {
// 元素
height: 16px;
line-height: 16px;
font-size: 12px;
color: #8c8c8c;
// 定位
position: absolute;
bottom: 30px;
a,
a:hover,
a:active {
color: inherit;
text-decoration: none;
}
}
}
// - - - - - PC 最小尺寸设置
@media screen and (min-width: 599px) and (max-width: 1366px) {
.container {
.content {
width: 710px;
height: 397px;
.pic {
width: 314px;
}
.field {
width: calc(710px - 314px);
left: 314px;
.form {
width: 320px;
:deep(.el-input__inner) {
width: 320px;
height: 56px;
}
.button {
height: 50px;
}
}
}
}
}
}
/* ===== MOBILE DESIGN ===== */
$mobileW: 375;
$mobileH: 812;
$mobileContentW: 327;
$mobileContentH: 376;
$mobileFormW: 280;
$mobileRowH: 48;
$mobileButtonH: 48;
// container
$mobileContainerBgImage: '../assets/images/bg-mobile.png';
// container-content
$mobileContentWidth: round($mobileContentW / $mobileW * 100) * 1vw;
$mobileContentHeight: round($mobileContentH / $mobileW * 100) / 100 * $mobileContentWidth;
// container-content-field-form
$mobileFormWidth: round($mobileFormW / $mobileW *100) * 1vw;
$mobileRowHeight: $mobileRowH * 1px;
$mobileButtonHeight: $mobileButtonH * 1px;
$iconBgImage: '../assets/images/icon.png';
// - - - - - 移动端设置
@media screen and (max-width: 599px) {
.container {
// 元素
background-image: url($mobileContainerBgImage);
min-width: 280px;
min-height: 568px;
// 文字
font-size: 17px;
font-family: PingFang SC;
font-weight: bold;
.logo {
display: none;
}
.content {
// 元素
width: $mobileContentWidth;
height: $mobileContentHeight;
min-width: 250px;
min-height: 340px;
// 定位
display: flex;
justify-content: center;
align-items: center;
.pic {
display: none;
}
.field {
// 元素
width: inherit;
min-height: inherit;
// 定位
left: 0;
display: flex;
flex-direction: column;
.mobile-title {
// 元素
margin: 0 0 20px;
display: block;
}
.form {
width: $mobileFormWidth;
// - - - tab
:deep(.el-tabs__header) {
display: none;
}
:deep(.el-tabs__content) {
padding: 0;
}
// - - - input
:deep(.el-input__inner) {
height: $mobileRowHeight;
line-height: 24px;
// 文字
text-align: center;
color: #262626;
}
:deep(.el-form-item) {
.button-code {
// 元素
height: $mobileRowHeight;
}
}
.button {
height: $mobileButtonHeight;
line-height: 24px;
color: #FFFFFF;
}
}
.mobile-switch {
display: block;
line-height: 20px;
font-size: 14px;
font-weight: 400;
color: #595959;
margin: 0;
.icon {
width: 14px;
height: 14px;
display: inline-block;
background-image: url($iconBgImage);
background-size: cover;
}
}
.mobile-switch:hover {
cursor: pointer;
}
}
}
.footer {
// 元素
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 17px;
color: #333333;
opacity: 0.6;
// 定位
position: absolute;
bottom: 20px;
}
}
}

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

@ -2,7 +2,7 @@
<div class="component-upload-image">
<el-upload
multiple
:action="uploadImgUrl"
:action="uploadFileUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
@ -43,7 +43,7 @@
</template>
<script>
import { getToken } from "@/utils/auth";
import { getAccessToken } from "@/utils/auth";
export default {
props: {
@ -76,11 +76,8 @@ export default {
dialogImageUrl: "",
dialogVisible: false,
hideUpload: false,
baseUrl: process.env.VUE_APP_BASE_API,
uploadImgUrl: 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: []
};
},
@ -93,11 +90,8 @@ export default {
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -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;
});
@ -127,7 +121,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 = [];
@ -188,7 +183,7 @@ export default {
for (let i in list) {
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

@ -292,8 +292,8 @@ export default {
if (task.endTime) {
html += `<p>结束时间:${this.parseTime(task.endTime)}</p>`
}
if (task.comment) {
html += `<p>审批建议:${task.comment}</p>`
if (task.reason) {
html += `<p>审批建议:${task.reason}</p>`
}
} else if (element.type === 'bpmn:EndEvent' && this.processInstance) {
html = `<p>结果:${this.getDictDataLabel(this.DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, this.processInstance.result)}</p>`;

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} from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import {login, logout, getInfo, socialQuickLogin, socialBindLogin, smsLogin} from '@/api/login'
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,15 +74,29 @@ 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)
})
})
},
// 登录
SmsLogin({ commit }, userInfo) {
const mobile = userInfo.mobile.trim()
const mobileCode = userInfo.mobileCode
return new Promise((resolve, reject) => {
smsLogin(mobile,mobileCode).then(res => {
res = res.data;
// 设置 token
setToken(res)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
@ -128,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()
@ -137,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: 10000
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

@ -39,7 +39,7 @@ export default {
simulation: true,
labelEditing: false,
labelVisible: false,
prefix: "activiti",
prefix: "flowable",
headerButtonSize: "mini",
additionalModel: [CustomContentPadProvider, CustomPaletteProvider]
},

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

@ -14,8 +14,8 @@
{{ processInstance.startUser.nickname }}
<el-tag type="info" size="mini">{{ processInstance.startUser.deptName }}</el-tag>
</el-form-item>
<el-form-item label="审批建议" prop="comment">
<el-input type="textarea" v-model="auditForms[index].comment" placeholder="请输入审批建议" />
<el-form-item label="审批建议" prop="reason">
<el-input type="textarea" v-model="auditForms[index].reason" placeholder="请输入审批建议" />
</el-form-item>
</el-form>
<div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px;">
@ -66,8 +66,8 @@
<label v-if="item.endTime" style="color:#8a909c;font-weight: normal"> {{ parseTime(item.endTime) }}</label>
<label v-if="item.durationInMillis" style="margin-left: 30px;font-weight: normal">耗时</label>
<label v-if="item.durationInMillis" style="color:#8a909c;font-weight: normal"> {{ getDateStar(item.durationInMillis) }} </label>
<p v-if="item.comment">
<el-tag :type="getTimelineItemType(item)">{{ item.comment }}</el-tag>
<p v-if="item.reason">
<el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag>
</p>
</el-card>
</el-timeline-item>
@ -148,7 +148,7 @@ export default {
runningTasks: [],
auditForms: [],
auditRule: {
comment: [{ required: true, message: "审批建议不能为空", trigger: "blur" }],
reason: [{ required: true, message: "审批建议不能为空", trigger: "blur" }],
},
// 转派审批人
@ -259,7 +259,7 @@ export default {
}
this.runningTasks.push({...task});
this.auditForms.push({
comment: ''
reason: ''
})
});
@ -351,7 +351,7 @@ export default {
}
const data = {
id: task.id,
comment: this.auditForms[index].comment
reason: this.auditForms[index].reason
}
if (pass) {
approveTask(data).then(response => {

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

@ -28,7 +28,7 @@
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result"/>
</template>
</el-table-column>
<el-table-column label="审批意见" align="center" prop="comment" width="200" />
<el-table-column label="审批意见" align="center" prop="reason" width="200" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>

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

@ -41,7 +41,7 @@
<el-table v-loading="loading" :data="configList">
<el-table-column label="参数主键" align="center" prop="id" />
<el-table-column label="参数分" align="center" prop="group" />
<el-table-column label="参数分" align="center" prop="group" />
<el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="参数键名" align="center" prop="key" :show-overflow-tooltip="true" />
<el-table-column label="参数键值" align="center" prop="value" />
@ -50,9 +50,9 @@
<dict-tag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="是否敏感" align="center" prop="sensitive">
<el-table-column label="是否可见" align="center" prop="visible">
<template slot-scope="scope">
<span>{{ scope.row.sensitive ? '是' : '否' }}</span>
<span>{{ scope.row.visible ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
@ -76,8 +76,8 @@
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="参数分" prop="group">
<el-input v-model="form.group" placeholder="请输入参数分" />
<el-form-item label="参数分" prop="category">
<el-input v-model="form.category" placeholder="请输入参数分" />
</el-form-item>
<el-form-item label="参数名称" prop="name">
<el-input v-model="form.name" placeholder="请输入参数名称" />
@ -88,8 +88,8 @@
<el-form-item label="参数键值" prop="value">
<el-input v-model="form.value" placeholder="请输入参数键值" />
</el-form-item>
<el-form-item label="是否敏感" prop="type">
<el-radio-group v-model="form.sensitive">
<el-form-item label="是否可见" prop="type">
<el-radio-group v-model="form.visible">
<el-radio :key="true" :label="true"></el-radio>
<el-radio :key="false" :label="false"></el-radio>
</el-radio-group>
@ -143,8 +143,8 @@ export default {
form: {},
// 表单校验
rules: {
group: [
{ required: true, message: "参数分不能为空", trigger: "blur" }
category: [
{ required: true, message: "参数分不能为空", trigger: "blur" }
],
name: [
{ required: true, message: "参数名称不能为空", trigger: "blur" }

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

@ -1,61 +1,123 @@
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">芋道后台管理系统</h3>
<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="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" @keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnable">
<el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
</el-form-item>
<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>
<el-form-item style="width:100%;">
<div class="oauth-login" style="display:flex">
<div class="oauth-login-item" v-for="item in SysUserSocialTypeEnum" :key="item.type" @click="doSocialLogin(item)">
<img :src="item.img" height="25px" width="25px" alt="登录" >
<span>{{item.title}}</span>
</div>
<!-- 表单 -->
<div class="form-cont">
<el-tabs class="form" v-model="loginForm.loginType" style=" float:none;">
<el-tab-pane label="账号密码登录" name="uname">
</el-tab-pane>
<el-tab-pane label="短信验证码登录" name="sms">
</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>
<!-- 账号密码登录 -->
<div v-if="loginForm.loginType === 'uname'">
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码"
@keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnable">
<el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0 0 25px 0;">记住密码</el-checkbox>
</div>
<!-- 短信验证码登录 -->
<div v-if="loginForm.loginType === 'sms'">
<el-form-item prop="mobile">
<el-input v-model="loginForm.mobile" type="text" auto-complete="off" placeholder="请输入手机号">
<svg-icon slot="prefix" icon-class="phone" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="mobileCode">
<el-input v-model="loginForm.mobileCode" type="text" auto-complete="off" placeholder="短信验证码"
@keyup.enter.native="handleLogin">
<template slot="icon">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
</template>
<template slot="append">
<span v-if="mobileCodeTimer <= 0" class="getMobileCode" @click="getSmsCode" style="cursor: pointer;">获取验证码</span>
<span v-if="mobileCodeTimer > 0" class="getMobileCode">{{ mobileCodeTimer }}秒后可重新获取</span>
</template>
</el-input>
</el-form-item>
</div>
<!-- 下方的登录按钮 -->
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;"
@click.native.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
</el-form-item>
<!-- 社交登录 -->
<el-form-item style="width:100%;">
<div class="oauth-login" style="display:flex">
<div class="oauth-login-item" v-for="item in SysUserSocialTypeEnum" :key="item.type" @click="doSocialLogin(item)">
<img :src="item.img" height="25px" width="25px" alt="登录" >
<span>{{item.title}}</span>
</div>
</div>
</el-form-item>
</el-form>
</div>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2020-2021 iocoder.cn All Rights Reserved.</span>
</div>
</div>
<!-- footer -->
<div class="footer">
Copyright © 2020-2022 iocoder.cn All Rights Reserved.
</div>
</div>
</template>
<script>
import { getCodeImg,socialAuthRedirect } from "@/api/login";
import { getTenantIdByName } from "@/api/system/tenant";
import {getCodeImg, sendSmsCode, socialAuthRedirect} from "@/api/login";
import {getTenantIdByName} from "@/api/system/tenant";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
import {SystemUserSocialTypeEnum} from "@/utils/constants";
import { getTenantEnable } from "@/utils/ruoyi";
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",
@ -64,31 +126,50 @@ export default {
codeUrl: "",
captchaEnable: true,
tenantEnable: true,
mobileCodeTimer: 0,
loginForm: {
loginType: "uname",
username: "admin",
password: "admin123",
mobile: "",
mobileCode: "",
rememberMe: false,
code: "",
uuid: "",
tenantName: "芋道源码",
},
loginRules: {
scene: 21,
LoginRules: {
username: [
{ required: true, trigger: "blur", message: "用户名不能为空" }
{required: true, trigger: "blur", message: "用户名不能为空"}
],
password: [
{ required: true, trigger: "blur", message: "密码不能为空" }
{required: true, trigger: "blur", message: "密码不能为空"}
],
code: [{required: true, trigger: "change", message: "验证码不能为空"}],
mobile: [
{required: true, trigger: "blur", message: "手机号不能为空"},
{
validator: function (rule, value, callback) {
if (/^1[0-9]\d{9}$/.test(value) == false) {
callback(new Error("手机号格式错误"));
} else {
callback();
}
}, trigger: "blur"
}
],
code: [{ required: true, trigger: "change", message: "验证码不能为空" }],
tenantName: [
{ required: true, trigger: "blur", message: "租户不能为空" },
{required: true, trigger: "blur", message: "租户不能为空"},
{
validator: (rule, value, callback) => {
// debugger
getTenantIdByName(value).then(res => {
const tenantId = res.data;
if (tenantId && tenantId >= 0) {
// 设置租户
Cookies.set("tenantId", tenantId);
setTenantId(tenantId)
callback();
} else {
callback('租户不存在');
@ -97,7 +178,7 @@ export default {
},
trigger: 'blur'
}
],
]
},
loading: false,
redirect: undefined,
@ -138,15 +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 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,
...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() {
@ -155,19 +237,21 @@ 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()
}
// 发起登陆
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
// console.log("发起登录", this.loginForm);
this.$store.dispatch(this.loginForm.loginType === "sms" ? "SmsLogin" : "Login", this.loginForm).then(() => {
this.$router.push({path: this.redirect || "/"}).catch(() => {
});
}).catch(() => {
this.loading = false;
this.getCode();
@ -187,74 +271,34 @@ export default {
// console.log(res.url);
window.location.href = res.data;
});
},
/** ========== 以下为升级短信登录 ========== */
getSmsCode() {
if (this.mobileCodeTimer > 0) return;
this.$refs.loginForm.validate(valid => {
if (!valid) return;
sendSmsCode(this.loginForm.mobile, this.scene, this.loginForm.uuid, this.loginForm.code).then(res => {
this.$modal.msgSuccess("获取验证码成功")
this.mobileCodeTimer = 60;
let msgTimer = setInterval(() => {
this.mobileCodeTimer = this.mobileCodeTimer - 1;
if (this.mobileCodeTimer <= 0) {
clearInterval(msgTimer);
}
}, 1000);
});
});
}
}
};
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/login.scss";
<style rel="stylesheet/scss" lang="scss">
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("http://static.yudao.iocoder.cn/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 500px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 38px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 38px;
}
.oauth-login {
display: flex;
align-items: cen;
cursor:pointer;
}
.oauth-login-item {

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

@ -1,28 +1,63 @@
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">绑定账号</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" @keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
</el-form-item>
<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>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2020-2021 iocoder.cn All Rights Reserved.</span>
<!-- 表单 -->
<div class="form-cont">
<el-tabs class="form" v-model="loginForm.loginType " style=" float:none;">
<el-tab-pane label="绑定账号" name="uname">
</el-tab-pane>
</el-tabs>
<div>
<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="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码"
@keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnable">
<el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0 0 25px 0;">记住密码</el-checkbox>
<!-- 下方的登录按钮 -->
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;"
@click.native.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</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>
@ -30,15 +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: [
@ -68,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;
@ -83,14 +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 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)
username: username ? username : this.loginForm.username,
password: password ? password : this.loginForm.password,
rememberMe: rememberMe ? getRememberMe() : false,
loginType: this.loginForm.loginType,
};
},
handleLogin() {
@ -98,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,
@ -123,64 +189,5 @@ export default {
</script>
<style rel="stylesheet/scss" lang="scss">
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("http://static.yudao.iocoder.cn/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 38px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 38px;
}
@import "~@/assets/styles/login.scss";
</style>

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,300 @@
<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="可重定向的 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="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="权限" 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: {