mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-10-30 02:57:08 +08:00
feat: 系统配置新增安全设置功能
1、在系统配置中增加安全配置,支持配置密码策略 2、移除个人安全设置 3、在账号管理中增加修改密码功能 4、每次登录后检测密码是否过期并提示修改
This commit is contained in:
@@ -261,7 +261,6 @@ continew-admin-ui # 前端项目
|
|||||||
│ │ │ └─ online # 在线用户
|
│ │ │ └─ online # 在线用户
|
||||||
│ │ ├─ setting # 设置
|
│ │ ├─ setting # 设置
|
||||||
│ │ │ ├─ profile # 账号管理
|
│ │ │ ├─ profile # 账号管理
|
||||||
│ │ │ └─ security # 安全设置
|
|
||||||
│ │ ├─ tool # 系统工具
|
│ │ ├─ tool # 系统工具
|
||||||
│ │ │ └─ generator # 代码生成
|
│ │ │ └─ generator # 代码生成
|
||||||
│ │ └─ system # 系统管理
|
│ │ └─ system # 系统管理
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface UserInfo {
|
|||||||
phone: string
|
phone: string
|
||||||
avatar: string
|
avatar: string
|
||||||
pwdResetTime: string
|
pwdResetTime: string
|
||||||
|
passwordExpired: boolean
|
||||||
registrationDate: string
|
registrationDate: string
|
||||||
deptName: string
|
deptName: string
|
||||||
roles: string[]
|
roles: string[]
|
||||||
|
|||||||
@@ -251,6 +251,16 @@ export interface BasicConfigResp {
|
|||||||
site_copyright: string
|
site_copyright: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 安全配置类型 */
|
||||||
|
export interface SecurityConfigResp {
|
||||||
|
password_contain_name: OptionResp
|
||||||
|
password_error_count: OptionResp
|
||||||
|
password_lock_minutes: OptionResp
|
||||||
|
password_min_length: OptionResp
|
||||||
|
password_special_char: OptionResp
|
||||||
|
password_update_interval: OptionResp
|
||||||
|
}
|
||||||
|
|
||||||
/** 绑定三方账号信息*/
|
/** 绑定三方账号信息*/
|
||||||
export interface BindSocialAccountRes {
|
export interface BindSocialAccountRes {
|
||||||
source: string
|
source: string
|
||||||
|
|||||||
@@ -53,9 +53,6 @@
|
|||||||
<a-doption @click="router.push('/setting/profile')">
|
<a-doption @click="router.push('/setting/profile')">
|
||||||
<span>账号管理</span>
|
<span>账号管理</span>
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-doption @click="router.push('/setting/security')">
|
|
||||||
<span>安全设置</span>
|
|
||||||
</a-doption>
|
|
||||||
<a-divider :margin="0" />
|
<a-divider :margin="0" />
|
||||||
<a-doption @click="logout">
|
<a-doption @click="logout">
|
||||||
<span>退出登录</span>
|
<span>退出登录</span>
|
||||||
@@ -111,10 +108,12 @@ const logout = () => {
|
|||||||
.user {
|
.user {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-icon-down {
|
.arco-icon-down {
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|||||||
@@ -62,12 +62,6 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
name: 'SettingProfile',
|
name: 'SettingProfile',
|
||||||
component: () => import('@/views/setting/profile/index.vue'),
|
component: () => import('@/views/setting/profile/index.vue'),
|
||||||
meta: { title: '账号管理', showInTabs: false }
|
meta: { title: '账号管理', showInTabs: false }
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/setting/security',
|
|
||||||
name: 'SettingSecurity',
|
|
||||||
component: () => import('@/views/setting/security/index.vue'),
|
|
||||||
meta: { title: '安全设置', showInTabs: false }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -88,7 +82,7 @@ export function resetRouter() {
|
|||||||
router.getRoutes().forEach((route) => {
|
router.getRoutes().forEach((route) => {
|
||||||
const { name } = route
|
const { name } = route
|
||||||
// console.log('name', name, path)
|
// console.log('name', name, path)
|
||||||
if (name && !['Home', 'Setting', 'SettingProfile', 'SettingSecurity'].includes(name.toString())) {
|
if (name && !['Home', 'Setting', 'SettingProfile'].includes(name.toString())) {
|
||||||
router.hasRoute(name) && router.removeRoute(name)
|
router.hasRoute(name) && router.removeRoute(name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const storeSetup = () => {
|
|||||||
phone: '',
|
phone: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
pwdResetTime: '',
|
pwdResetTime: '',
|
||||||
|
passwordExpired: false,
|
||||||
registrationDate: '',
|
registrationDate: '',
|
||||||
deptName: '',
|
deptName: '',
|
||||||
roles: [],
|
roles: [],
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-space direction="vertical" fill class="w-full">
|
<a-space direction="vertical" fill class="w-full">
|
||||||
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
|
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long
|
||||||
|
>立即登录
|
||||||
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getImageCaptcha } from '@/apis'
|
import { getImageCaptcha } from '@/apis'
|
||||||
import { Message, type FormInstance } from '@arco-design/web-vue'
|
import { Message, type FormInstance, Modal } from '@arco-design/web-vue'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { encryptByRsa } from '@/utils/encrypt'
|
import { encryptByRsa } from '@/utils/encrypt'
|
||||||
@@ -92,6 +94,7 @@ const handleLogin = async () => {
|
|||||||
const { rememberMe } = loginConfig.value
|
const { rememberMe } = loginConfig.value
|
||||||
loginConfig.value.username = rememberMe ? form.username : ''
|
loginConfig.value.username = rememberMe ? form.username : ''
|
||||||
Message.success('欢迎使用')
|
Message.success('欢迎使用')
|
||||||
|
checkPasswordExpired()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
getCaptcha()
|
getCaptcha()
|
||||||
form.captcha = ''
|
form.captcha = ''
|
||||||
@@ -100,6 +103,26 @@ const handleLogin = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkPasswordExpired = () => {
|
||||||
|
if (!userStore.userInfo.passwordExpired) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '密码已过期,是否去修改?',
|
||||||
|
hideCancel: false,
|
||||||
|
closable: true,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
try {
|
||||||
|
await router.push({ path: '/setting/profile' })
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const captchaImgBase64 = ref()
|
const captchaImgBase64 = ref()
|
||||||
// 获取验证码
|
// 获取验证码
|
||||||
const getCaptcha = () => {
|
const getCaptcha = () => {
|
||||||
@@ -151,6 +174,7 @@ onMounted(() => {
|
|||||||
background-color: rgb(var(--danger-1));
|
background-color: rgb(var(--danger-1));
|
||||||
border-color: rgb(var(--danger-3));
|
border-color: rgb(var(--danger-3));
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-input-wrapper.arco-input-error:hover {
|
.arco-input-wrapper.arco-input-error:hover {
|
||||||
background-color: rgb(var(--danger-1));
|
background-color: rgb(var(--danger-1));
|
||||||
border-color: rgb(var(--danger-6));
|
border-color: rgb(var(--danger-6));
|
||||||
@@ -160,6 +184,7 @@ onMounted(() => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-input-wrapper:hover {
|
.arco-input-wrapper:hover {
|
||||||
border-color: rgb(var(--arcoblue-6));
|
border-color: rgb(var(--arcoblue-6));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,44 @@
|
|||||||
<div class="sub-text">
|
<div class="sub-text">
|
||||||
密码至少包含
|
密码至少包含
|
||||||
<span class="sub-text-value">大写字母</span>
|
<span class="sub-text-value">大写字母</span>
|
||||||
<span class="sub-text-value">大写字母</span>
|
|
||||||
<span class="sub-text-value">小写字母</span>
|
<span class="sub-text-value">小写字母</span>
|
||||||
<span class="sub-text-value">数字</span>
|
<span class="sub-text-value">数字</span>
|
||||||
<span class="sub-text-value">特殊字符</span>3种
|
<span class="sub-text-value" v-if="securityConfig.password_special_char.value == 1">特殊字符</span>
|
||||||
|
</div>
|
||||||
|
<div class="sub-text" v-if="securityConfig.password_contain_name.value == 1">
|
||||||
|
密码不能包含<span class="sub-text-value">正反序用户名</span>
|
||||||
|
</div>
|
||||||
|
<div class="sub-text">
|
||||||
|
密码长度至少
|
||||||
|
<span class="sub-text-value">
|
||||||
|
{{ securityConfig.password_min_length.value }}
|
||||||
|
</span>
|
||||||
|
位
|
||||||
|
</div>
|
||||||
|
<div class="sub-text">
|
||||||
|
<div v-if="securityConfig.password_expiration_days.value == 0">未设置密码有效期</div>
|
||||||
|
<div v-else>
|
||||||
|
密码有效期
|
||||||
|
<span class="sub-text-value">
|
||||||
|
{{ securityConfig.password_expiration_days.value }}
|
||||||
|
</span>
|
||||||
|
天
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sub-text">
|
||||||
|
连续密码错误可重试
|
||||||
|
<span class="sub-text-value">
|
||||||
|
{{ securityConfig.password_error_count.value }}
|
||||||
|
</span>
|
||||||
|
次
|
||||||
|
</div>
|
||||||
|
<div class="sub-text">
|
||||||
|
超过错误密码重试次数账号将被锁定
|
||||||
|
<span class="sub-text-value">
|
||||||
|
{{ securityConfig.password_lock_minutes.value }}
|
||||||
|
</span>
|
||||||
|
分钟
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-text">限制密码长度至少为<span class="sub-text-value">6</span>位</div>
|
|
||||||
<div class="sub-text">未设置密码有效期</div>
|
|
||||||
<div class="sub-text">新密码不能与历史前<span class="sub-text-value">N</span>次密码重复</div>
|
|
||||||
<div class="sub-text">1小时内密码错误可重试<span class="sub-text-value">N</span>次</div>
|
|
||||||
<div class="sub-text">超过错误密码重试次数账号将被锁定<span class="sub-text-value">N</span>分钟</div>
|
|
||||||
<a-link class="link">修改规则(未开放)</a-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -38,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { updateUserPassword } from '@/apis'
|
import { listOption, type OptionResp, type SecurityConfigResp, updateUserPassword } from '@/apis'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { encryptByRsa } from '@/utils/encrypt'
|
import { encryptByRsa } from '@/utils/encrypt'
|
||||||
import { type Columns, GiForm } from '@/components/GiForm'
|
import { type Columns, GiForm } from '@/components/GiForm'
|
||||||
@@ -60,7 +87,7 @@ const columns: Columns = [
|
|||||||
label: '当前密码',
|
label: '当前密码',
|
||||||
field: 'oldPassword',
|
field: 'oldPassword',
|
||||||
type: 'input-password',
|
type: 'input-password',
|
||||||
rules: [{ required: true, message: '请输入当前密码' }],
|
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }],
|
||||||
hide: () => {
|
hide: () => {
|
||||||
return userInfo.pwdResetTime
|
return userInfo.pwdResetTime
|
||||||
}
|
}
|
||||||
@@ -69,13 +96,13 @@ const columns: Columns = [
|
|||||||
label: '新密码',
|
label: '新密码',
|
||||||
field: 'newPassword',
|
field: 'newPassword',
|
||||||
type: 'input-password',
|
type: 'input-password',
|
||||||
rules: [{ required: true, message: '请输入新密码' }]
|
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '确认新密码',
|
label: '确认新密码',
|
||||||
field: 'rePassword',
|
field: 'rePassword',
|
||||||
type: 'input-password',
|
type: 'input-password',
|
||||||
rules: [{ required: true, message: '请再次输入新密码' }],
|
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }],
|
||||||
props: {
|
props: {
|
||||||
placeholder: '请再次输入新密码'
|
placeholder: '请再次输入新密码'
|
||||||
}
|
}
|
||||||
@@ -105,6 +132,14 @@ const onUpdate = async () => {
|
|||||||
const save = async () => {
|
const save = async () => {
|
||||||
const isInvalid = await formRef.value?.formRef?.validate()
|
const isInvalid = await formRef.value?.formRef?.validate()
|
||||||
if (isInvalid) return false
|
if (isInvalid) return false
|
||||||
|
if (form.newPassword !== form.rePassword) {
|
||||||
|
Message.error('两次新密码不一致')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (form.newPassword === form.oldPassword) {
|
||||||
|
Message.error('新密码与旧密码不能相同')
|
||||||
|
return false
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await updateUserPassword({
|
await updateUserPassword({
|
||||||
oldPassword: encryptByRsa(form.oldPassword) || '',
|
oldPassword: encryptByRsa(form.oldPassword) || '',
|
||||||
@@ -116,6 +151,28 @@ const save = async () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const securityConfig = ref<SecurityConfigResp>({
|
||||||
|
password_contain_name: {},
|
||||||
|
password_error_count: {},
|
||||||
|
password_expiration_days: {},
|
||||||
|
password_lock_minutes: {},
|
||||||
|
password_min_length: {},
|
||||||
|
password_special_char: {},
|
||||||
|
password_update_interval: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询列表数据
|
||||||
|
const getDataList = async () => {
|
||||||
|
const { data } = await listOption({ code: Object.keys(securityConfig.value) })
|
||||||
|
securityConfig.value = data.reduce((obj: SecurityConfigResp, option: OptionResp) => {
|
||||||
|
obj[option.code] = { ...option, value: parseInt(option.value) }
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getDataList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="gi_page">
|
<div class="gi_page">
|
||||||
<a-row wrap :gutter="16">
|
<a-row wrap :gutter="16" align="stretch">
|
||||||
<a-col :xs="24" :sm="24" :md="10" :lg="10" :xl="7" :xxl="7">
|
<a-col :xs="24" :sm="24" :md="10" :lg="10" :xl="7" :xxl="7">
|
||||||
<LeftBox />
|
<LeftBox />
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :md="14" :lg="14" :xl="17" :xxl="17">
|
<a-col :xs="24" :sm="24" :md="14" :lg="14" :xl="17" :xxl="17">
|
||||||
<RightBox />
|
<div>
|
||||||
|
<PasswordPolicy />
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px">
|
||||||
|
<RightBox />
|
||||||
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,6 +19,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LeftBox from './LeftBox.vue'
|
import LeftBox from './LeftBox.vue'
|
||||||
import RightBox from './RightBox.vue'
|
import RightBox from './RightBox.vue'
|
||||||
|
import PasswordPolicy from './PasswordPolicy.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'SettingProfile' })
|
defineOptions({ name: 'SettingProfile' })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-card title="账号保护" bordered class="gradient-card">
|
|
||||||
<div v-for="item in modeList" :key="item.title">
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
|
|
||||||
<div class="info">
|
|
||||||
<div class="info-top">
|
|
||||||
<span class="label">{{ item.title }}</span>
|
|
||||||
<span class="bind">
|
|
||||||
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
|
|
||||||
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
|
|
||||||
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{
|
|
||||||
item.status ? '已开启' : '未开启'
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-desc">
|
|
||||||
{{ item.subtitle }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="btn-wrapper">
|
|
||||||
<a-switch disabled title="未开放" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { ModeItem } from '../type'
|
|
||||||
|
|
||||||
const modeList = ref<ModeItem[]>([])
|
|
||||||
modeList.value = [
|
|
||||||
{
|
|
||||||
title: '登录保护',
|
|
||||||
icon: 'protect',
|
|
||||||
subtitle: '开启登录保护后,账号登录需进行二次身份验证',
|
|
||||||
status: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作保护',
|
|
||||||
icon: 'protect',
|
|
||||||
subtitle: '进行敏感操作时需进行二次身份校验',
|
|
||||||
status: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-card title="基本设置" bordered class="gradient-card">
|
|
||||||
<div v-for="item in modeList" :key="item.title">
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
|
|
||||||
<div class="info">
|
|
||||||
<div class="info-top">
|
|
||||||
<span class="label">{{ item.title }}</span>
|
|
||||||
<span class="bind">
|
|
||||||
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
|
|
||||||
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
|
|
||||||
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{
|
|
||||||
item.status ? '已绑定' : '未绑定'
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-desc">
|
|
||||||
<span class="value">{{ item.value }}</span>
|
|
||||||
{{ item.subtitle }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="btn-wrapper">
|
|
||||||
<a-button class="btn" :type="item.status ? 'secondary' : 'primary'" @click="onUpdate(item.type)">
|
|
||||||
{{ item.status ? '修改' : '绑定' }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
<VerifyModel ref="verifyModelRef" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { ModeItem } from '../type'
|
|
||||||
import VerifyModel from '../components/VerifyModel.vue'
|
|
||||||
import { useUserStore } from '@/stores'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const userInfo = computed(() => userStore.userInfo)
|
|
||||||
|
|
||||||
const modeList = ref<ModeItem[]>([])
|
|
||||||
modeList.value = [
|
|
||||||
{
|
|
||||||
title: '安全手机',
|
|
||||||
icon: 'phone-color',
|
|
||||||
value: `${userInfo.value.phone + ' ' || '手机号'}`,
|
|
||||||
subtitle: `可用于身份验证、密码找回、通知接收`,
|
|
||||||
type: 'phone',
|
|
||||||
status: !!userInfo.value.phone
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '安全邮箱',
|
|
||||||
icon: 'email-color',
|
|
||||||
value: `${userInfo.value.email + ' ' || '邮箱'}`,
|
|
||||||
subtitle: `可用于身份验证、密码找回、通知接收`,
|
|
||||||
type: 'email',
|
|
||||||
status: !!userInfo.value.email
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
|
||||||
// 修改
|
|
||||||
const onUpdate = (type: string) => {
|
|
||||||
verifyModelRef.value?.open(type)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mode-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
.mode-item-content {
|
|
||||||
display: flex;
|
|
||||||
.icon {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-card title="登录会话设置" bordered class="gradient-card">
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon-wrapper"><GiSvgIcon name="message-color" :size="26" /></div>
|
|
||||||
<div class="info">
|
|
||||||
<div class="info-top">
|
|
||||||
<span class="label">登录态保持时间设置</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-desc">保持登录状态的限制</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail">
|
|
||||||
<div class="sub-text-wrapper">
|
|
||||||
<div class="sub-text">无操作登录会话保持<span class="sub-text-value">30</span>分钟,超时登录会话将失效</div>
|
|
||||||
<div class="sub-text">登录会话最大保持<span class="sub-text-value">0</span>天,超时登录会话将失效</div>
|
|
||||||
<a-link class="link">修改规则(未开放)</a-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup></script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="gi_page">
|
|
||||||
<a-row wrap :gutter="16">
|
|
||||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
|
||||||
<BasicsSetting />
|
|
||||||
</a-col>
|
|
||||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
|
||||||
<SessionSetting />
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-row wrap :gutter="16" style="margin-top: 16px">
|
|
||||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
|
||||||
<PasswordPolicy />
|
|
||||||
</a-col>
|
|
||||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
|
||||||
<AccountProtection />
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import BasicsSetting from './BasicsSetting.vue'
|
|
||||||
import SessionSetting from './SessionSetting.vue'
|
|
||||||
import PasswordPolicy from './PasswordPolicy.vue'
|
|
||||||
import AccountProtection from './AccountProtection.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'SettingSecurity' })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.gi_page {
|
|
||||||
background-color: var(--color-bg-1);
|
|
||||||
.flex_box {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
.flex_item_container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
& .flex_item_container:first-child {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
167
src/views/system/config/components/SecuritySetting.vue
Normal file
167
src/views/system/config/components/SecuritySetting.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<a-form style="margin-top: 20px" ref="formRef" :model="form" size="small" label-align="left" :disabled="!isUpdate">
|
||||||
|
<a-list size="small" :bordered="false">
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item
|
||||||
|
:help="form.password_expiration_days.description"
|
||||||
|
:label="form.password_expiration_days.name"
|
||||||
|
field="password_expiration_days"
|
||||||
|
>
|
||||||
|
<a-input-number class="input-width" :min="0" :max="999" v-model="form.password_expiration_days.value">
|
||||||
|
<template #append>天</template>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item :help="form.password_min_length.description" :label="form.password_min_length.name">
|
||||||
|
<a-input-number class="input-width" :min="8" :max="32" v-model="form.password_min_length.value" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item :help="form.password_update_interval.description" :label="form.password_update_interval.name">
|
||||||
|
<a-input-number class="input-width" :min="0" :max="9999" v-model="form.password_update_interval.value">
|
||||||
|
<template #append>分钟</template>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item :help="form.password_error_count.description" :label="form.password_error_count.name">
|
||||||
|
<a-input-number class="input-width" :min="0" :max="9999" v-model="form.password_error_count.value" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item :help="form.password_lock_minutes.description" :label="form.password_lock_minutes.name">
|
||||||
|
<a-input-number class="input-width" :min="0" :max="9999" v-model="form.password_lock_minutes.value">
|
||||||
|
<template #append>分钟</template>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item :help="form.password_special_char.description" :label="form.password_special_char.name">
|
||||||
|
<a-switch type="round" :checked-value="1" :unchecked-value="0" v-model="form.password_special_char.value" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="border: none">
|
||||||
|
<a-form-item :help="form.password_contain_name.description" :label="form.password_contain_name.name">
|
||||||
|
<a-switch type="round" :checked-value="1" :unchecked-value="0" v-model="form.password_contain_name.value" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item style="padding-top: 13px; border: none">
|
||||||
|
<a-space>
|
||||||
|
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
|
||||||
|
<template #icon>
|
||||||
|
<icon-undo />
|
||||||
|
</template>
|
||||||
|
恢复默认
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
|
||||||
|
<template #icon>
|
||||||
|
<icon-edit />
|
||||||
|
</template>
|
||||||
|
修改
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="isUpdate" type="primary" @click="handleSave">
|
||||||
|
<template #icon>
|
||||||
|
<icon-save />
|
||||||
|
</template>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="isUpdate" @click="reset">
|
||||||
|
<template #icon>
|
||||||
|
<icon-refresh />
|
||||||
|
</template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="isUpdate" @click="handleCancel">
|
||||||
|
<template #icon>
|
||||||
|
<icon-undo />
|
||||||
|
</template>
|
||||||
|
取消
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { listOption, updateOption, resetOptionValue, type SecurityConfigResp, type OptionResp } from '@/apis'
|
||||||
|
import { Message, Modal, type FormInstance } from '@arco-design/web-vue'
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const form = ref<SecurityConfigResp>({
|
||||||
|
password_contain_name: {},
|
||||||
|
password_error_count: {},
|
||||||
|
password_expiration_days: {},
|
||||||
|
password_lock_minutes: {},
|
||||||
|
password_min_length: {},
|
||||||
|
password_special_char: {},
|
||||||
|
password_update_interval: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const reset = () => {
|
||||||
|
getDataList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUpdate = ref(false)
|
||||||
|
// 修改
|
||||||
|
const onUpdate = () => {
|
||||||
|
isUpdate.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
reset()
|
||||||
|
isUpdate.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryForm = {
|
||||||
|
code: Object.keys(form.value)
|
||||||
|
}
|
||||||
|
// 查询列表数据
|
||||||
|
const getDataList = async () => {
|
||||||
|
const { data } = await listOption(queryForm)
|
||||||
|
form.value = data.reduce((obj: SecurityConfigResp, option: OptionResp) => {
|
||||||
|
obj[option.code] = { ...option, value: parseInt(option.value) }
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = async () => {
|
||||||
|
await updateOption(
|
||||||
|
Object.entries(form.value).map(([key, value]) => {
|
||||||
|
return { code: key, value: value.value }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
handleCancel()
|
||||||
|
Message.success('保存成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复默认
|
||||||
|
const handleResetValue = async () => {
|
||||||
|
await resetOptionValue(queryForm)
|
||||||
|
Message.success('恢复成功')
|
||||||
|
await getDataList()
|
||||||
|
}
|
||||||
|
const onResetValue = () => {
|
||||||
|
Modal.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: '确认恢复基础配置为默认值吗?',
|
||||||
|
hideCancel: false,
|
||||||
|
maskClosable: false,
|
||||||
|
onOk: handleResetValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getDataList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.input-width {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
<BasicSetting />
|
<BasicSetting />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="2" title="邮件配置(暂未开放)" disabled></a-tab-pane>
|
<a-tab-pane key="2" title="邮件配置(暂未开放)" disabled></a-tab-pane>
|
||||||
|
<a-tab-pane key="3" title="安全设置">
|
||||||
|
<SecuritySetting />
|
||||||
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,6 +16,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BasicSetting from './components/BasicSetting.vue'
|
import BasicSetting from './components/BasicSetting.vue'
|
||||||
|
import SecuritySetting from './components/SecuritySetting.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'SystemConfig' })
|
defineOptions({ name: 'SystemConfig' })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user