36 changed files with 9 additions and 1883 deletions
@ -1,81 +0,0 @@ |
|||||||
import { defHttp } from '@/utils/http/axios' |
|
||||||
import { getRefreshToken } from '@/utils/auth' |
|
||||||
|
|
||||||
enum Api { |
|
||||||
Login = '/system/auth/login', |
|
||||||
RefreshToken = '/system/auth/refresh-token?refreshToken=', |
|
||||||
GetTenantIdByName = '/system/tenant/get-id-by-name?name=', |
|
||||||
LoginOut = '/system/auth/logout', |
|
||||||
GetUserInfo = '/system/auth/get-permission-info', |
|
||||||
GetCaptcha = '/system/captcha/get', |
|
||||||
CheckCaptcha = '/system/captcha/check', |
|
||||||
} |
|
||||||
|
|
||||||
// 刷新访问令牌
|
|
||||||
export function refreshToken() { |
|
||||||
const refreshToken: string = getRefreshToken() |
|
||||||
return defHttp.post({ url: Api.RefreshToken + refreshToken }) |
|
||||||
} |
|
||||||
|
|
||||||
// 登出
|
|
||||||
export function loginOut() { |
|
||||||
return defHttp.delete({ url: Api.LoginOut }) |
|
||||||
} |
|
||||||
|
|
||||||
// 获取用户权限信息
|
|
||||||
export function getUserInfo() { |
|
||||||
return defHttp.get({ url: Api.GetUserInfo }) |
|
||||||
} |
|
||||||
|
|
||||||
// 获取登录验证码
|
|
||||||
export function sendSmsCode(mobile, scene) { |
|
||||||
return defHttp.post({ |
|
||||||
url: '/system/auth/send-sms-code', |
|
||||||
data: { |
|
||||||
mobile, |
|
||||||
scene, |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 获取验证图片 以及token
|
|
||||||
export function getCaptcha(data) { |
|
||||||
return defHttp.post({ url: Api.GetCaptcha, data }, { isReturnNativeResponse: true }) |
|
||||||
} |
|
||||||
|
|
||||||
// 滑动或者点选验证
|
|
||||||
export function checkCaptcha(data) { |
|
||||||
return defHttp.post({ url: Api.CheckCaptcha, data }, { isReturnNativeResponse: true }) |
|
||||||
} |
|
||||||
|
|
||||||
// ========== OAUTH 2.0 相关 ==========
|
|
||||||
|
|
||||||
export function getAuthorize(clientId) { |
|
||||||
return defHttp.get({ url: `/system/oauth2/authorize?clientId=${clientId}` }) |
|
||||||
} |
|
||||||
|
|
||||||
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 defHttp.post({ |
|
||||||
url: '/system/oauth2/authorize', |
|
||||||
headers: { |
|
||||||
'Content-type': 'application/x-www-form-urlencoded', |
|
||||||
}, |
|
||||||
params: { |
|
||||||
response_type: responseType, |
|
||||||
client_id: clientId, |
|
||||||
redirect_uri: redirectUri, |
|
||||||
state, |
|
||||||
auto_approve: autoApprove, |
|
||||||
scope: JSON.stringify(scopes), |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
export interface UserLoginVO { |
|
||||||
username: string |
|
||||||
password: string |
|
||||||
captchaVerification: string |
|
||||||
} |
|
||||||
|
|
||||||
export interface TentantNameVO { |
|
||||||
id: number |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
import type { RouteMeta } from 'vue-router' |
|
||||||
|
|
||||||
export interface RouteItem { |
|
||||||
path: string |
|
||||||
component: any |
|
||||||
meta: RouteMeta |
|
||||||
name?: string |
|
||||||
alias?: string | string[] |
|
||||||
redirect?: string |
|
||||||
caseSensitive?: boolean |
|
||||||
children?: RouteItem[] |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export interface UploadApiResult { |
|
||||||
message: string |
|
||||||
code: number |
|
||||||
url: string |
|
||||||
} |
|
@ -1,38 +0,0 @@ |
|||||||
/** |
|
||||||
* @description: Login interface parameters |
|
||||||
*/ |
|
||||||
export interface LoginParams { |
|
||||||
username: string |
|
||||||
password: string |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @description: SmsLogin interface parameters |
|
||||||
*/ |
|
||||||
export interface SmsLoginParams { |
|
||||||
mobile: number |
|
||||||
code: number |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @description: Login interface return value |
|
||||||
*/ |
|
||||||
export interface LoginResultModel { |
|
||||||
userId: string | number |
|
||||||
accessToken: string |
|
||||||
refreshToken: string |
|
||||||
expiresTime: number |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @description: Get user information return value |
|
||||||
*/ |
|
||||||
export interface GetUserInfoModel { |
|
||||||
user: userModel |
|
||||||
} |
|
||||||
|
|
||||||
export interface userModel { |
|
||||||
id: string | number |
|
||||||
avatar: string |
|
||||||
nickname: string |
|
||||||
} |
|
@ -1,128 +0,0 @@ |
|||||||
import { ContentTypeEnum } from '@/enums/httpEnum' |
|
||||||
import { defHttp } from '@/utils/http/axios' |
|
||||||
|
|
||||||
export interface ProfileDept { |
|
||||||
id: number |
|
||||||
name: string |
|
||||||
} |
|
||||||
export interface ProfileRole { |
|
||||||
id: number |
|
||||||
name: string |
|
||||||
} |
|
||||||
export interface ProfilePost { |
|
||||||
id: number |
|
||||||
name: string |
|
||||||
} |
|
||||||
export interface SocialUser { |
|
||||||
id: number |
|
||||||
type: number |
|
||||||
openid: string |
|
||||||
token: string |
|
||||||
rawTokenInfo: string |
|
||||||
nickname: string |
|
||||||
avatar: string |
|
||||||
rawUserInfo: string |
|
||||||
code: string |
|
||||||
state: string |
|
||||||
} |
|
||||||
export interface ProfileVO { |
|
||||||
id: number |
|
||||||
username: string |
|
||||||
nickname: string |
|
||||||
dept: ProfileDept |
|
||||||
roles: ProfileRole[] |
|
||||||
posts: ProfilePost[] |
|
||||||
socialUsers: SocialUser[] |
|
||||||
email: string |
|
||||||
mobile: string |
|
||||||
sex: number |
|
||||||
avatar: string |
|
||||||
status: number |
|
||||||
remark: string |
|
||||||
loginIp: string |
|
||||||
loginDate: Date |
|
||||||
createTime: Date |
|
||||||
} |
|
||||||
|
|
||||||
export interface UserProfileUpdateReqVO { |
|
||||||
nickname: string |
|
||||||
email: string |
|
||||||
mobile: string |
|
||||||
sex: number |
|
||||||
} |
|
||||||
|
|
||||||
enum Api { |
|
||||||
getUserProfileApi = '/system/user/profile/get', |
|
||||||
putUserProfileApi = '/system/user/profile/update', |
|
||||||
uploadAvatarApi = '/system/user/profile/update-avatar', |
|
||||||
updateUserPwdApi = '/system/user/profile/update-password', |
|
||||||
socialBindApi = '/system/social-user/bind', |
|
||||||
socialUnbindApi = '/system/social-user/unbind', |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @description: getUserProfileApi |
|
||||||
*/ |
|
||||||
export function getUserProfileApi() { |
|
||||||
return defHttp.get({ url: Api.getUserProfileApi }) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @description: updateUserProfileApi |
|
||||||
*/ |
|
||||||
export function updateUserProfileApi(data: UserProfileUpdateReqVO) { |
|
||||||
return defHttp.put({ url: Api.putUserProfileApi, data }) |
|
||||||
} |
|
||||||
|
|
||||||
// 用户密码重置
|
|
||||||
export function updateUserPwdApi(oldPassword: string, newPassword: string) { |
|
||||||
return defHttp.put({ |
|
||||||
url: Api.updateUserPwdApi, |
|
||||||
data: { |
|
||||||
oldPassword, |
|
||||||
newPassword, |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 用户头像上传
|
|
||||||
export function uploadAvatarApi(data) { |
|
||||||
return defHttp.put({ |
|
||||||
url: Api.uploadAvatarApi, |
|
||||||
headers: { |
|
||||||
'Content-type': ContentTypeEnum.FORM_DATA, |
|
||||||
'ignoreCancelToken': true, |
|
||||||
}, |
|
||||||
data, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 社交绑定,使用 code 授权码
|
|
||||||
export function socialBind(type, code, state) { |
|
||||||
return defHttp.post({ |
|
||||||
url: Api.socialBindApi, |
|
||||||
data: { |
|
||||||
type, |
|
||||||
code, |
|
||||||
state, |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 取消社交绑定
|
|
||||||
export function socialUnbind(type, openid) { |
|
||||||
return defHttp.delete({ |
|
||||||
url: Api.socialUnbindApi, |
|
||||||
data: { |
|
||||||
type, |
|
||||||
openid, |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 社交授权的跳转
|
|
||||||
export function socialAuthRedirect(type, redirectUri) { |
|
||||||
return defHttp.get({ |
|
||||||
url: `/system/auth/social-auth-redirect?type=${type}&redirectUri=${redirectUri}`, |
|
||||||
}) |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
import verify from './src/Verify.vue' |
|
||||||
import { withInstall } from '@/utils/index' |
|
||||||
|
|
||||||
export const Verify = withInstall(verify) |
|
@ -1,132 +0,0 @@ |
|||||||
<script type="text/babel"> |
|
||||||
/** |
|
||||||
* Verify 验证码组件 |
|
||||||
* @description 分发验证码使用 |
|
||||||
*/ |
|
||||||
import { computed, ref, toRefs, watchEffect } from 'vue' |
|
||||||
import VerifySlide from './Verify/VerifySlide.vue' |
|
||||||
import VerifyPoints from './Verify/VerifyPoints.vue' |
|
||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
import './style/verify.css' |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Vue3Verify', |
|
||||||
components: { |
|
||||||
VerifySlide, |
|
||||||
VerifyPoints, |
|
||||||
}, |
|
||||||
props: { |
|
||||||
captchaType: { |
|
||||||
type: String, |
|
||||||
required: true, |
|
||||||
}, |
|
||||||
figure: { |
|
||||||
type: Number, |
|
||||||
}, |
|
||||||
arith: { |
|
||||||
type: Number, |
|
||||||
}, |
|
||||||
mode: { |
|
||||||
type: String, |
|
||||||
default: 'pop', |
|
||||||
}, |
|
||||||
vSpace: { |
|
||||||
type: Number, |
|
||||||
}, |
|
||||||
explain: { |
|
||||||
type: String, |
|
||||||
}, |
|
||||||
imgSize: { |
|
||||||
type: Object, |
|
||||||
default() { |
|
||||||
return { |
|
||||||
width: '310px', |
|
||||||
height: '155px', |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
blockSize: { |
|
||||||
type: Object, |
|
||||||
}, |
|
||||||
barSize: { |
|
||||||
type: Object, |
|
||||||
}, |
|
||||||
}, |
|
||||||
setup(props) { |
|
||||||
const { t } = useI18n() |
|
||||||
const { captchaType, mode } = toRefs(props) |
|
||||||
const clickShow = ref(false) |
|
||||||
const verifyType = ref(undefined) |
|
||||||
const componentType = ref(undefined) |
|
||||||
|
|
||||||
const instance = ref({}) |
|
||||||
|
|
||||||
const showBox = computed(() => { |
|
||||||
if (mode.value === 'pop') |
|
||||||
return clickShow.value |
|
||||||
else |
|
||||||
return true |
|
||||||
}) |
|
||||||
/** |
|
||||||
* refresh |
|
||||||
* @description 刷新 |
|
||||||
*/ |
|
||||||
const refresh = () => { |
|
||||||
if (instance.value.refresh) |
|
||||||
instance.value.refresh() |
|
||||||
} |
|
||||||
const closeBox = () => { |
|
||||||
clickShow.value = false |
|
||||||
refresh() |
|
||||||
} |
|
||||||
const show = () => { |
|
||||||
if (mode.value === 'pop') |
|
||||||
clickShow.value = true |
|
||||||
} |
|
||||||
watchEffect(() => { |
|
||||||
switch (captchaType.value) { |
|
||||||
case 'blockPuzzle': |
|
||||||
verifyType.value = '2' |
|
||||||
componentType.value = 'VerifySlide' |
|
||||||
break |
|
||||||
case 'clickWord': |
|
||||||
verifyType.value = '' |
|
||||||
componentType.value = 'VerifyPoints' |
|
||||||
break |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
return { |
|
||||||
t, |
|
||||||
clickShow, |
|
||||||
verifyType, |
|
||||||
componentType, |
|
||||||
instance, |
|
||||||
showBox, |
|
||||||
closeBox, |
|
||||||
show, |
|
||||||
} |
|
||||||
}, |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<div v-show="showBox" :class="mode === 'pop' ? 'mask' : ''"> |
|
||||||
<div :class="mode === 'pop' ? 'verifybox' : ''" :style="{ 'max-width': `${parseInt(imgSize.width) + 20}px` }"> |
|
||||||
<div v-if="mode === 'pop'" class="verifybox-top"> |
|
||||||
{{ t('component.captcha.verification') }} |
|
||||||
<span class="verifybox-close" @click="closeBox"> |
|
||||||
<i class="iconfont icon-close" /> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div class="verifybox-bottom" :style="{ padding: mode === 'pop' ? '10px' : '0' }"> |
|
||||||
<!-- 验证码容器 --> |
|
||||||
<component |
|
||||||
:is="componentType" v-if="componentType" ref="instance" :captcha-type="captchaType" :type="verifyType" |
|
||||||
:figure="figure" :arith="arith" :mode="mode" :spaces="vSpace" :explain="explain" :img-size="imgSize" |
|
||||||
:block-size="blockSize" :bar-size="barSize" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</template> |
|
@ -1,256 +0,0 @@ |
|||||||
<script type="text/babel" setup> |
|
||||||
/** |
|
||||||
* VerifyPoints |
|
||||||
* @description 点选 |
|
||||||
*/ |
|
||||||
import { getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs } from 'vue' |
|
||||||
import { resetSize } from './../utils/util' |
|
||||||
import { aesEncrypt } from './../utils/ase' |
|
||||||
import { checkCaptcha, getCaptcha } from '@/api/base/login' |
|
||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
|
|
||||||
const props = defineProps({ |
|
||||||
// 弹出式pop,固定fixed |
|
||||||
mode: { |
|
||||||
type: String, |
|
||||||
default: 'fixed', |
|
||||||
}, |
|
||||||
captchaType: { |
|
||||||
type: String, |
|
||||||
}, |
|
||||||
// 间隔 |
|
||||||
spaces: { |
|
||||||
type: Number, |
|
||||||
default: 5, |
|
||||||
}, |
|
||||||
imgSize: { |
|
||||||
type: Object, |
|
||||||
default() { |
|
||||||
return { |
|
||||||
width: '310px', |
|
||||||
height: '155px', |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
barSize: { |
|
||||||
type: Object, |
|
||||||
default() { |
|
||||||
return { |
|
||||||
width: '310px', |
|
||||||
height: '40px', |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
const { t } = useI18n() |
|
||||||
const { mode, captchaType } = toRefs(props) |
|
||||||
const { proxy } = getCurrentInstance() |
|
||||||
const secretKey = ref('') // 后端返回的ase加密秘钥 |
|
||||||
const checkNum = ref(3) // 默认需要点击的字数 |
|
||||||
const fontPos = reactive([]) // 选中的坐标信息 |
|
||||||
const checkPosArr = reactive([]) // 用户点击的坐标 |
|
||||||
const num = ref(1) // 点击的记数 |
|
||||||
const pointBackImgBase = ref('') // 后端获取到的背景图片 |
|
||||||
const poinTextList = reactive([]) // 后端返回的点击字体顺序 |
|
||||||
const backToken = ref('') // 后端返回的token值 |
|
||||||
const setSize = reactive({ |
|
||||||
imgHeight: 0, |
|
||||||
imgWidth: 0, |
|
||||||
barHeight: 0, |
|
||||||
barWidth: 0, |
|
||||||
}) |
|
||||||
const tempPoints = reactive([]) |
|
||||||
const text = ref('') |
|
||||||
const barAreaColor = ref(undefined) |
|
||||||
const barAreaBorderColor = ref(undefined) |
|
||||||
const showRefresh = ref(true) |
|
||||||
const bindingClick = ref(true) |
|
||||||
|
|
||||||
function init() { |
|
||||||
// 加载页面 |
|
||||||
fontPos.splice(0, fontPos.length) |
|
||||||
checkPosArr.splice(0, checkPosArr.length) |
|
||||||
num.value = 1 |
|
||||||
getPictrue() |
|
||||||
nextTick(() => { |
|
||||||
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy) |
|
||||||
setSize.imgHeight = imgHeight |
|
||||||
setSize.imgWidth = imgWidth |
|
||||||
setSize.barHeight = barHeight |
|
||||||
setSize.barWidth = barWidth |
|
||||||
proxy.$parent.$emit('ready', proxy) |
|
||||||
}) |
|
||||||
} |
|
||||||
onMounted(() => { |
|
||||||
// 禁止拖拽 |
|
||||||
init() |
|
||||||
proxy.$el.onselectstart = function () { |
|
||||||
return false |
|
||||||
} |
|
||||||
}) |
|
||||||
const canvas = ref(null) |
|
||||||
|
|
||||||
// 获取坐标 |
|
||||||
const getMousePos = function (obj, e) { |
|
||||||
const x = e.offsetX |
|
||||||
const y = e.offsetY |
|
||||||
return { x, y } |
|
||||||
} |
|
||||||
// 创建坐标点 |
|
||||||
const createPoint = function (pos) { |
|
||||||
tempPoints.push(Object.assign({}, pos)) |
|
||||||
return num.value + 1 |
|
||||||
} |
|
||||||
|
|
||||||
// 坐标转换函数 |
|
||||||
const pointTransfrom = function (pointArr, imgSize) { |
|
||||||
const newPointArr = pointArr.map((p) => { |
|
||||||
const x = Math.round((310 * p.x) / Number.parseInt(imgSize.imgWidth)) |
|
||||||
const y = Math.round((155 * p.y) / Number.parseInt(imgSize.imgHeight)) |
|
||||||
return { x, y } |
|
||||||
}) |
|
||||||
return newPointArr |
|
||||||
} |
|
||||||
|
|
||||||
const refresh = async function () { |
|
||||||
tempPoints.splice(0, tempPoints.length) |
|
||||||
barAreaColor.value = '#000' |
|
||||||
barAreaBorderColor.value = '#ddd' |
|
||||||
bindingClick.value = true |
|
||||||
fontPos.splice(0, fontPos.length) |
|
||||||
checkPosArr.splice(0, checkPosArr.length) |
|
||||||
num.value = 1 |
|
||||||
await getPictrue() |
|
||||||
showRefresh.value = true |
|
||||||
} |
|
||||||
|
|
||||||
function canvasClick(e) { |
|
||||||
checkPosArr.push(getMousePos(canvas, e)) |
|
||||||
if (num.value === checkNum.value) { |
|
||||||
num.value = createPoint(getMousePos(canvas, e)) |
|
||||||
// 按比例转换坐标值 |
|
||||||
const arr = pointTransfrom(checkPosArr, setSize) |
|
||||||
checkPosArr.length = 0 |
|
||||||
checkPosArr.push(...arr) |
|
||||||
// 等创建坐标执行完 |
|
||||||
setTimeout(() => { |
|
||||||
// var flag = this.comparePos(this.fontPos, this.checkPosArr); |
|
||||||
// 发送后端请求 |
|
||||||
const captchaVerification = secretKey.value |
|
||||||
? aesEncrypt(`${backToken.value}---${JSON.stringify(checkPosArr)}`, secretKey.value) |
|
||||||
: `${backToken.value}---${JSON.stringify(checkPosArr)}` |
|
||||||
const data = { |
|
||||||
captchaType: captchaType.value, |
|
||||||
pointJson: secretKey.value ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value) : JSON.stringify(checkPosArr), |
|
||||||
token: backToken.value, |
|
||||||
} |
|
||||||
checkCaptcha(data).then((response) => { |
|
||||||
const res = response.data |
|
||||||
if (res.repCode === '0000') { |
|
||||||
barAreaColor.value = '#4cae4c' |
|
||||||
barAreaBorderColor.value = '#5cb85c' |
|
||||||
text.value = t('component.captcha.success') |
|
||||||
bindingClick.value = false |
|
||||||
if (mode.value === 'pop') { |
|
||||||
setTimeout(() => { |
|
||||||
proxy.$parent.clickShow = false |
|
||||||
refresh() |
|
||||||
}, 1500) |
|
||||||
} |
|
||||||
proxy.$parent.$emit('success', { captchaVerification }) |
|
||||||
} |
|
||||||
else { |
|
||||||
proxy.$parent.$emit('error', proxy) |
|
||||||
barAreaColor.value = '#d9534f' |
|
||||||
barAreaBorderColor.value = '#d9534f' |
|
||||||
text.value = t('component.captcha.fail') |
|
||||||
setTimeout(() => { |
|
||||||
refresh() |
|
||||||
}, 700) |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 400) |
|
||||||
} |
|
||||||
if (num.value < checkNum.value) |
|
||||||
num.value = createPoint(getMousePos(canvas, e)) |
|
||||||
} |
|
||||||
|
|
||||||
// 请求背景图片和验证图片 |
|
||||||
async function getPictrue() { |
|
||||||
const data = { |
|
||||||
captchaType: captchaType.value, |
|
||||||
} |
|
||||||
const res = await getCaptcha(data) |
|
||||||
if (res.data.repCode === '0000') { |
|
||||||
pointBackImgBase.value = res.data.repData.originalImageBase64 |
|
||||||
backToken.value = res.data.repData.token |
|
||||||
secretKey.value = res.data.repData.secretKey |
|
||||||
poinTextList.value = res.data.repData.wordList |
|
||||||
text.value = `${t('component.captcha.point')}【${poinTextList.value.join(',')}】` |
|
||||||
} |
|
||||||
else { |
|
||||||
text.value = res.data.repMsg |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<div style="position: relative"> |
|
||||||
<div class="verify-img-out"> |
|
||||||
<div |
|
||||||
class="verify-img-panel" |
|
||||||
:style="{ |
|
||||||
'width': setSize.imgWidth, |
|
||||||
'height': setSize.imgHeight, |
|
||||||
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`, |
|
||||||
'margin-bottom': `${spaces}px`, |
|
||||||
}" |
|
||||||
> |
|
||||||
<div v-show="showRefresh" class="verify-refresh" style="z-index: 3" @click="refresh"> |
|
||||||
<i class="iconfont icon-refresh" /> |
|
||||||
</div> |
|
||||||
<img |
|
||||||
ref="canvas" |
|
||||||
:src="`data:image/png;base64,${pointBackImgBase}`" |
|
||||||
alt="" |
|
||||||
style=" display: block;width: 100%; height: 100%" |
|
||||||
@click="bindingClick ? canvasClick($event) : undefined" |
|
||||||
> |
|
||||||
|
|
||||||
<div |
|
||||||
v-for="(tempPoint, index) in tempPoints" |
|
||||||
:key="index" |
|
||||||
class="point-area" |
|
||||||
:style="{ |
|
||||||
'background-color': '#1abd6c', |
|
||||||
'color': '#fff', |
|
||||||
'z-index': 9999, |
|
||||||
'width': '20px', |
|
||||||
'height': '20px', |
|
||||||
'text-align': 'center', |
|
||||||
'line-height': '20px', |
|
||||||
'border-radius': '50%', |
|
||||||
'position': 'absolute', |
|
||||||
'top': `${parseInt(tempPoint.y - 10)}px`, |
|
||||||
'left': `${parseInt(tempPoint.x - 10)}px`, |
|
||||||
}" |
|
||||||
> |
|
||||||
{{ index + 1 }} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<!-- 'height': this.barSize.height, --> |
|
||||||
<div |
|
||||||
class="verify-bar-area" |
|
||||||
:style="{ |
|
||||||
'width': setSize.imgWidth, |
|
||||||
'color': barAreaColor, |
|
||||||
'border-color': barAreaBorderColor, |
|
||||||
'line-height': barSize.height, |
|
||||||
}" |
|
||||||
> |
|
||||||
<span class="verify-msg">{{ text }}</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</template> |
|
@ -1,364 +0,0 @@ |
|||||||
<script type="text/babel" setup> |
|
||||||
/** |
|
||||||
* VerifySlide |
|
||||||
* @description 滑块 |
|
||||||
*/ |
|
||||||
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue' |
|
||||||
import { aesEncrypt } from './../utils/ase' |
|
||||||
import { resetSize } from './../utils/util' |
|
||||||
import { checkCaptcha, getCaptcha } from '@/api/base/login' |
|
||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
|
|
||||||
const props = defineProps({ |
|
||||||
captchaType: { |
|
||||||
type: String, |
|
||||||
}, |
|
||||||
type: { |
|
||||||
type: String, |
|
||||||
default: '1', |
|
||||||
}, |
|
||||||
// 弹出式pop,固定fixed |
|
||||||
mode: { |
|
||||||
type: String, |
|
||||||
default: 'fixed', |
|
||||||
}, |
|
||||||
spaces: { |
|
||||||
type: Number, |
|
||||||
default: 5, |
|
||||||
}, |
|
||||||
explain: { |
|
||||||
type: String, |
|
||||||
default: '', |
|
||||||
}, |
|
||||||
imgSize: { |
|
||||||
type: Object, |
|
||||||
default() { |
|
||||||
return { |
|
||||||
width: '310px', |
|
||||||
height: '155px', |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
blockSize: { |
|
||||||
type: Object, |
|
||||||
default() { |
|
||||||
return { |
|
||||||
width: '50px', |
|
||||||
height: '50px', |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
barSize: { |
|
||||||
type: Object, |
|
||||||
default() { |
|
||||||
return { |
|
||||||
width: '310px', |
|
||||||
height: '30px', |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
const { t } = useI18n() |
|
||||||
const { mode, captchaType, type, blockSize, explain } = toRefs(props) |
|
||||||
const { proxy } = getCurrentInstance() |
|
||||||
const secretKey = ref('') // 后端返回的ase加密秘钥 |
|
||||||
const passFlag = ref('') // 是否通过的标识 |
|
||||||
const backImgBase = ref('') // 验证码背景图片 |
|
||||||
const blockBackImgBase = ref('') // 验证滑块的背景图片 |
|
||||||
const backToken = ref('') // 后端返回的唯一token值 |
|
||||||
const startMoveTime = ref('') // 移动开始的时间 |
|
||||||
const endMovetime = ref('') // 移动结束的时间 |
|
||||||
const tipWords = ref('') |
|
||||||
const text = ref('') |
|
||||||
const finishText = ref('') |
|
||||||
const setSize = reactive({ |
|
||||||
imgHeight: 0, |
|
||||||
imgWidth: 0, |
|
||||||
barHeight: 0, |
|
||||||
barWidth: 0, |
|
||||||
}) |
|
||||||
const moveBlockLeft = ref(undefined) |
|
||||||
const leftBarWidth = ref(undefined) |
|
||||||
// 移动中样式 |
|
||||||
const moveBlockBackgroundColor = ref(undefined) |
|
||||||
const leftBarBorderColor = ref('#ddd') |
|
||||||
const iconColor = ref(undefined) |
|
||||||
const iconClass = ref('icon-right') |
|
||||||
const status = ref(false) // 鼠标状态 |
|
||||||
const isEnd = ref(false) // 是够验证完成 |
|
||||||
const showRefresh = ref(true) |
|
||||||
const transitionLeft = ref('') |
|
||||||
const transitionWidth = ref('') |
|
||||||
const startLeft = ref(0) |
|
||||||
|
|
||||||
const barArea = computed(() => { |
|
||||||
return proxy.$el.querySelector('.verify-bar-area') |
|
||||||
}) |
|
||||||
function init() { |
|
||||||
if (explain.value === '') |
|
||||||
text.value = t('component.captcha.slide') |
|
||||||
else |
|
||||||
text.value = explain.value |
|
||||||
|
|
||||||
getPictrue() |
|
||||||
nextTick(() => { |
|
||||||
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy) |
|
||||||
setSize.imgHeight = imgHeight |
|
||||||
setSize.imgWidth = imgWidth |
|
||||||
setSize.barHeight = barHeight |
|
||||||
setSize.barWidth = barWidth |
|
||||||
proxy.$parent.$emit('ready', proxy) |
|
||||||
}) |
|
||||||
|
|
||||||
window.removeEventListener('touchmove', (e) => { |
|
||||||
move(e) |
|
||||||
}) |
|
||||||
window.removeEventListener('mousemove', (e) => { |
|
||||||
move(e) |
|
||||||
}) |
|
||||||
|
|
||||||
// 鼠标松开 |
|
||||||
window.removeEventListener('touchend', () => { |
|
||||||
end() |
|
||||||
}) |
|
||||||
window.removeEventListener('mouseup', () => { |
|
||||||
end() |
|
||||||
}) |
|
||||||
|
|
||||||
window.addEventListener('touchmove', (e) => { |
|
||||||
move(e) |
|
||||||
}) |
|
||||||
window.addEventListener('mousemove', (e) => { |
|
||||||
move(e) |
|
||||||
}) |
|
||||||
|
|
||||||
// 鼠标松开 |
|
||||||
window.addEventListener('touchend', () => { |
|
||||||
end() |
|
||||||
}) |
|
||||||
window.addEventListener('mouseup', () => { |
|
||||||
end() |
|
||||||
}) |
|
||||||
} |
|
||||||
watch(type, () => { |
|
||||||
init() |
|
||||||
}) |
|
||||||
onMounted(() => { |
|
||||||
// 禁止拖拽 |
|
||||||
init() |
|
||||||
proxy.$el.onselectstart = function () { |
|
||||||
return false |
|
||||||
} |
|
||||||
}) |
|
||||||
// 鼠标按下 |
|
||||||
function start(e) { |
|
||||||
e = e || window.event |
|
||||||
let x |
|
||||||
if (!e.touches) { |
|
||||||
// 兼容PC端 |
|
||||||
x = e.clientX |
|
||||||
} |
|
||||||
else { |
|
||||||
// 兼容移动端 |
|
||||||
x = e.touches[0].pageX |
|
||||||
} |
|
||||||
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left) |
|
||||||
startMoveTime.value = +new Date() // 开始滑动的时间 |
|
||||||
if (isEnd.value === false) { |
|
||||||
text.value = '' |
|
||||||
moveBlockBackgroundColor.value = '#337ab7' |
|
||||||
leftBarBorderColor.value = '#337AB7' |
|
||||||
iconColor.value = '#fff' |
|
||||||
e.stopPropagation() |
|
||||||
status.value = true |
|
||||||
} |
|
||||||
} |
|
||||||
// 鼠标移动 |
|
||||||
function move(e) { |
|
||||||
e = e || window.event |
|
||||||
let x |
|
||||||
if (status.value && isEnd.value === false) { |
|
||||||
if (!e.touches) { |
|
||||||
// 兼容PC端 |
|
||||||
x = e.clientX |
|
||||||
} |
|
||||||
else { |
|
||||||
// 兼容移动端 |
|
||||||
x = e.touches[0].pageX |
|
||||||
} |
|
||||||
const bar_area_left = barArea.value.getBoundingClientRect().left |
|
||||||
let move_block_left = x - bar_area_left // 小方块相对于父元素的left值 |
|
||||||
if (move_block_left >= barArea.value.offsetWidth - Number.parseInt(Number.parseInt(blockSize.value.width) / 2) - 2) |
|
||||||
move_block_left = barArea.value.offsetWidth - Number.parseInt(Number.parseInt(blockSize.value.width) / 2) - 2 |
|
||||||
|
|
||||||
if (move_block_left <= 0) |
|
||||||
move_block_left = Number.parseInt(Number.parseInt(blockSize.value.width) / 2) |
|
||||||
|
|
||||||
// 拖动后小方块的left值 |
|
||||||
moveBlockLeft.value = `${move_block_left - startLeft.value}px` |
|
||||||
leftBarWidth.value = `${move_block_left - startLeft.value}px` |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 鼠标松开 |
|
||||||
function end() { |
|
||||||
endMovetime.value = +new Date() |
|
||||||
// 判断是否重合 |
|
||||||
if (status.value && isEnd.value === false) { |
|
||||||
let moveLeftDistance = Number.parseInt((moveBlockLeft.value || '').replace('px', '')) |
|
||||||
moveLeftDistance = (moveLeftDistance * 310) / Number.parseInt(setSize.imgWidth) |
|
||||||
const data = { |
|
||||||
captchaType: captchaType.value, |
|
||||||
pointJson: secretKey.value |
|
||||||
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value) |
|
||||||
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }), |
|
||||||
token: backToken.value, |
|
||||||
} |
|
||||||
checkCaptcha(data).then((response) => { |
|
||||||
const res = response.data |
|
||||||
if (res.repCode === '0000') { |
|
||||||
moveBlockBackgroundColor.value = '#5cb85c' |
|
||||||
leftBarBorderColor.value = '#5cb85c' |
|
||||||
iconColor.value = '#fff' |
|
||||||
iconClass.value = 'icon-check' |
|
||||||
showRefresh.value = false |
|
||||||
isEnd.value = true |
|
||||||
if (mode.value === 'pop') { |
|
||||||
setTimeout(() => { |
|
||||||
proxy.$parent.clickShow = false |
|
||||||
refresh() |
|
||||||
}, 1500) |
|
||||||
} |
|
||||||
passFlag.value = true |
|
||||||
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s |
|
||||||
${t('component.captcha.success')}` |
|
||||||
const captchaVerification = secretKey.value |
|
||||||
? aesEncrypt(`${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5.0 })}`, secretKey.value) |
|
||||||
: `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5.0 })}` |
|
||||||
setTimeout(() => { |
|
||||||
tipWords.value = '' |
|
||||||
proxy.$parent.closeBox() |
|
||||||
proxy.$parent.$emit('success', { captchaVerification }) |
|
||||||
}, 1000) |
|
||||||
} |
|
||||||
else { |
|
||||||
moveBlockBackgroundColor.value = '#d9534f' |
|
||||||
leftBarBorderColor.value = '#d9534f' |
|
||||||
iconColor.value = '#fff' |
|
||||||
iconClass.value = 'icon-close' |
|
||||||
passFlag.value = false |
|
||||||
setTimeout(() => { |
|
||||||
refresh() |
|
||||||
}, 1000) |
|
||||||
proxy.$parent.$emit('error', proxy) |
|
||||||
tipWords.value = t('component.captcha.fail') |
|
||||||
setTimeout(() => { |
|
||||||
tipWords.value = '' |
|
||||||
}, 1000) |
|
||||||
} |
|
||||||
}) |
|
||||||
status.value = false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function refresh() { |
|
||||||
showRefresh.value = true |
|
||||||
finishText.value = '' |
|
||||||
|
|
||||||
transitionLeft.value = 'left .3s' |
|
||||||
moveBlockLeft.value = 0 |
|
||||||
|
|
||||||
leftBarWidth.value = undefined |
|
||||||
transitionWidth.value = 'width .3s' |
|
||||||
|
|
||||||
leftBarBorderColor.value = '#ddd' |
|
||||||
moveBlockBackgroundColor.value = '#fff' |
|
||||||
iconColor.value = '#000' |
|
||||||
iconClass.value = 'icon-right' |
|
||||||
isEnd.value = false |
|
||||||
|
|
||||||
await getPictrue() |
|
||||||
setTimeout(() => { |
|
||||||
transitionWidth.value = '' |
|
||||||
transitionLeft.value = '' |
|
||||||
text.value = explain.value |
|
||||||
}, 300) |
|
||||||
} |
|
||||||
|
|
||||||
// 请求背景图片和验证图片 |
|
||||||
async function getPictrue() { |
|
||||||
const data = { |
|
||||||
captchaType: captchaType.value, |
|
||||||
} |
|
||||||
const res = await getCaptcha(data) |
|
||||||
if (res.data.repCode === '0000') { |
|
||||||
backImgBase.value = res.data.repData.originalImageBase64 |
|
||||||
blockBackImgBase.value = `data:image/png;base64,${res.data.repData.jigsawImageBase64}` |
|
||||||
backToken.value = res.data.repData.token |
|
||||||
secretKey.value = res.data.repData.secretKey |
|
||||||
} |
|
||||||
else { |
|
||||||
tipWords.value = res.data.repMsg |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<div style="position: relative"> |
|
||||||
<div v-if="type === '2'" class="verify-img-out" :style="{ height: `${parseInt(setSize.imgHeight) + spaces}px` }"> |
|
||||||
<div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }"> |
|
||||||
<img :src="`data:image/png;base64,${backImgBase}`" alt="" style=" display: block;width: 100%; height: 100%"> |
|
||||||
<div v-show="showRefresh" class="verify-refresh" @click="refresh"> |
|
||||||
<i class="iconfont icon-refresh" /> |
|
||||||
</div> |
|
||||||
<transition name="tips"> |
|
||||||
<span v-if="tipWords" class="verify-tips" :class="passFlag ? 'suc-bg' : 'err-bg'"> |
|
||||||
{{ tipWords }} |
|
||||||
</span> |
|
||||||
</transition> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<!-- 公共部分 --> |
|
||||||
<div class="verify-bar-area" :style="{ 'width': setSize.imgWidth, 'height': barSize.height, 'line-height': barSize.height }"> |
|
||||||
<span class="verify-msg" v-text="text" /> |
|
||||||
<div |
|
||||||
class="verify-left-bar" |
|
||||||
:style="{ |
|
||||||
'width': leftBarWidth !== undefined ? leftBarWidth : barSize.height, |
|
||||||
'height': barSize.height, |
|
||||||
'border-color': leftBarBorderColor, |
|
||||||
'transaction': transitionWidth, |
|
||||||
}" |
|
||||||
> |
|
||||||
<span class="verify-msg" v-text="finishText" /> |
|
||||||
<div |
|
||||||
class="verify-move-block" |
|
||||||
:style="{ |
|
||||||
'width': barSize.height, |
|
||||||
'height': barSize.height, |
|
||||||
'background-color': moveBlockBackgroundColor, |
|
||||||
'left': moveBlockLeft, |
|
||||||
'transition': transitionLeft, |
|
||||||
}" |
|
||||||
@touchstart="start" |
|
||||||
@mousedown="start" |
|
||||||
> |
|
||||||
<i class="iconfont verify-icon" :class="[iconClass]" :style="{ color: iconColor }" /> |
|
||||||
<div |
|
||||||
v-if="type === '2'" |
|
||||||
class="verify-sub-block" |
|
||||||
:style="{ |
|
||||||
'width': `${Math.floor((parseInt(setSize.imgWidth) * 47) / 310)}px`, |
|
||||||
'height': setSize.imgHeight, |
|
||||||
'top': `-${parseInt(setSize.imgHeight) + spaces}px`, |
|
||||||
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`, |
|
||||||
}" |
|
||||||
> |
|
||||||
<img :src="blockBackImgBase" alt="" style=" display: block;width: 100%; height: 100%; -webkit-user-drag: none"> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</template> |
|
@ -1,4 +0,0 @@ |
|||||||
import VerifySlide from './VerifySlide.vue' |
|
||||||
import VerifyPoints from './VerifyPoints.vue' |
|
||||||
|
|
||||||
export { VerifySlide, VerifyPoints } |
|
File diff suppressed because one or more lines are too long
@ -1,15 +0,0 @@ |
|||||||
import CryptoJS from 'crypto-js' |
|
||||||
|
|
||||||
/** |
|
||||||
* @word 要加密的内容 |
|
||||||
* @keyWord String 服务器随机返回的关键字 |
|
||||||
*/ |
|
||||||
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { |
|
||||||
const key = CryptoJS.enc.Utf8.parse(keyWord) |
|
||||||
const srcs = CryptoJS.enc.Utf8.parse(word) |
|
||||||
const encrypted = CryptoJS.AES.encrypt(srcs, key, { |
|
||||||
mode: CryptoJS.mode.ECB, |
|
||||||
padding: CryptoJS.pad.Pkcs7, |
|
||||||
}) |
|
||||||
return encrypted.toString() |
|
||||||
} |
|
@ -1,93 +0,0 @@ |
|||||||
export function resetSize(vm) { |
|
||||||
let img_width, img_height, bar_width, bar_height // 图片的宽度、高度,移动条的宽度、高度
|
|
||||||
const EmployeeWindow = window as any |
|
||||||
const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth |
|
||||||
const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight |
|
||||||
if (vm.imgSize.width.includes('%')) |
|
||||||
img_width = `${(Number.parseInt(vm.imgSize.width) / 100) * parentWidth}px` |
|
||||||
else |
|
||||||
img_width = vm.imgSize.width |
|
||||||
|
|
||||||
if (vm.imgSize.height.includes('%')) |
|
||||||
img_height = `${(Number.parseInt(vm.imgSize.height) / 100) * parentHeight}px` |
|
||||||
else |
|
||||||
img_height = vm.imgSize.height |
|
||||||
|
|
||||||
if (vm.barSize.width.includes('%')) |
|
||||||
bar_width = `${(Number.parseInt(vm.barSize.width) / 100) * parentWidth}px` |
|
||||||
else |
|
||||||
bar_width = vm.barSize.width |
|
||||||
|
|
||||||
if (vm.barSize.height.includes('%')) |
|
||||||
bar_height = `${(Number.parseInt(vm.barSize.height) / 100) * parentHeight}px` |
|
||||||
else |
|
||||||
bar_height = vm.barSize.height |
|
||||||
|
|
||||||
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height } |
|
||||||
} |
|
||||||
|
|
||||||
export const _code_chars = [ |
|
||||||
1, |
|
||||||
2, |
|
||||||
3, |
|
||||||
4, |
|
||||||
5, |
|
||||||
6, |
|
||||||
7, |
|
||||||
8, |
|
||||||
9, |
|
||||||
'a', |
|
||||||
'b', |
|
||||||
'c', |
|
||||||
'd', |
|
||||||
'e', |
|
||||||
'f', |
|
||||||
'g', |
|
||||||
'h', |
|
||||||
'i', |
|
||||||
'j', |
|
||||||
'k', |
|
||||||
'l', |
|
||||||
'm', |
|
||||||
'n', |
|
||||||
'o', |
|
||||||
'p', |
|
||||||
'q', |
|
||||||
'r', |
|
||||||
's', |
|
||||||
't', |
|
||||||
'u', |
|
||||||
'v', |
|
||||||
'w', |
|
||||||
'x', |
|
||||||
'y', |
|
||||||
'z', |
|
||||||
'A', |
|
||||||
'B', |
|
||||||
'C', |
|
||||||
'D', |
|
||||||
'E', |
|
||||||
'F', |
|
||||||
'G', |
|
||||||
'H', |
|
||||||
'I', |
|
||||||
'J', |
|
||||||
'K', |
|
||||||
'L', |
|
||||||
'M', |
|
||||||
'N', |
|
||||||
'O', |
|
||||||
'P', |
|
||||||
'Q', |
|
||||||
'R', |
|
||||||
'S', |
|
||||||
'T', |
|
||||||
'U', |
|
||||||
'V', |
|
||||||
'W', |
|
||||||
'X', |
|
||||||
'Y', |
|
||||||
'Z', |
|
||||||
] |
|
||||||
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'] |
|
||||||
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'] |
|
@ -1,69 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
import { List } from 'ant-design-vue' |
|
||||||
import { onMounted } from 'vue' |
|
||||||
import type { ListItem } from './data' |
|
||||||
import { CollapseContainer } from '@/components/Container/index' |
|
||||||
import { getUserProfileApi } from '@/api/base/profile' |
|
||||||
|
|
||||||
const accountBindList: ListItem[] = [ |
|
||||||
{ |
|
||||||
key: '20', |
|
||||||
title: '钉钉', |
|
||||||
description: '当前未绑定钉钉账号', |
|
||||||
extra: '绑定', |
|
||||||
avatar: 'i-ri:dingding-fill', |
|
||||||
color: '#2eabff', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '30', |
|
||||||
title: '企业微信', |
|
||||||
description: '当前未绑定企业微信', |
|
||||||
extra: '绑定', |
|
||||||
avatar: 'i-ri:wechat-line', |
|
||||||
color: '#2eabff', |
|
||||||
}, |
|
||||||
] |
|
||||||
|
|
||||||
async function init() { |
|
||||||
const userInfo = await getUserProfileApi() |
|
||||||
// TODO |
|
||||||
for (const i in accountBindList) { |
|
||||||
if (userInfo.socialUsers) { |
|
||||||
for (const j in userInfo.socialUsers) { |
|
||||||
if (accountBindList[i].key === userInfo.socialUsers[j].type) { |
|
||||||
accountBindList[i].title = '已绑定' |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
onMounted(async () => { |
|
||||||
await init() |
|
||||||
}) |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<CollapseContainer title="账号绑定" :can-expan="false"> |
|
||||||
<List> |
|
||||||
<template v-for="item in accountBindList" :key="item.key"> |
|
||||||
<List.Item> |
|
||||||
<List.Item.Meta> |
|
||||||
<template #avatar> |
|
||||||
<span v-if="item.avatar" class="text-4xl" :class="item.avatar" :style="{ color: item.color }" /> |
|
||||||
</template> |
|
||||||
<template #title> |
|
||||||
{{ item.title }} |
|
||||||
<a-button v-if="item.extra" type="link" size="small" class="float-right mr-7.5 mt-2.5 cursor-pointer"> |
|
||||||
{{ item.extra }} |
|
||||||
</a-button> |
|
||||||
</template> |
|
||||||
<template #description> |
|
||||||
<div>{{ item.description }}</div> |
|
||||||
</template> |
|
||||||
</List.Item.Meta> |
|
||||||
</List.Item> |
|
||||||
</template> |
|
||||||
</List> |
|
||||||
</CollapseContainer> |
|
||||||
</template> |
|
@ -1,76 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
import { Button, Col, Row } from 'ant-design-vue' |
|
||||||
import { computed, onMounted } from 'vue' |
|
||||||
import { baseSetschemas } from './data' |
|
||||||
import { BasicForm, useForm } from '@/components/Form/index' |
|
||||||
import { CollapseContainer } from '@/components/Container' |
|
||||||
import { CropperAvatar } from '@/components/Cropper' |
|
||||||
import { useMessage } from '@/hooks/web/useMessage' |
|
||||||
import headerImg from '@/assets/images/header.jpg' |
|
||||||
import { useUserStore } from '@/store/modules/user' |
|
||||||
import { getUserProfileApi, updateUserProfileApi, uploadAvatarApi } from '@/api/base/profile' |
|
||||||
|
|
||||||
const { createMessage } = useMessage() |
|
||||||
const userStore = useUserStore() |
|
||||||
|
|
||||||
const [register, { setFieldsValue, validate }] = useForm({ |
|
||||||
labelWidth: 120, |
|
||||||
schemas: baseSetschemas, |
|
||||||
showActionButtonGroup: false, |
|
||||||
}) |
|
||||||
|
|
||||||
onMounted(async () => { |
|
||||||
const data = await getUserProfileApi() |
|
||||||
setFieldsValue(data) |
|
||||||
}) |
|
||||||
|
|
||||||
const avatar = computed(() => { |
|
||||||
const { avatar } = userStore.getUserInfo.user |
|
||||||
return avatar || headerImg |
|
||||||
}) |
|
||||||
|
|
||||||
async function updateAvatar({ data }) { |
|
||||||
const res = await uploadAvatarApi({ avatarFile: data }) |
|
||||||
const userinfo = userStore.getUserInfo |
|
||||||
userinfo.user.avatar = res |
|
||||||
userStore.setUserInfo(userinfo) |
|
||||||
} |
|
||||||
|
|
||||||
async function handleSubmit() { |
|
||||||
try { |
|
||||||
const values = await validate() |
|
||||||
await updateUserProfileApi(values as any) |
|
||||||
} |
|
||||||
finally { |
|
||||||
createMessage.success('更新成功!') |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<CollapseContainer title="基本设置" :can-expan="false"> |
|
||||||
<Row :gutter="24"> |
|
||||||
<Col :span="14"> |
|
||||||
<BasicForm @register="register" /> |
|
||||||
</Col> |
|
||||||
<Col :span="10"> |
|
||||||
<div> |
|
||||||
<div class="mb-2"> |
|
||||||
头像 |
|
||||||
</div> |
|
||||||
<CropperAvatar |
|
||||||
:value="avatar" |
|
||||||
btn-text="更换头像" |
|
||||||
:btn-props="{ preIcon: 'i-ant-design:cloud-upload-outlined' }" |
|
||||||
width="150" |
|
||||||
class="mb-4 block rounded-full" |
|
||||||
@change="updateAvatar" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</Col> |
|
||||||
</Row> |
|
||||||
<Button type="primary" @click="handleSubmit"> |
|
||||||
更新基本信息 |
|
||||||
</Button> |
|
||||||
</CollapseContainer> |
|
||||||
</template> |
|
@ -1,49 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
import { ref } from 'vue' |
|
||||||
import { passwordSchema } from './data' |
|
||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
import { useMessage } from '@/hooks/web/useMessage' |
|
||||||
import { BasicForm, useForm } from '@/components/Form' |
|
||||||
import { BasicModal, useModalInner } from '@/components/Modal' |
|
||||||
import { updateUserPwdApi } from '@/api/base/profile' |
|
||||||
|
|
||||||
defineOptions({ name: 'PasswordModal' }) |
|
||||||
|
|
||||||
const emit = defineEmits(['success', 'register']) |
|
||||||
const { t } = useI18n() |
|
||||||
const { createMessage } = useMessage() |
|
||||||
const title = ref('修改密码') |
|
||||||
|
|
||||||
const [registerForm, { resetFields, validate }] = useForm({ |
|
||||||
labelWidth: 120, |
|
||||||
baseColProps: { span: 24 }, |
|
||||||
schemas: passwordSchema, |
|
||||||
showActionButtonGroup: false, |
|
||||||
actionColOptions: { span: 23 }, |
|
||||||
}) |
|
||||||
|
|
||||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(() => { |
|
||||||
resetFields() |
|
||||||
setModalProps({ confirmLoading: false }) |
|
||||||
}) |
|
||||||
|
|
||||||
async function handleSubmit() { |
|
||||||
try { |
|
||||||
const values = await validate() |
|
||||||
await updateUserPwdApi(values.oldPassword, values.newPassword) |
|
||||||
setModalProps({ confirmLoading: true }) |
|
||||||
closeModal() |
|
||||||
emit('success') |
|
||||||
createMessage.success(t('common.saveSuccessText')) |
|
||||||
} |
|
||||||
finally { |
|
||||||
setModalProps({ confirmLoading: false }) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<BasicModal v-bind="$attrs" :title="title" @register="registerModal" @ok="handleSubmit"> |
|
||||||
<BasicForm @register="registerForm" /> |
|
||||||
</BasicModal> |
|
||||||
</template> |
|
@ -1,47 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
import { List } from 'ant-design-vue' |
|
||||||
import { secureSettingList } from './data' |
|
||||||
import PasswordModal from './PasswordModal.vue' |
|
||||||
import { CollapseContainer } from '@/components/Container/index' |
|
||||||
import { useModal } from '@/components/Modal' |
|
||||||
import { useMessage } from '@/hooks/web/useMessage' |
|
||||||
|
|
||||||
const ListItem = List.Item |
|
||||||
const ListItemMeta = List.Item.Meta |
|
||||||
|
|
||||||
const { createMessage } = useMessage() |
|
||||||
const [registerModal, { openModal }] = useModal() |
|
||||||
|
|
||||||
function handleEdit(title: string) { |
|
||||||
if (title === '账户密码') |
|
||||||
openModal(true, {}) |
|
||||||
} |
|
||||||
function handleSuccess() { |
|
||||||
createMessage.success('更新成功!') |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<CollapseContainer title="安全设置" :can-expan="false"> |
|
||||||
<List> |
|
||||||
<template v-for="item in secureSettingList" :key="item.key"> |
|
||||||
<ListItem> |
|
||||||
<ListItemMeta> |
|
||||||
<template #title> |
|
||||||
{{ item.title }} |
|
||||||
<div v-if="item.extra" class="float-right mr-7.5 mt-2.5 cursor-pointer text-blue-500 font-normal"> |
|
||||||
<a-button type="link" @click="handleEdit(item.title)"> |
|
||||||
{{ item.extra }} |
|
||||||
</a-button> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
<template #description> |
|
||||||
<div>{{ item.description }}</div> |
|
||||||
</template> |
|
||||||
</ListItemMeta> |
|
||||||
</ListItem> |
|
||||||
</template> |
|
||||||
</List> |
|
||||||
</CollapseContainer> |
|
||||||
<PasswordModal @register="registerModal" @success="handleSuccess" /> |
|
||||||
</template> |
|
@ -1,165 +0,0 @@ |
|||||||
/* eslint-disable prefer-promise-reject-errors */ |
|
||||||
import type { FormSchema } from '@/components/Form' |
|
||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
|
|
||||||
const { t } = useI18n() |
|
||||||
|
|
||||||
export interface ListItem { |
|
||||||
key: string |
|
||||||
title: string |
|
||||||
description: string |
|
||||||
extra?: string |
|
||||||
avatar?: string |
|
||||||
color?: string |
|
||||||
} |
|
||||||
|
|
||||||
// tab的list
|
|
||||||
export const settingList = [ |
|
||||||
{ |
|
||||||
key: '1', |
|
||||||
name: '基本设置', |
|
||||||
component: 'BaseSetting', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '2', |
|
||||||
name: '安全设置', |
|
||||||
component: 'SecureSetting', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '3', |
|
||||||
name: '账号绑定', |
|
||||||
component: 'AccountBind', |
|
||||||
}, |
|
||||||
] |
|
||||||
|
|
||||||
// 基础设置 form
|
|
||||||
export const baseSetschemas: FormSchema[] = [ |
|
||||||
{ |
|
||||||
field: 'nickname', |
|
||||||
component: 'Input', |
|
||||||
label: t('profile.user.nickname'), |
|
||||||
colProps: { span: 18 }, |
|
||||||
}, |
|
||||||
{ |
|
||||||
field: 'mobile', |
|
||||||
component: 'Input', |
|
||||||
label: t('profile.user.mobile'), |
|
||||||
colProps: { span: 18 }, |
|
||||||
}, |
|
||||||
{ |
|
||||||
field: 'email', |
|
||||||
component: 'Input', |
|
||||||
label: t('profile.user.email'), |
|
||||||
colProps: { span: 18 }, |
|
||||||
}, |
|
||||||
{ |
|
||||||
field: 'sex', |
|
||||||
component: 'RadioGroup', |
|
||||||
componentProps: { |
|
||||||
options: [ |
|
||||||
{ label: '男', value: 1 }, |
|
||||||
{ label: '女', value: 2 }, |
|
||||||
], |
|
||||||
}, |
|
||||||
label: t('profile.user.sex'), |
|
||||||
colProps: { span: 18 }, |
|
||||||
}, |
|
||||||
] |
|
||||||
|
|
||||||
// 安全设置 list
|
|
||||||
export const secureSettingList: ListItem[] = [ |
|
||||||
{ |
|
||||||
key: '1', |
|
||||||
title: '账户密码', |
|
||||||
description: '当前密码强度::强', |
|
||||||
extra: '修改', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '2', |
|
||||||
title: '密保手机', |
|
||||||
description: '已绑定手机::138****8293', |
|
||||||
extra: '修改', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '3', |
|
||||||
title: '密保问题', |
|
||||||
description: '未设置密保问题,密保问题可有效保护账户安全', |
|
||||||
extra: '修改', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '4', |
|
||||||
title: '备用邮箱', |
|
||||||
description: '已绑定邮箱::ant***sign.com', |
|
||||||
extra: '修改', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '5', |
|
||||||
title: 'MFA 设备', |
|
||||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认', |
|
||||||
extra: '修改', |
|
||||||
}, |
|
||||||
] |
|
||||||
|
|
||||||
// 新消息通知 list
|
|
||||||
export const msgNotifyList: ListItem[] = [ |
|
||||||
{ |
|
||||||
key: '1', |
|
||||||
title: '账户密码', |
|
||||||
description: '其他用户的消息将以站内信的形式通知', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '2', |
|
||||||
title: '系统消息', |
|
||||||
description: '系统消息将以站内信的形式通知', |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: '3', |
|
||||||
title: '待办任务', |
|
||||||
description: '待办任务将以站内信的形式通知', |
|
||||||
}, |
|
||||||
] |
|
||||||
|
|
||||||
export const passwordSchema: FormSchema[] = [ |
|
||||||
{ |
|
||||||
field: 'oldPassword', |
|
||||||
label: '当前密码', |
|
||||||
component: 'InputPassword', |
|
||||||
required: true, |
|
||||||
}, |
|
||||||
{ |
|
||||||
field: 'newPassword', |
|
||||||
label: '新密码', |
|
||||||
component: 'StrengthMeter', |
|
||||||
componentProps: { |
|
||||||
placeholder: '新密码', |
|
||||||
}, |
|
||||||
rules: [ |
|
||||||
{ |
|
||||||
required: true, |
|
||||||
message: '请输入新密码', |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
field: 'confirmPassword', |
|
||||||
label: '确认密码', |
|
||||||
component: 'InputPassword', |
|
||||||
|
|
||||||
dynamicRules: ({ values }) => { |
|
||||||
return [ |
|
||||||
{ |
|
||||||
required: true, |
|
||||||
validator: (_, value) => { |
|
||||||
if (!value) |
|
||||||
return Promise.reject('密码不能为空') |
|
||||||
|
|
||||||
if (value !== values.newPassword) |
|
||||||
return Promise.reject('两次输入的密码不一致!') |
|
||||||
|
|
||||||
return Promise.resolve() |
|
||||||
}, |
|
||||||
}, |
|
||||||
] |
|
||||||
}, |
|
||||||
}, |
|
||||||
] |
|
@ -1,29 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
import { TabPane, Tabs } from 'ant-design-vue' |
|
||||||
import { ref } from 'vue' |
|
||||||
import { settingList } from './data' |
|
||||||
import BaseSetting from './BaseSetting.vue' |
|
||||||
import SecureSetting from './SecureSetting.vue' |
|
||||||
import AccountBind from './AccountBind.vue' |
|
||||||
import { ScrollContainer } from '@/components/Container/index' |
|
||||||
|
|
||||||
const wrapperRef = ref(null) |
|
||||||
|
|
||||||
const tabBarStyle = { width: '220px' } |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<ScrollContainer> |
|
||||||
<div ref="wrapperRef" class="m-3 rounded-1.5 bg-[var(--component-background)]"> |
|
||||||
<Tabs tab-position="left" :tab-bar-style="tabBarStyle"> |
|
||||||
<template v-for="item in settingList" :key="item.key"> |
|
||||||
<TabPane :tab="item.name"> |
|
||||||
<BaseSetting v-if="item.component === 'BaseSetting'" /> |
|
||||||
<SecureSetting v-if="item.component === 'SecureSetting'" /> |
|
||||||
<AccountBind v-if="item.component === 'AccountBind'" /> |
|
||||||
</TabPane> |
|
||||||
</template> |
|
||||||
</Tabs> |
|
||||||
</div> |
|
||||||
</ScrollContainer> |
|
||||||
</template> |
|
Loading…
Reference in new issue