refactor: 账号管理功能大体完成,后续优化与细节调整

This commit is contained in:
秋帆
2024-04-21 22:33:48 +08:00
parent 124674c530
commit 875b72b3a0
32 changed files with 1279 additions and 397 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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;
}
}
}

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>