refactor: 账号管理功能大体完成,后续优化与细节调整
@@ -6,9 +6,9 @@ import createAutoImport from './auto-import'
|
||||
import createComponents from './components'
|
||||
import createSvgIcon from './svg-icon'
|
||||
import createMock from './mock'
|
||||
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
export default function createVitePlugins(viteEnv, isBuild = false) {
|
||||
const vitePlugins: (PluginOption | PluginOption[])[] = [vue(), vueJsx()]
|
||||
const vitePlugins: (PluginOption | PluginOption[])[] = [vue(), vueJsx(), VueDevTools()]
|
||||
vitePlugins.push(createAutoImport())
|
||||
vitePlugins.push(createComponents())
|
||||
vitePlugins.push(createSvgIcon(isBuild))
|
||||
|
@@ -41,6 +41,7 @@
|
||||
"query-string": "^9.0.0",
|
||||
"v-viewer": "^3.0.10",
|
||||
"viewerjs": "^1.11.6",
|
||||
"vite-plugin-vue-devtools": "^7.0.27",
|
||||
"vue": "^3.4.21",
|
||||
"vue-codemirror6": "^1.1.27",
|
||||
"vue-color-kit": "^1.0.5",
|
||||
|
483
pnpm-lock.yaml
generated
@@ -54,5 +54,5 @@ export interface LoginResp {
|
||||
|
||||
// 第三方登录授权类型
|
||||
export interface SocialAuthAuthorizeResp {
|
||||
authorizeUrl: string;
|
||||
authorizeUrl: string
|
||||
}
|
@@ -7,3 +7,11 @@ const BASE_URL = '/captcha'
|
||||
export function getImageCaptcha() {
|
||||
return http.get<Common.ImageCaptchaResp>(`${BASE_URL}/img`)
|
||||
}
|
||||
/**@desc 获取手机验证码 */
|
||||
export function getPhoneCaptcha(query: { phone: string }) {
|
||||
return http.get<boolean>(`${BASE_URL}/sms`, query)
|
||||
}
|
||||
/**@desc 获取邮箱验证码 */
|
||||
export function getEmailCaptcha(query: { email: string }) {
|
||||
return http.get<boolean>(`${BASE_URL}/mail`, query)
|
||||
}
|
||||
|
@@ -222,3 +222,8 @@ export interface BasicConfigRecordResp {
|
||||
site_logo?: string
|
||||
site_favicon?: string
|
||||
}
|
||||
/** 绑定三方账号信息*/
|
||||
export interface BindSocialAccountRes {
|
||||
source: string
|
||||
description: string
|
||||
}
|
||||
|
@@ -37,3 +37,25 @@ export function exportUser(query: System.UserQuery) {
|
||||
export function resetUserPwd(data: any, id: string) {
|
||||
return http.patch(`${BASE_URL}/${id}/password`, data)
|
||||
}
|
||||
|
||||
/** @desc 修改用户基础信息 */
|
||||
export function updateUserBaseInfo(data: { nickname?: string; gender?: number }) {
|
||||
return http.patch(`${BASE_URL}/basic/info`, data)
|
||||
}
|
||||
|
||||
/** @desc 修改邮箱 */
|
||||
export function updateUserEmail(data: { newEmail: string; captcha: string; currentPassword: string }) {
|
||||
return http.patch(`${BASE_URL}/email`, data)
|
||||
}
|
||||
/**@desc 绑定三方账号 */
|
||||
export function bindSocialAccount(source: string, data: any) {
|
||||
return http.post(`${BASE_URL}/social/${source}`, data)
|
||||
}
|
||||
/**@desc 获取绑定的三方账号 */
|
||||
export function getSocialAccount() {
|
||||
return http.get<System.BindSocialAccountRes[]>(`${BASE_URL}/social`)
|
||||
}
|
||||
/**@desc 解绑三方账号 */
|
||||
export function unbindSocialAccount(source: string) {
|
||||
return http.del(`${BASE_URL}/social/${source}`)
|
||||
}
|
||||
|
6
src/assets/icons/Mail.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="24" fill="#F6F7FB"/>
|
||||
<rect x="12.334" y="14.667" width="23.3333" height="18.6667" fill="#4785FF"/>
|
||||
<path d="M12.334 14.667H35.6673V18.167C29.0348 24.1362 18.9665 24.1362 12.334 18.167V14.667Z" fill="#94C2FF"/>
|
||||
<circle cx="23.9993" cy="23.4163" r="2.33333" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 407 B |
6
src/assets/icons/MailUnbind.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="24" fill="#F6F7FB"/>
|
||||
<rect x="12.334" y="14.667" width="23.3333" height="18.6667" fill="#86909C"/>
|
||||
<path d="M12.334 14.667H35.6673V18.167C29.0348 24.1362 18.9665 24.1362 12.334 18.167V14.667Z" fill="#C3C7CE"/>
|
||||
<circle cx="23.9993" cy="23.4163" r="2.33333" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 407 B |
7
src/assets/icons/Tel.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="24" fill="#F6F7FB"/>
|
||||
<path d="M15.25 11.75H32.75V36.25H15.25V11.75Z" fill="#4785FF"/>
|
||||
<path d="M15.25 28.0835H32.75V36.2502H15.25V28.0835Z" fill="#94C2FF"/>
|
||||
<circle cx="24.0013" cy="32.1668" r="2.33333" fill="white"/>
|
||||
<path d="M20.5 14.0835H27.5V15.8335H20.5V14.0835Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 420 B |
7
src/assets/icons/TelUnbind.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="24" fill="#F6F7FB"/>
|
||||
<path d="M15.25 11.75H32.75V36.25H15.25V11.75Z" fill="#86909C"/>
|
||||
<path d="M15.25 28.0835H32.75V36.2502H15.25V28.0835Z" fill="white" fill-opacity="0.5"/>
|
||||
<circle cx="24.0013" cy="32.1668" r="2.33333" fill="white"/>
|
||||
<path d="M20.5 14.0835H27.5V15.8335H20.5V14.0835Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 437 B |
5
src/assets/icons/id.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 3C1 1.89543 1.89543 1 3 1H11C12.1046 1 13 1.89543 13 3V11C13 12.1046 12.1046 13 11 13H3C1.89543 13 1 12.1046 1 11V3Z" fill="#86909C"/>
|
||||
<path d="M5.01758 4V10H3.74902V4H5.01758Z" fill="white"/>
|
||||
<path d="M8.18823 10H6.06128V4H8.20581C8.80933 4 9.32886 4.12012 9.7644 4.36035C10.2 4.59863 10.5349 4.94141 10.7693 5.38867C11.0056 5.83594 11.1238 6.37109 11.1238 6.99414C11.1238 7.61914 11.0056 8.15625 10.7693 8.60547C10.5349 9.05469 10.198 9.39941 9.75854 9.63965C9.32104 9.87988 8.79761 10 8.18823 10ZM7.32983 8.91309H8.1355C8.5105 8.91309 8.82593 8.84668 9.08179 8.71387C9.3396 8.5791 9.53296 8.37109 9.66186 8.08984C9.79272 7.80664 9.85815 7.44141 9.85815 6.99414C9.85815 6.55078 9.79272 6.18848 9.66186 5.90723C9.53296 5.62598 9.34058 5.41895 9.08472 5.28613C8.82886 5.15332 8.51343 5.08691 8.13843 5.08691H7.32983V8.91309Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 956 B |
4
src/assets/icons/loginProtect.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 10L24 4L41 10V28.1366C41 32.389 38.7495 36.3238 35.0842 38.4799L30.0842 41.421C26.3289 43.6301 21.6711 43.6301 17.9158 41.421L12.9158 38.4799C9.25049 36.3238 7 32.389 7 28.1366V10Z" fill="#4785FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.139 17.9763L22.0848 33.2062L13.9395 25.0608L16.0608 22.9395L21.9154 28.7941L32.8612 16.0239L35.139 17.9763Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 491 B |
6
src/assets/icons/loginStatus.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M45 38V21H21V38H36L41 44V38H45Z" fill="#94C2FF"/>
|
||||
<path d="M4 32V6H40V32H17L10 40V32H4Z" fill="#4785FF"/>
|
||||
<rect x="10" y="12" width="24" height="3" fill="white"/>
|
||||
<rect x="10" y="19" width="14" height="3" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 332 B |
7
src/assets/icons/mfa.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 6H38L44 15H4L10 6Z" fill="#94C2FF"/>
|
||||
<rect x="4" y="15" width="40" height="25" fill="#4785FF"/>
|
||||
<path d="M14.7539 20H17.3496V28.6367H15.668V22.7949C15.668 22.627 15.6699 22.3926 15.6738 22.0918C15.6777 21.7871 15.6797 21.5527 15.6797 21.3887L14.0449 28.6367H12.293L10.6699 21.3887C10.6699 21.5527 10.6719 21.7871 10.6758 22.0918C10.6797 22.3926 10.6816 22.627 10.6816 22.7949V28.6367H9V20H11.625L13.1953 26.791L14.7539 20Z" fill="white"/>
|
||||
<path d="M18.5454 20.0117H24.6684V21.5293H20.3384V23.5156H24.1294V25.0156H20.3384V28.6367H18.5454V20.0117Z" fill="white"/>
|
||||
<path d="M27.1064 25.373H29.2978L28.2197 21.9746L27.1064 25.373ZM27.2177 20H29.2568L32.3154 28.6367H30.3584L29.8017 26.8613H26.6201L26.0224 28.6367H24.1357L27.2177 20Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 862 B |
6
src/assets/icons/password.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 27V14C32 9.58172 28.4183 6 24 6C19.5817 6 16 9.58172 16 14V27C16 31.4183 19.5817 35 24 35C28.4183 35 32 31.4183 32 27ZM24 3C17.9249 3 13 7.92487 13 14V27C13 33.0751 17.9249 38 24 38C30.0751 38 35 33.0751 35 27V14C35 7.92487 30.0751 3 24 3Z" fill="#94C2FF"/>
|
||||
<path d="M6 13H42V41H6V13Z" fill="#4785FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 27C25.3807 27 26.5 25.8807 26.5 24.5C26.5 23.1193 25.3807 22 24 22C22.6193 22 21.5 23.1193 21.5 24.5C21.5 25.8807 22.6193 27 24 27ZM24 30C27.0376 30 29.5 27.5376 29.5 24.5C29.5 21.4624 27.0376 19 24 19C20.9624 19 18.5 21.4624 18.5 24.5C18.5 27.5376 20.9624 30 24 30Z" fill="white"/>
|
||||
<path d="M22.5 27H25.5V35H22.5V27Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 846 B |
5
src/assets/icons/success.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00033 1.16666C10.222 1.16666 12.8337 3.77833 12.8337 6.99999C12.8337 10.2217 10.222 12.8333 7.00033 12.8333C3.77867 12.8333 1.16699 10.2217 1.16699 6.99999C1.16699 3.77833 3.77867 1.16666 7.00033 1.16666Z" fill="#00B42A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.14096 5.01194L9.47611 5.29316C9.66128 5.44838 9.68542 5.72433 9.53011 5.90943C9.53008 5.90946 9.53006 5.90949 9.52994 5.90944L6.60396 9.39457C6.44853 9.57951 6.17264 9.60361 5.9875 9.44842L5.31721 8.88598L8.52451 5.06578C8.67993 4.88084 8.95583 4.85674 9.14096 5.01194Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.19314 7.32433L4.48668 6.99832C4.64581 6.82159 4.91696 6.80433 5.09721 6.95947L7.15505 8.73065L6.59516 9.40433C6.44073 9.59016 6.16489 9.6156 5.97906 9.46117C5.97677 9.45926 5.9745 9.45733 5.97225 9.45538L4.23181 7.94776C4.04918 7.78956 4.02938 7.51326 4.18758 7.33063C4.18941 7.32851 4.19126 7.32641 4.19314 7.32433Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
5
src/assets/icons/warning.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.01" x="0.333333" y="0.333333" width="13.3333" height="13.3333" stroke="#F0F0F0" stroke-width="0.666667"/>
|
||||
<path d="M6.99995 13.4159C3.45612 13.4159 0.583282 10.543 0.583282 6.99919C0.583282 3.45536 3.45612 0.58252 6.99995 0.58252C10.5438 0.58252 13.4166 3.45536 13.4166 6.99919C13.4166 10.543 10.5438 13.4159 6.99995 13.4159Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.99995 13.4159C3.45612 13.4159 0.583282 10.543 0.583282 6.99919C0.583282 3.45536 3.45612 0.58252 6.99995 0.58252C10.5438 0.58252 13.4166 3.45536 13.4166 6.99919C13.4166 10.543 10.5438 13.4159 6.99995 13.4159ZM6.1833 10.1492C6.1833 9.69815 6.54893 9.33252 6.99996 9.33252C7.45099 9.33252 7.81663 9.69815 7.81663 10.1492C7.81663 10.6002 7.45099 10.9659 6.99996 10.9659C6.54893 10.9659 6.1833 10.6002 6.1833 10.1492ZM6.21963 3.68084C6.1833 3.75214 6.1833 3.84548 6.1833 4.03217V7.92384C6.1833 8.11052 6.1833 8.20386 6.21963 8.27517C6.25158 8.33789 6.30258 8.38888 6.3653 8.42084C6.4366 8.45717 6.52994 8.45717 6.71663 8.45717H7.2833C7.46998 8.45717 7.56332 8.45717 7.63463 8.42084C7.69735 8.38888 7.74834 8.33789 7.7803 8.27517C7.81663 8.20386 7.81663 8.11052 7.81663 7.92384V4.03217C7.81663 3.84548 7.81663 3.75214 7.7803 3.68084C7.74834 3.61812 7.69735 3.56712 7.63463 3.53517C7.56332 3.49884 7.46998 3.49884 7.2833 3.49884H6.71663C6.52994 3.49884 6.4366 3.49884 6.3653 3.53517C6.30258 3.56712 6.25158 3.61812 6.21963 3.68084Z" fill="#FF8B07"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@@ -46,6 +46,12 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/social/callback',
|
||||
name: 'SocialCallback',
|
||||
component: () => import('@/views/login/social/index.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'Setting',
|
||||
|
@@ -2,15 +2,18 @@ import { defineStore } from 'pinia'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { resetRouter } from '@/router'
|
||||
import { accountLogin as accountLoginApi, logout as logoutApi, getUserInfo as getUserInfoApi } from '@/apis'
|
||||
import type { UserInfo } from '@/apis'
|
||||
import { socialAuth, type UserInfo } from '@/apis'
|
||||
import { setToken, clearToken, getToken } from '@/utils/auth'
|
||||
import { resetHasRouteFlag } from '@/router/permission'
|
||||
import getAvatar from '@/utils/avatar'
|
||||
|
||||
const storeSetup = () => {
|
||||
const userInfo = reactive<Pick<UserInfo, 'nickname' | 'avatar'>>({
|
||||
const userInfo = reactive<Pick<UserInfo, 'nickname' | 'avatar' | 'email' | 'phone' | 'registrationDate'>>({
|
||||
nickname: '',
|
||||
avatar: ''
|
||||
avatar: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
registrationDate: ''
|
||||
})
|
||||
const name = computed(() => userInfo.nickname)
|
||||
const avatar = computed(() => userInfo.avatar)
|
||||
@@ -32,7 +35,16 @@ const storeSetup = () => {
|
||||
setToken(res.data.token)
|
||||
token.value = res.data.token
|
||||
}
|
||||
|
||||
// 三方账号身份登录
|
||||
const socialLogin = async (source: string, req: any) => {
|
||||
try {
|
||||
const res = await socialAuth(source, req)
|
||||
setToken(res.data.token)
|
||||
} catch (err) {
|
||||
clearToken()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
// 退出登录
|
||||
const logout = async () => {
|
||||
try {
|
||||
@@ -57,6 +69,9 @@ const storeSetup = () => {
|
||||
const res = await getUserInfoApi()
|
||||
userInfo.nickname = res.data.nickname
|
||||
userInfo.avatar = getAvatar(res.data.avatar, res.data.gender)
|
||||
userInfo.email = res.data.email
|
||||
userInfo.phone = res.data.phone
|
||||
userInfo.registrationDate = res.data.registrationDate
|
||||
if (res.data.roles && res.data.roles.length) {
|
||||
roles.value = res.data.roles
|
||||
permissions.value = res.data.permissions
|
||||
@@ -71,6 +86,7 @@ const storeSetup = () => {
|
||||
roles,
|
||||
permissions,
|
||||
accountLogin,
|
||||
socialLogin,
|
||||
logout,
|
||||
logoutCallBack,
|
||||
getInfo,
|
||||
|
68
src/views/login/social/index.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" :tip="isLogin() ? '绑定中。。。' : '登录中。。。'">
|
||||
<div></div>
|
||||
</a-spin>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isLogin } from '@/utils/auth'
|
||||
import { bindSocialAccount } from '@/apis'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getCurrentInstance, ref } from 'vue'
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const source = route.query.source as string
|
||||
|
||||
/**
|
||||
* 绑定第三方账号
|
||||
*/
|
||||
const handleBindSocial = () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query
|
||||
bindSocialAccount(source, othersQuery)
|
||||
.then((res) => {
|
||||
router.push({
|
||||
path: '/setting/profile',
|
||||
query: {
|
||||
tab: 'security-setting'
|
||||
}
|
||||
})
|
||||
proxy.$message.success(res.msg)
|
||||
})
|
||||
.catch(() => {
|
||||
router.push({
|
||||
path: '/setting/profile',
|
||||
query: {
|
||||
tab: 'security-setting'
|
||||
}
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
if (isLogin()) {
|
||||
handleBindSocial()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SocialCallback'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
div {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 45%;
|
||||
margin-left: -50px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
</style>
|
43
src/views/setting/components/Card.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card_header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="card_body">
|
||||
<slot name="body"> </slot>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-1);
|
||||
border: 1px solid var(--color-neutral-2);
|
||||
.card_header {
|
||||
padding: 18px 20px;
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
left bottom,
|
||||
from(rgba(232, 244, 255, 0.5)),
|
||||
to(hsla(0, 0%, 100%, 0))
|
||||
);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
background: linear-gradient(180deg, rgba(232, 244, 255, 0.5), hsla(0, 0%, 100%, 0));
|
||||
}
|
||||
.card_body {
|
||||
flex: 1;
|
||||
padding: 15px 28px;
|
||||
}
|
||||
.card_footer {
|
||||
padding: 15px 28px;
|
||||
border-top: 1px solid var(--color-neutral-2);
|
||||
}
|
||||
}
|
||||
</style>
|
107
src/views/setting/components/VerifyModel.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" :title="title" @before-ok="save" @cancel="handleCancel">
|
||||
<a-form :model="form" ref="formRef">
|
||||
<a-form-item
|
||||
field="newPhone"
|
||||
label="新手机号"
|
||||
:rules="[{ required: true, match: Regexp.Phone, message: '请输入正确的手机号' }]"
|
||||
v-if="verifyType === 'phone'"
|
||||
>
|
||||
<a-input v-model="form.newPhone" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="email"
|
||||
label="邮箱"
|
||||
v-if="verifyType === 'email'"
|
||||
:rules="[{ required: true, match: Regexp.Email, message: '请输入正确的邮箱' }]"
|
||||
>
|
||||
<a-input v-model="form.email" />
|
||||
</a-form-item>
|
||||
<a-form-item field="verifyCode" label="验证码" :rules="[{ required: true, message: '请输入正确的验证码' }]">
|
||||
<a-input v-model="form.captcha" />
|
||||
<a-button type="outline" @click="onSendCaptcha">发送验证码</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="currentPassword"
|
||||
label="当前密码"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入当前密码' },
|
||||
{ match: Regexp.Password, message: '请输入格式的密码' }
|
||||
]"
|
||||
>
|
||||
<a-input v-model="form.currentPassword" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { getPhoneCaptcha, getEmailCaptcha, updateUserEmail } from '@/apis'
|
||||
import { encryptByRsa } from '@/utils/encrypt'
|
||||
import * as Regexp from '@/utils/regexp'
|
||||
import { Message, type Modal } from '@arco-design/web-vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
const userStore = useUserStore()
|
||||
const visible = ref<boolean>(false)
|
||||
const form = reactive({
|
||||
newPhone: '',
|
||||
captcha: '',
|
||||
currentPassword: '',
|
||||
email: ''
|
||||
})
|
||||
const formRef = ref()
|
||||
const verifyType = ref()
|
||||
const title = computed(() => {
|
||||
return verifyType.value === 'phone' ? '修改手机号' : '修改邮箱'
|
||||
})
|
||||
const onSendCaptcha = () => {
|
||||
formRef.value.validateField(verifyType.value === 'phone' ? 'newPhone' : 'email', (validate) => {
|
||||
if (!validate) {
|
||||
// 发送验证码
|
||||
if (verifyType.value === 'phone') {
|
||||
//手机号
|
||||
getPhoneCaptcha({ phone: form.newPhone }).then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
} else if (verifyType.value === 'email') {
|
||||
//邮箱
|
||||
getEmailCaptcha({ email: form.email }).then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const save: InstanceType<typeof Modal>['onBeforeOk'] = async () => {
|
||||
const flag = await formRef.value?.validate()
|
||||
if (flag) return false
|
||||
try {
|
||||
const res = await updateUserEmail({
|
||||
newEmail: form.email,
|
||||
captcha: form.captcha,
|
||||
currentPassword: encryptByRsa(form.currentPassword) as string
|
||||
})
|
||||
if (res.code === 200) {
|
||||
Message.success('修改成功')
|
||||
visible.value = false
|
||||
// 修改成功后,重新获取用户信息
|
||||
userStore.getInfo()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
// return await saveApi()
|
||||
}
|
||||
const handleCancel = () => {
|
||||
formRef.value?.resetFields()
|
||||
visible.value = false
|
||||
}
|
||||
const open = (type: string) => {
|
||||
verifyType.value = type
|
||||
visible.value = true
|
||||
}
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
54
src/views/setting/profile/AvatarModel.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<a-model v-model:visible="visible">
|
||||
<div class="cropper">
|
||||
// 裁剪左侧内容
|
||||
<div class="cropper_left">
|
||||
<vueCropper
|
||||
:tyle="{ width: '400px' }"
|
||||
ref="cropperRef"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:info-true="options.infoTrue"
|
||||
:auto-crop="options.autoCrop"
|
||||
:fixed-box="options.fixedBox"
|
||||
:can-move="options.canMoveBox"
|
||||
:can-scale="options.canScale"
|
||||
:fixed-number="fixedNumber"
|
||||
:fixed="options.fixed"
|
||||
:full="options.full"
|
||||
:center-box="options.centerBox"
|
||||
@real-time="previewHandle"
|
||||
/>
|
||||
<div class="reupload_box">
|
||||
<div class="reupload_text" @click="uploadFile('reload')">重新上传</div>
|
||||
<div>
|
||||
<el-icon class="rotate_right" @click="changeScale(1)">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
<el-icon class="rotate_right" @click="changeScale(-1)">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
<el-icon class="rotate_right" @click="rotateRight">
|
||||
<RefreshRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cropper_right">
|
||||
<div class="preview_text">预览</div>
|
||||
<div :style="getStyle" class="previewImg">
|
||||
<div :style="previewFileStyle">
|
||||
<img :style="previews.img" :src="previews.url" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-model>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import VueCropper from 'vue-cropper'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const visible = ref(true)
|
||||
</script>
|
113
src/views/setting/profile/LeftBox.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<Card style="height: 600px">
|
||||
<template #header> 主账号信息 </template>
|
||||
<template #body>
|
||||
<div class="body">
|
||||
<section>
|
||||
<div class="avatar">
|
||||
<img src="https://q1.itc.cn/q_70/images03/20240320/fcf023d835c54f78bac6c7efc98fbb4c.jpeg" />
|
||||
</div>
|
||||
<div class="name">
|
||||
<span style="margin-right: 10px">{{ userInfo.nickname }}</span>
|
||||
<icon-edit :size="16" class="btn" @click="onEditNickName" />
|
||||
</div>
|
||||
<div class="id">
|
||||
<GiSvgIcon name="id" :size="16" />
|
||||
<span style="margin-left: 10px">88888888</span>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<div class="footer_item">
|
||||
<div>
|
||||
<span style="margin-right: 10px">安全手机</span>
|
||||
<span>{{ userInfo.phone }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
><GiSvgIcon :name="userInfo.email ? 'success' : 'warning'" :size="14" /><span
|
||||
style="margin-left: 5px; font-size: 12px"
|
||||
>{{ userInfo.phone ? '已绑定' : '未绑定' }}</span
|
||||
></span
|
||||
>
|
||||
<a-divider direction="vertical" />
|
||||
<icon-edit :size="16" class="btn" @click="openVerifyModel('phone')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer_item">
|
||||
<div>
|
||||
<span style="margin-right: 10px">安全邮箱</span>
|
||||
<span>{{ userInfo.email }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
><GiSvgIcon :name="userInfo.email ? 'success' : 'warning'" :size="14" /><span
|
||||
style="margin-left: 5px; font-size: 12px"
|
||||
>{{ userInfo.email ? '已绑定' : '未绑定' }}</span
|
||||
></span
|
||||
>
|
||||
<a-divider direction="vertical" />
|
||||
<icon-edit :size="16" class="btn" @click="openVerifyModel('email')" />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="footer">注册于 {{ userInfo.registrationDate }}</div>
|
||||
</template>
|
||||
</Card>
|
||||
<VerifyModel ref="verifyModelRef" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Card from '../components/Card.vue'
|
||||
import { updateUserBaseInfo } from '@/apis'
|
||||
import VerifyModel from '../components/VerifyModel.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
||||
const onEditNickName = () => {
|
||||
userStore.editNickNameVisible = true
|
||||
}
|
||||
const openVerifyModel = (type: 'phone' | 'email') => {
|
||||
verifyModelRef.value?.open(type)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
& > section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.avatar > img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.name {
|
||||
font-size: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
& > footer .footer_item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
padding: 15px 28px;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--color-neutral-2);
|
||||
}
|
||||
</style>
|
@@ -1,161 +1,137 @@
|
||||
<template>
|
||||
<div class="right-box">
|
||||
<section class="right-box__header">
|
||||
<a-avatar :size="60" :trigger-icon-style="{ color: '#3491FA' }">
|
||||
<img :src="userStore.avatar" />
|
||||
<template #trigger-icon>
|
||||
<IconCamera />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<section class="username">{{ userStore.name }}</section>
|
||||
<ul class="list">
|
||||
<li><icon-user /><span>前端开发工程师</span></li>
|
||||
<li><icon-safe /><span>前端</span></li>
|
||||
<li><icon-location /><span>广州</span></li>
|
||||
</ul>
|
||||
<a-button type="primary" class="edit-btn"
|
||||
><template #icon> <icon-edit /> </template>编辑信息</a-button
|
||||
>
|
||||
</section>
|
||||
|
||||
<a-tabs hide-content default-active-key="2">
|
||||
<a-tab-pane key="1">
|
||||
<template #title>文章</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2">
|
||||
<template #title>项目</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3">
|
||||
<template #title>应用(3)</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<section class="right-box__comment">
|
||||
<a-comment
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
:author="item.name"
|
||||
datetime="1个小时之前"
|
||||
align="right"
|
||||
class="comment-item"
|
||||
>
|
||||
<template #actions>
|
||||
<a-space :size="20">
|
||||
<span class="action" key="heart">
|
||||
<span><IconHeart /></span>
|
||||
<span>83</span>
|
||||
</span>
|
||||
<span class="action" key="star">
|
||||
<span><IconStar /></span>
|
||||
<span>3</span>
|
||||
</span>
|
||||
<span class="action" key="reply"> <IconMessage /><span>回复</span></span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<a-avatar>
|
||||
<img alt="avatar" :src="item.avatar" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</template>
|
||||
</a-comment>
|
||||
</section>
|
||||
</div>
|
||||
<Card>
|
||||
<template #header> 登录方式 </template>
|
||||
<template #body>
|
||||
<div class="mode-list">
|
||||
<div v-for="item in modeList" :key="item.title" class="mode-item">
|
||||
<div class="mode-item-box">
|
||||
<div class="mode-item-icon">
|
||||
<GiSvgIcon :name="item.icon" :size="50" />
|
||||
</div>
|
||||
<div class="mode-item-content">
|
||||
<div class="mode-item-title">
|
||||
<div>{{ item.title }}</div>
|
||||
<div style="margin-left: 10px">
|
||||
<GiSvgIcon :name="item.status ? 'success' : 'warning'" :size="14" /><span
|
||||
style="margin-left: 5px; font-size: 12px"
|
||||
>{{ item.status ? '已绑定' : '未绑定' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-item-subtitle">{{ item.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-item-btn">
|
||||
<a-button @click="openVerifyModel(item.type, item.status)" v-if="item.jumpMode == 'modal'">{{
|
||||
item.status ? '修改' : '绑定'
|
||||
}}</a-button>
|
||||
<a-button @click="onBinding(item.type, item.status)" v-else-if="item.jumpMode == 'link'">{{
|
||||
item.status ? '解绑' : '绑定'
|
||||
}}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<VerifyModel ref="verifyModelRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores'
|
||||
import Card from '../components/Card.vue'
|
||||
import VerifyModel from '../components/VerifyModel.vue'
|
||||
import { socialAuth, getSocialAccount, unbindSocialAccount } from '@/apis'
|
||||
interface ModeItem {
|
||||
title: string
|
||||
icon: string
|
||||
subtitle: string
|
||||
type: 'phone' | 'email' | 'gitee' | 'github'
|
||||
jumpMode: 'link' | 'modal'
|
||||
status: boolean
|
||||
}
|
||||
const userStore = useUserStore()
|
||||
|
||||
const list = [
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
||||
const openVerifyModel = (type: 'phone' | 'email') => {
|
||||
verifyModelRef.value?.open(type)
|
||||
}
|
||||
const socialList = ref<any>([])
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
modeList.value = [
|
||||
{
|
||||
avatar:
|
||||
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: 'Lin',
|
||||
text: '生活会让你苦上一阵子,等你适应以后,再让你苦上一辈子'
|
||||
title: '绑定手机号',
|
||||
icon: 'Tel',
|
||||
subtitle: `${userInfo.value.phone || '绑定后'},可通过手机验证码快捷登录`,
|
||||
type: 'phone',
|
||||
jumpMode: 'modal',
|
||||
status: userInfo.value.phone ? true : false
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: 'Lin',
|
||||
text: '我从一无所有,到资产过亿,从家徒四壁,到豪车别墅,这些不是靠的别人,完全是靠我自己,一点一滴,想出来的'
|
||||
title: '绑定邮箱',
|
||||
icon: 'Mail',
|
||||
subtitle: `${userInfo.value.email || '绑定后'},可通过邮箱验证码进行登录`,
|
||||
type: 'email',
|
||||
jumpMode: 'modal',
|
||||
status: userInfo.value.email ? true : false
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: 'Lin',
|
||||
text: '有很多事情你当时想不通,别着急,过一段时间你再想,就想不起来了'
|
||||
title: '绑定Gitee',
|
||||
icon: 'gitee',
|
||||
subtitle: '绑定后,可通过Gitee进行登录',
|
||||
jumpMode: 'link',
|
||||
type: 'gitee',
|
||||
status: socialList.value.some((el) => el == 'gitee')
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: 'Lin',
|
||||
text: '⽐你优秀的⼈都⽐你努⼒,你努力还有什么用'
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: '窃·格瓦拉',
|
||||
text: '打工这辈子是不可能打工的,做生意又不会做,就是偷这种东西,才可以维持生活这样子'
|
||||
title: '绑定GitHub',
|
||||
icon: 'github',
|
||||
subtitle: '绑定后,可通过github进行登录',
|
||||
type: 'github',
|
||||
jumpMode: 'link',
|
||||
status: socialList.value.some((el) => el == 'github')
|
||||
}
|
||||
]
|
||||
const initData = () => {
|
||||
getSocialAccount().then((res) => {
|
||||
socialList.value = res.data.map((el) => el.source)
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
initData()
|
||||
})
|
||||
const onBinding = (type: string, status: boolean) => {
|
||||
if (!status) {
|
||||
socialAuth(type).then((res) => {
|
||||
window.open(res.data.authorizeUrl, '_self')
|
||||
})
|
||||
} else {
|
||||
unbindSocialAccount(type).then((res) => {
|
||||
if (res.code == 200) {
|
||||
userStore.getInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-btn {
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
background: transparent;
|
||||
&:hover {
|
||||
background: rgb(var(--primary-5));
|
||||
border-color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
flex: 1;
|
||||
background-color: var(--color-bg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
&__header {
|
||||
min-height: 204px;
|
||||
height: fit-content;
|
||||
.mode-list {
|
||||
.mode-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-white);
|
||||
background-color: rgb(var(--primary-6));
|
||||
.username {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.list {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
.mode-item-box {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
> li {
|
||||
margin-right: 15px;
|
||||
span {
|
||||
margin-left: 2px;
|
||||
}
|
||||
align-items: center;
|
||||
.mode-item-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__comment {
|
||||
flex: 1;
|
||||
padding: 20px 30px;
|
||||
padding-left: 16px;
|
||||
overflow: auto;
|
||||
.comment-item {
|
||||
margin-bottom: 15px;
|
||||
.text {
|
||||
color: $color-text-2;
|
||||
.mode-item-content > div {
|
||||
line-height: 26px;
|
||||
}
|
||||
.mode-item-content .mode-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,66 +1,12 @@
|
||||
<template>
|
||||
<div class="user">
|
||||
<div class="user__info">
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :md="24" :lg="10" :xl="8" :xxl="7">
|
||||
<section class="user-card">
|
||||
<div class="user-card__header">
|
||||
<a-avatar :size="60" :trigger-icon-style="{ color: '#3491FA' }">
|
||||
<img :src="userStore.avatar" />
|
||||
<template #trigger-icon>
|
||||
<IconCamera />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<div class="name">{{ userStore.name }}</div>
|
||||
<p class="desc">尘缘已定,不念过往</p>
|
||||
</div>
|
||||
|
||||
<ul class="user-card__list">
|
||||
<li class="list-item">
|
||||
<span class="icon"><icon-bookmark :stroke-width="1" :size="16" /></span>
|
||||
<span>前端工程师</span>
|
||||
</li>
|
||||
<li class="list-item">
|
||||
<span class="icon"><icon-branch :stroke-width="1" :size="16" /></span
|
||||
><span>中台-数据平台团队-前端创新团队-前端架构和平台工具团队</span>
|
||||
</li>
|
||||
<li class="list-item">
|
||||
<span class="icon"><icon-location :stroke-width="1" :size="16" /></span><span>广州市</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a-row justify="space-around" class="user-card__images">
|
||||
<img src="https://file.iviewui.com/admin-pro-dist/img/icon-social-weibo.cbf658a0.svg" />
|
||||
<img src="https://file.iviewui.com/admin-pro-dist/img/icon-social-zhihu.1dc5a4ff.svg" />
|
||||
<img src="https://file.iviewui.com/admin-pro-dist/img/icon-social-facebook.e95df60e.svg" />
|
||||
<img src="https://file.iviewui.com/admin-pro-dist/img/icon-social-twitter.5db80e81.svg" />
|
||||
</a-row>
|
||||
|
||||
<a-divider style="border-bottom-style: dashed" />
|
||||
|
||||
<a-typography-title :heading="6">标签</a-typography-title>
|
||||
<a-space wrap :size="5">
|
||||
<a-tag>vue3</a-tag>
|
||||
<a-tag>pinia</a-tag>
|
||||
<a-tag>vite</a-tag>
|
||||
<a-tag>ts</a-tag>
|
||||
<a-tag>arco design</a-tag>
|
||||
</a-space>
|
||||
|
||||
<a-descriptions :column="1" style="margin-top: 20px">
|
||||
<a-descriptions-item label="星座">双鱼座</a-descriptions-item>
|
||||
<a-descriptions-item label="生日">07月16日</a-descriptions-item>
|
||||
<a-descriptions-item label="爱好">
|
||||
<a-space wrap :size="5">
|
||||
<a-tag color="purple">王者荣耀</a-tag>
|
||||
<a-tag color="magenta">旅行</a-tag>
|
||||
</a-space>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</section>
|
||||
<a-row justify="space-between">
|
||||
<a-col :span="7" style="padding-right: 20px">
|
||||
<LeftBox />
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :md="24" :lg="14" :xl="16" :xxl="17">
|
||||
<RightBox></RightBox>
|
||||
<a-col :span="17">
|
||||
<RightBox />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
@@ -71,7 +17,7 @@
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import RightBox from './RightBox.vue'
|
||||
|
||||
import LeftBox from './LeftBox.vue'
|
||||
defineOptions({ name: 'Profile' })
|
||||
|
||||
const route = useRoute()
|
||||
@@ -80,7 +26,7 @@ const userStore = useUserStore()
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user {
|
||||
background-color: var(--color-bg-1);
|
||||
// background-color: var(--color-bg-1);
|
||||
&__alert {
|
||||
padding: $padding;
|
||||
padding-bottom: 0;
|
||||
@@ -88,51 +34,7 @@ const userStore = useUserStore()
|
||||
&__info {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: $padding;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-bg-1);
|
||||
border-radius: 2px;
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.name {
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
line-height: 1.5;
|
||||
margin: 8px;
|
||||
color: $color-text-1;
|
||||
}
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: $color-text-3;
|
||||
}
|
||||
}
|
||||
&__list {
|
||||
margin-top: 20px;
|
||||
.list-item {
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__images {
|
||||
margin: 10px 0;
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
// display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
99
src/views/setting/security/AccountProtection.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<Card>
|
||||
<template #header> 账号保护 </template>
|
||||
<template #body>
|
||||
<div class="mode-item" v-for="item in modeList" :key="item.title">
|
||||
<div class="mode-item-content">
|
||||
<div class="icon"><GiSvgIcon :name="item.icon" :size="36" /></div>
|
||||
<div>
|
||||
<div style="font-size: 14px; font-weight: 500; line-height: 28px; display: flex; align-items: center">
|
||||
<span>{{ item.title }}</span>
|
||||
<div style="margin-left: 10px">
|
||||
<GiSvgIcon :name="item.status ? 'success' : 'warning'" :size="14" /><span
|
||||
style="margin-left: 5px; font-size: 12px"
|
||||
>{{ item.status ? '已开启' : '未开启' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 12px">{{ item.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a-button disabled>未开放</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="content_title">
|
||||
<div class="icon"><GiSvgIcon name="loginProtect" :size="36" /></div>
|
||||
<div>
|
||||
<div style="font-size: 14px; font-weight: 500; line-height: 28px">操作保护</div>
|
||||
<div style="font-size: 12px">进行敏感操作时需进行二次身份校验</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_Box">
|
||||
<p>可使用<span class="subTitle">手机号</span>进行二次身份验证</p>
|
||||
<p>未设置密码有效期</p>
|
||||
<p>敏感操作二次身份验证后<span class="subTitle">10</span>分钟内不需要再次进行验证</p>
|
||||
<p class="link_btn">修改规则(未开发)</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Card from '../components/Card.vue'
|
||||
interface ModeItem {
|
||||
title: string
|
||||
icon: string
|
||||
subtitle: string
|
||||
status: boolean
|
||||
}
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
modeList.value = [
|
||||
{ title: '登录保护', icon: 'loginProtect', subtitle: '开启登录保护后,账号登录需进行二次身份验证', status: false }
|
||||
]
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.mode-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
.mode-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.content_Box {
|
||||
border-left: 1px solid #ccc;
|
||||
margin-left: 40px;
|
||||
padding-left: 10px;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
& > p {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
.link_btn {
|
||||
cursor: pointer;
|
||||
color: #007aff;
|
||||
&:hover {
|
||||
color: rgba($color: #007aff, $alpha: 0.8);
|
||||
}
|
||||
}
|
||||
.subTitle {
|
||||
background: var(--color-neutral-2);
|
||||
padding: 1px 5px;
|
||||
margin: 0px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
62
src/views/setting/security/BasicsSetting.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<Card style="height: 100%">
|
||||
<template #header>基本设置</template>
|
||||
<template #body>
|
||||
<div class="mode-item" v-for="item in modeList" :key="item.title">
|
||||
<div class="mode-item-content">
|
||||
<div class="icon"><GiSvgIcon :name="item.icon" :size="36" /></div>
|
||||
<div>
|
||||
<div style="font-size: 14px; font-weight: 500; line-height: 28px; display: flex; align-items: center">
|
||||
<span>{{ item.title }}</span>
|
||||
<div style="margin-left: 10px">
|
||||
<GiSvgIcon :name="item.status ? 'success' : 'warning'" :size="14" /><span
|
||||
style="margin-left: 5px; font-size: 12px"
|
||||
>{{ item.status ? '已开启' : '未开启' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 12px">{{ item.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a-button @click="openVerifyModel(item.type)">修改</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<VerifyModel ref="verifyModelRef" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Card from '../components/Card.vue'
|
||||
import VerifyModel from '../components/VerifyModel.vue'
|
||||
interface ModeItem {
|
||||
title: string
|
||||
icon: string
|
||||
subtitle: string
|
||||
type: 'phone' | 'email'
|
||||
status: boolean
|
||||
}
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
modeList.value = [
|
||||
{ title: '绑定手机号', icon: 'Tel', subtitle: '+86******88888可通过手机验证码快捷登录', type: 'phone', status: true },
|
||||
{ title: '绑定邮箱', icon: 'Mail', subtitle: '邮箱可用于身份验证、密码找回、通知接收', type: 'email', status: true }
|
||||
]
|
||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
||||
const openVerifyModel = (type: 'phone' | 'email') => {
|
||||
verifyModelRef.value?.open(type)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.mode-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
.mode-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
61
src/views/setting/security/PasswordPolicy.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<Card>
|
||||
<template #header> 密码策略 </template>
|
||||
<template #body>
|
||||
<div class="content_title">
|
||||
<div class="icon"><GiSvgIcon name="password" :size="36" /></div>
|
||||
<div>
|
||||
<div style="font-size: 14px; font-weight: 500; line-height: 28px">登录密码</div>
|
||||
<div style="font-size: 12px">为了您的账号安全,建议定期修改密码</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_Box">
|
||||
<p>
|
||||
密码至少包含 <span class="subTitle">大写字母</span><span class="subTitle">小写字母</span
|
||||
><span class="subTitle">数字</span><span class="subTitle">特殊字符</span>3种
|
||||
</p>
|
||||
<p>限制密码长度至少为<span class="subTitle">8</span>位</p>
|
||||
<p>未设置密码有效期</p>
|
||||
<p>新密码不能与历史前<span class="subTitle">3</span>次密码重复</p>
|
||||
<p>1小时内密码错误可重试 <span class="subTitle">5</span>次</p>
|
||||
<p>超过错误密码重试次数账号将被锁定<span class="subTitle">60</span>分钟</p>
|
||||
<p class="link_btn">修改规则(未开发)</p>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Card from '../components/Card.vue'
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.content_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.content_Box {
|
||||
border-left: 1px solid #ccc;
|
||||
margin-left: 40px;
|
||||
padding-left: 10px;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
& > p {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
.link_btn {
|
||||
cursor: pointer;
|
||||
color: #007aff;
|
||||
&:hover {
|
||||
color: rgba($color: #007aff, $alpha: 0.8);
|
||||
}
|
||||
}
|
||||
.subTitle {
|
||||
background: var(--color-neutral-2);
|
||||
padding: 1px 5px;
|
||||
margin: 0px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
48
src/views/setting/security/SessionSetting.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<Card style="height: 100%">
|
||||
<template #header> 登录会话设置 </template>
|
||||
<template #body>
|
||||
<div class="content_title">
|
||||
<div class="icon"><GiSvgIcon name="loginStatus" :size="36" /></div>
|
||||
<div>
|
||||
<div style="font-size: 14px; font-weight: 500; line-height: 28px">登录态保持时间设置</div>
|
||||
<div style="font-size: 12px">保持登录状态的限制</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_Box">
|
||||
<p>操作登录会话保持120分钟,超时登录会话将失效</p>
|
||||
<p>登录会话最大保持0天,超时登录会话将失效</p>
|
||||
<p class="link_btn">修改规则(未开发)</p>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Card from '../components/Card.vue'
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.content_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.content_Box {
|
||||
border-left: 1px solid #ccc;
|
||||
margin-left: 40px;
|
||||
padding-left: 10px;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
& > p {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
.link_btn {
|
||||
cursor: pointer;
|
||||
color: #007aff;
|
||||
&:hover {
|
||||
color: rgba($color: #007aff, $alpha: 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,10 +1,30 @@
|
||||
<template>
|
||||
<div class="page"></div>
|
||||
<div class="page">
|
||||
<div class="flex_box">
|
||||
<div class="flex_item_container">
|
||||
<BasicsSetting />
|
||||
</div>
|
||||
<div class="flex_item_container">
|
||||
<SessionSetting />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex_box">
|
||||
<div class="flex_item_container">
|
||||
<PasswordPolicy />
|
||||
</div>
|
||||
<div class="flex_item_container">
|
||||
<AccountProtection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
import BasicsSetting from './BasicsSetting.vue'
|
||||
import SessionSetting from './SessionSetting.vue'
|
||||
import PasswordPolicy from './PasswordPolicy.vue'
|
||||
import AccountProtection from './AccountProtection.vue'
|
||||
defineOptions({ name: 'Security' })
|
||||
|
||||
const route = useRoute()
|
||||
@@ -15,5 +35,17 @@ const form = reactive({ name: '' })
|
||||
.page {
|
||||
padding: $padding;
|
||||
background-color: var(--color-bg-1);
|
||||
.flex_box {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
.flex_item_container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
& .flex_item_container:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|