mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-11 16:57:09 +08:00
refactor: 账号管理功能大体完成,后续优化与细节调整
This commit is contained in:
68
src/views/login/social/index.vue
Normal file
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
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
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
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
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
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
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
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
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>
|
||||
|
Reference in New Issue
Block a user