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