mirror of
				https://github.com/continew-org/continew-admin-ui.git
				synced 2025-10-31 22:57:15 +08:00 
			
		
		
		
	feat: 系统配置新增安全设置功能
1、在系统配置中增加安全配置,支持配置密码策略 2、移除个人安全设置 3、在账号管理中增加修改密码功能 4、每次登录后检测密码是否过期并提示修改
This commit is contained in:
		| @@ -261,7 +261,6 @@ continew-admin-ui      # 前端项目 | ||||
| │  │  │  └─ online           # 在线用户 | ||||
| │  │  ├─ setting         # 设置 | ||||
| │  │  │  ├─ profile        # 账号管理 | ||||
| │  │  │  └─ security       # 安全设置 | ||||
| │  │  ├─ tool            # 系统工具 | ||||
| │  │  │  └─ generator      # 代码生成 | ||||
| │  │  └─ system          # 系统管理 | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export interface UserInfo { | ||||
|   phone: string | ||||
|   avatar: string | ||||
|   pwdResetTime: string | ||||
|   passwordExpired: boolean | ||||
|   registrationDate: string | ||||
|   deptName: string | ||||
|   roles: string[] | ||||
|   | ||||
| @@ -251,6 +251,16 @@ export interface BasicConfigResp { | ||||
|   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 { | ||||
|   source: string | ||||
|   | ||||
| @@ -53,9 +53,6 @@ | ||||
|           <a-doption @click="router.push('/setting/profile')"> | ||||
|             <span>账号管理</span> | ||||
|           </a-doption> | ||||
|           <a-doption @click="router.push('/setting/security')"> | ||||
|             <span>安全设置</span> | ||||
|           </a-doption> | ||||
|           <a-divider :margin="0" /> | ||||
|           <a-doption @click="logout"> | ||||
|             <span>退出登录</span> | ||||
| @@ -111,10 +108,12 @@ const logout = () => { | ||||
| .user { | ||||
|   cursor: pointer; | ||||
|   color: var(--color-text-1); | ||||
|  | ||||
|   .username { | ||||
|     margin-left: 10px; | ||||
|     white-space: nowrap; | ||||
|   } | ||||
|  | ||||
|   .arco-icon-down { | ||||
|     transition: all 0.3s; | ||||
|     margin-left: 2px; | ||||
|   | ||||
| @@ -62,12 +62,6 @@ export const constantRoutes: RouteRecordRaw[] = [ | ||||
|         name: 'SettingProfile', | ||||
|         component: () => import('@/views/setting/profile/index.vue'), | ||||
|         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) => { | ||||
|       const { name } = route | ||||
|       // 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) | ||||
|       } | ||||
|     }) | ||||
|   | ||||
| @@ -27,6 +27,7 @@ const storeSetup = () => { | ||||
|     phone: '', | ||||
|     avatar: '', | ||||
|     pwdResetTime: '', | ||||
|     passwordExpired: false, | ||||
|     registrationDate: '', | ||||
|     deptName: '', | ||||
|     roles: [], | ||||
|   | ||||
| @@ -31,7 +31,9 @@ | ||||
|     </a-form-item> | ||||
|     <a-form-item> | ||||
|       <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-form-item> | ||||
|   </a-form> | ||||
| @@ -39,7 +41,7 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| 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 { useStorage } from '@vueuse/core' | ||||
| import { encryptByRsa } from '@/utils/encrypt' | ||||
| @@ -92,6 +94,7 @@ const handleLogin = async () => { | ||||
|     const { rememberMe } = loginConfig.value | ||||
|     loginConfig.value.username = rememberMe ? form.username : '' | ||||
|     Message.success('欢迎使用') | ||||
|     checkPasswordExpired() | ||||
|   } catch (error) { | ||||
|     getCaptcha() | ||||
|     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 getCaptcha = () => { | ||||
| @@ -151,6 +174,7 @@ onMounted(() => { | ||||
|   background-color: rgb(var(--danger-1)); | ||||
|   border-color: rgb(var(--danger-3)); | ||||
| } | ||||
|  | ||||
| .arco-input-wrapper.arco-input-error:hover { | ||||
|   background-color: rgb(var(--danger-1)); | ||||
|   border-color: rgb(var(--danger-6)); | ||||
| @@ -160,6 +184,7 @@ onMounted(() => { | ||||
|   font-size: 13px; | ||||
|   color: var(--color-text-1); | ||||
| } | ||||
|  | ||||
| .arco-input-wrapper:hover { | ||||
|   border-color: rgb(var(--arcoblue-6)); | ||||
| } | ||||
|   | ||||
| @@ -17,17 +17,44 @@ | ||||
|         <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>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 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> | ||||
|   </a-card> | ||||
| @@ -38,7 +65,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <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 { encryptByRsa } from '@/utils/encrypt' | ||||
| import { type Columns, GiForm } from '@/components/GiForm' | ||||
| @@ -60,7 +87,7 @@ const columns: Columns = [ | ||||
|     label: '当前密码', | ||||
|     field: 'oldPassword', | ||||
|     type: 'input-password', | ||||
|     rules: [{ required: true, message: '请输入当前密码' }], | ||||
|     rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }], | ||||
|     hide: () => { | ||||
|       return userInfo.pwdResetTime | ||||
|     } | ||||
| @@ -69,13 +96,13 @@ const columns: Columns = [ | ||||
|     label: '新密码', | ||||
|     field: 'newPassword', | ||||
|     type: 'input-password', | ||||
|     rules: [{ required: true, message: '请输入新密码' }] | ||||
|     rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }] | ||||
|   }, | ||||
|   { | ||||
|     label: '确认新密码', | ||||
|     field: 'rePassword', | ||||
|     type: 'input-password', | ||||
|     rules: [{ required: true, message: '请再次输入新密码' }], | ||||
|     rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }], | ||||
|     props: { | ||||
|       placeholder: '请再次输入新密码' | ||||
|     } | ||||
| @@ -105,6 +132,14 @@ const onUpdate = async () => { | ||||
| const save = async () => { | ||||
|   const isInvalid = await formRef.value?.formRef?.validate() | ||||
|   if (isInvalid) return false | ||||
|   if (form.newPassword !== form.rePassword) { | ||||
|     Message.error('两次新密码不一致') | ||||
|     return false | ||||
|   } | ||||
|   if (form.newPassword === form.oldPassword) { | ||||
|     Message.error('新密码与旧密码不能相同') | ||||
|     return false | ||||
|   } | ||||
|   try { | ||||
|     await updateUserPassword({ | ||||
|       oldPassword: encryptByRsa(form.oldPassword) || '', | ||||
| @@ -116,6 +151,28 @@ const save = async () => { | ||||
|     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> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
| @@ -1,11 +1,16 @@ | ||||
| <template> | ||||
|   <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"> | ||||
|         <LeftBox /> | ||||
|       </a-col> | ||||
|       <a-col :xs="24" :sm="24" :md="14" :lg="14" :xl="17" :xxl="17"> | ||||
|         <div> | ||||
|           <PasswordPolicy /> | ||||
|         </div> | ||||
|         <div style="margin-top: 16px"> | ||||
|           <RightBox /> | ||||
|         </div> | ||||
|       </a-col> | ||||
|     </a-row> | ||||
|   </div> | ||||
| @@ -14,6 +19,7 @@ | ||||
| <script setup lang="ts"> | ||||
| import LeftBox from './LeftBox.vue' | ||||
| import RightBox from './RightBox.vue' | ||||
| import PasswordPolicy from './PasswordPolicy.vue' | ||||
|  | ||||
| defineOptions({ name: 'SettingProfile' }) | ||||
| </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 /> | ||||
|         </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-card> | ||||
|   </div> | ||||
| @@ -13,6 +16,7 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import BasicSetting from './components/BasicSetting.vue' | ||||
| import SecuritySetting from './components/SecuritySetting.vue' | ||||
|  | ||||
| defineOptions({ name: 'SystemConfig' }) | ||||
| </script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 kils
					kils