优化构架
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
getPasswordStrength,
|
||||
validateEditableAccountProfile,
|
||||
validatePasswordChange,
|
||||
} from './accountProfile.ts'
|
||||
|
||||
test('accepts a complete editable account profile', () => {
|
||||
assert.deepEqual(validateEditableAccountProfile({
|
||||
realName: '企业管理员',
|
||||
phone: '13800006601',
|
||||
email: 'admin@example.com',
|
||||
}), [])
|
||||
})
|
||||
|
||||
test('reports invalid profile fields together', () => {
|
||||
const errors = validateEditableAccountProfile({ realName: '', phone: '1'.repeat(33), email: 'bad' })
|
||||
assert.equal(errors.length, 3)
|
||||
})
|
||||
|
||||
test('validates password length, equality and confirmation', () => {
|
||||
assert.deepEqual(validatePasswordChange({
|
||||
currentPassword: 'OldPass123',
|
||||
newPassword: 'NewPass456',
|
||||
confirmPassword: 'NewPass456',
|
||||
}), [])
|
||||
assert.equal(validatePasswordChange({ currentPassword: '', newPassword: '123', confirmPassword: '456' }).length, 3)
|
||||
assert.match(validatePasswordChange({ currentPassword: 'SamePass1', newPassword: 'SamePass1', confirmPassword: 'SamePass1' }).join('|'), /不能与当前密码相同/)
|
||||
assert.match(validatePasswordChange({ currentPassword: 'OldPass1', newPassword: 'abcdefgh', confirmPassword: 'abcdefgh' }).join('|'), /同时包含字母和数字/)
|
||||
})
|
||||
|
||||
test('grades password strength without making extra complexity mandatory', () => {
|
||||
assert.equal(getPasswordStrength('short'), 'weak')
|
||||
assert.equal(getPasswordStrength('Password1'), 'medium')
|
||||
assert.equal(getPasswordStrength('Stronger@2026'), 'strong')
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
export interface EditableAccountProfile {
|
||||
realName: string
|
||||
phone: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface PasswordChangePayload {
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
export function validateEditableAccountProfile(profile: EditableAccountProfile): string[] {
|
||||
const errors: string[] = []
|
||||
const realName = profile.realName.trim()
|
||||
const phone = profile.phone.trim()
|
||||
const email = profile.email.trim()
|
||||
|
||||
if (realName.length < 1 || realName.length > 64) errors.push('姓名需为 1–64 个字符')
|
||||
if (phone.length > 32) errors.push('联系电话不能超过 32 个字符')
|
||||
if (email && !EMAIL_PATTERN.test(email)) errors.push('请输入有效的邮箱地址')
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export function validatePasswordChange(payload: PasswordChangePayload): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!payload.currentPassword) errors.push('请输入当前密码')
|
||||
if (payload.newPassword.length < 8 || payload.newPassword.length > 64) {
|
||||
errors.push('新密码需为 8–64 个字符')
|
||||
}
|
||||
if (payload.newPassword.length >= 8 && (!/[a-zA-Z]/.test(payload.newPassword) || !/\d/.test(payload.newPassword))) {
|
||||
errors.push('新密码必须同时包含字母和数字')
|
||||
}
|
||||
if (payload.newPassword && payload.newPassword === payload.currentPassword) {
|
||||
errors.push('新密码不能与当前密码相同')
|
||||
}
|
||||
if (payload.confirmPassword !== payload.newPassword) errors.push('两次输入的新密码不一致')
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export type PasswordStrength = 'weak' | 'medium' | 'strong'
|
||||
|
||||
export function getPasswordStrength(password: string): PasswordStrength {
|
||||
let categoryCount = 0
|
||||
if (/[a-zA-Z]/.test(password)) categoryCount += 1
|
||||
if (/\d/.test(password)) categoryCount += 1
|
||||
if (/[^a-zA-Z\d]/.test(password)) categoryCount += 1
|
||||
|
||||
if (password.length >= 12 && categoryCount >= 3) return 'strong'
|
||||
if (password.length >= 8 && categoryCount >= 2) return 'medium'
|
||||
return 'weak'
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const routerSource = fs.readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
|
||||
const navigationSource = fs.readFileSync(new URL('../navigation/clientNavigation.ts', import.meta.url), 'utf8')
|
||||
const viewSource = fs.readFileSync(new URL('./AccountProfileView.vue', import.meta.url), 'utf8')
|
||||
const authSource = fs.readFileSync(new URL('../stores/auth.ts', import.meta.url), 'utf8')
|
||||
const authApiSource = fs.readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('registers the account profile route and navigation entry', () => {
|
||||
assert.match(routerSource, /account\/profile'.*AccountProfileView\.vue/)
|
||||
assert.match(routerSource, /permission: 'account:profile:view'/)
|
||||
assert.match(navigationSource, /title: '我的信息'.*path: '\/account\/profile'/)
|
||||
})
|
||||
|
||||
test('implements profile view, edit, save and cancel states', () => {
|
||||
assert.match(viewSource, /基本信息/)
|
||||
assert.doesNotMatch(viewSource, /account-identity/)
|
||||
assert.doesNotMatch(viewSource, /<h2>基本信息<\/h2>|<h2>修改密码<\/h2>/)
|
||||
assert.match(viewSource, /function startEditing\(\)/)
|
||||
assert.match(viewSource, /function cancelEditing\(\)/)
|
||||
assert.match(viewSource, /async function saveProfile\(\)/)
|
||||
assert.match(viewSource, /账号标识暂不支持在客户端修改/)
|
||||
assert.match(viewSource, /企业归属由平台审核关系确定/)
|
||||
assert.match(authSource, /async function savePersonalProfile\(/)
|
||||
assert.match(viewSource, /await auth\.savePersonalProfile/)
|
||||
})
|
||||
|
||||
test('implements password validation, strength feedback and double-submit protection', () => {
|
||||
assert.match(viewSource, /async function changePassword\(\)/)
|
||||
assert.match(viewSource, /validatePasswordChange/)
|
||||
assert.match(viewSource, /callback\(new Error\('请输入新密码'\)\)/)
|
||||
assert.match(viewSource, /getPasswordStrength/)
|
||||
assert.match(viewSource, /passwordStrengthState/)
|
||||
assert.match(viewSource, /if \(passwordSaving\.value\) return/)
|
||||
assert.match(viewSource, /await changeCurrentPassword/)
|
||||
assert.match(authApiSource, /method: 'PUT', url: '\/auth\/password'/)
|
||||
assert.match(viewSource, /安全记录已更新/)
|
||||
assert.match(viewSource, /await router\.replace\('\/login'\)/)
|
||||
assert.doesNotMatch(viewSource, /DEMO_CURRENT_PASSWORD|静态演示/)
|
||||
})
|
||||
|
||||
test('provides explicit responsive account layouts', () => {
|
||||
assert.match(viewSource, /@media \(max-width: 820px\)/)
|
||||
assert.match(viewSource, /@media \(max-width: 620px\)/)
|
||||
assert.match(viewSource, /\.account-info-grid, \.account-form-grid \{ grid-template-columns: 1fr; \}/)
|
||||
assert.match(viewSource, /min-height: 44px/)
|
||||
})
|
||||
@@ -0,0 +1,321 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
CheckCircle2,
|
||||
KeyRound,
|
||||
Pencil,
|
||||
Save,
|
||||
ShieldCheck,
|
||||
UserRound,
|
||||
X,
|
||||
} from '@lucide/vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { changeCurrentPassword } from '@/api/auth'
|
||||
import {
|
||||
getPasswordStrength,
|
||||
validateEditableAccountProfile,
|
||||
validatePasswordChange,
|
||||
type PasswordChangePayload,
|
||||
} from '@/utils/accountProfile'
|
||||
|
||||
type AccountTab = 'profile' | 'password'
|
||||
|
||||
interface ProfileForm {
|
||||
username: string
|
||||
realName: string
|
||||
phone: string
|
||||
email: string
|
||||
tenantName: string
|
||||
}
|
||||
|
||||
const auth = useAuthStore()
|
||||
const router = useRouter()
|
||||
const activeTab = ref<AccountTab>('profile')
|
||||
const editing = ref(false)
|
||||
const profileSaving = ref(false)
|
||||
const passwordSaving = ref(false)
|
||||
const passwordChangedAt = ref('')
|
||||
const profileFormRef = ref<FormInstance>()
|
||||
const passwordFormRef = ref<FormInstance>()
|
||||
|
||||
const profileForm = reactive<ProfileForm>({ ...auth.profile })
|
||||
const passwordForm = reactive<PasswordChangePayload>({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const profileRules: FormRules<ProfileForm> = {
|
||||
realName: [
|
||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||
{ min: 1, max: 64, message: '姓名需为 1–64 个字符', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{ max: 32, message: '联系电话不能超过 32 个字符', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
const passwordRules: FormRules<PasswordChangePayload> = {
|
||||
currentPassword: [{
|
||||
required: true,
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value) callback(new Error('请输入当前密码'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}],
|
||||
newPassword: [{
|
||||
required: true,
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请输入新密码'))
|
||||
return
|
||||
}
|
||||
const errors = validatePasswordChange({
|
||||
currentPassword: passwordForm.currentPassword,
|
||||
newPassword: value,
|
||||
confirmPassword: value,
|
||||
})
|
||||
const relevantError = errors.find((error) => error.includes('8–64') || error.includes('同时包含') || error.includes('当前密码相同'))
|
||||
if (relevantError) callback(new Error(relevantError))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}],
|
||||
confirmPassword: [{
|
||||
required: true,
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value) callback(new Error('请再次输入新密码'))
|
||||
else if (value !== passwordForm.newPassword) callback(new Error('两次输入的新密码不一致'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}],
|
||||
}
|
||||
|
||||
const passwordStrength = computed(() => getPasswordStrength(passwordForm.newPassword))
|
||||
const passwordStrengthState = computed(() => passwordForm.newPassword ? passwordStrength.value : 'empty')
|
||||
const strengthLabel = computed(() => passwordForm.newPassword
|
||||
? ({ weak: '较弱', medium: '中等', strong: '较强' })[passwordStrength.value]
|
||||
: '未输入')
|
||||
|
||||
function syncProfileForm() {
|
||||
Object.assign(profileForm, auth.profile)
|
||||
profileFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function startEditing() {
|
||||
syncProfileForm()
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
syncProfileForm()
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
if (profileSaving.value) return
|
||||
const valid = await profileFormRef.value?.validate().catch(() => false)
|
||||
const errors = validateEditableAccountProfile(profileForm)
|
||||
if (!valid || errors.length > 0) return
|
||||
|
||||
profileSaving.value = true
|
||||
try {
|
||||
await auth.savePersonalProfile({
|
||||
realName: profileForm.realName.trim(),
|
||||
phone: profileForm.phone.trim(),
|
||||
email: profileForm.email.trim(),
|
||||
})
|
||||
syncProfileForm()
|
||||
editing.value = false
|
||||
ElMessage.success('个人信息已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '个人信息保存失败,请重试')
|
||||
} finally {
|
||||
profileSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetPasswordForm() {
|
||||
passwordForm.currentPassword = ''
|
||||
passwordForm.newPassword = ''
|
||||
passwordForm.confirmPassword = ''
|
||||
passwordFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
if (passwordSaving.value) return
|
||||
const valid = await passwordFormRef.value?.validate().catch(() => false)
|
||||
if (!valid || validatePasswordChange(passwordForm).length > 0) return
|
||||
|
||||
passwordSaving.value = true
|
||||
try {
|
||||
await changeCurrentPassword(passwordForm.currentPassword, passwordForm.newPassword)
|
||||
passwordChangedAt.value = new Date().toLocaleString('zh-CN', { hour12: false })
|
||||
resetPasswordForm()
|
||||
ElMessage.success('密码已修改,请使用新密码重新登录')
|
||||
await auth.logout().catch(() => undefined)
|
||||
await router.replace('/login')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '密码修改失败,请重试')
|
||||
} finally {
|
||||
passwordSaving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="account-page">
|
||||
<div class="client-card account-workspace">
|
||||
<el-tabs v-model="activeTab" class="account-tabs">
|
||||
<el-tab-pane name="profile">
|
||||
<template #label><span class="account-tab-label"><UserRound :size="16" />基本信息</span></template>
|
||||
|
||||
<div v-if="!editing" class="account-actions">
|
||||
<el-button type="primary" @click="startEditing"><Pencil :size="16" />编辑信息</el-button>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
v-if="editing"
|
||||
ref="profileFormRef"
|
||||
:model="profileForm"
|
||||
:rules="profileRules"
|
||||
label-position="top"
|
||||
class="account-form-grid"
|
||||
@submit.prevent="saveProfile"
|
||||
>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="profileForm.username" disabled />
|
||||
<span class="field-help">账号标识暂不支持在客户端修改</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="realName"><el-input v-model="profileForm.realName" maxlength="64" show-word-limit placeholder="请输入真实姓名" /></el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone"><el-input v-model="profileForm.phone" maxlength="32" inputmode="tel" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="邮箱" prop="email"><el-input v-model="profileForm.email" maxlength="128" inputmode="email" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="所属公司">
|
||||
<el-input v-model="profileForm.tenantName" disabled />
|
||||
<span class="field-help">企业归属由平台审核关系确定</span>
|
||||
</el-form-item>
|
||||
<div class="account-form-actions">
|
||||
<el-button :disabled="profileSaving" @click="cancelEditing"><X :size="16" />取消</el-button>
|
||||
<el-button type="primary" native-type="submit" :loading="profileSaving"><Save :size="16" />保存修改</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<dl v-else class="account-info-grid">
|
||||
<div><dt>用户名</dt><dd>{{ auth.profile.username }}</dd></div>
|
||||
<div><dt>姓名</dt><dd>{{ auth.profile.realName }}</dd></div>
|
||||
<div><dt>联系电话</dt><dd>{{ auth.profile.phone || '未填写' }}</dd></div>
|
||||
<div><dt>邮箱</dt><dd>{{ auth.profile.email || '未填写' }}</dd></div>
|
||||
<div><dt>所属公司</dt><dd>{{ auth.profile.tenantName }}</dd></div>
|
||||
</dl>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="password">
|
||||
<template #label><span class="account-tab-label"><KeyRound :size="16" />修改密码</span></template>
|
||||
|
||||
<div class="password-layout">
|
||||
<div>
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-position="top"
|
||||
class="password-form"
|
||||
@submit.prevent="changePassword"
|
||||
>
|
||||
<el-form-item label="当前密码" prop="currentPassword">
|
||||
<el-input v-model="passwordForm.currentPassword" type="password" show-password autocomplete="current-password" placeholder="请输入当前密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input v-model="passwordForm.newPassword" type="password" show-password autocomplete="new-password" maxlength="64" placeholder="请输入新密码" />
|
||||
<div class="password-strength" :data-strength="passwordStrengthState">
|
||||
<span aria-hidden="true"><i /><i /><i /></span>
|
||||
<small>密码强度:{{ strengthLabel }}</small>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||
<el-input v-model="passwordForm.confirmPassword" type="password" show-password autocomplete="new-password" maxlength="64" placeholder="请再次输入新密码" />
|
||||
</el-form-item>
|
||||
<div class="password-actions">
|
||||
<el-button :disabled="passwordSaving" @click="resetPasswordForm">清空</el-button>
|
||||
<el-button type="primary" native-type="submit" :loading="passwordSaving"><KeyRound :size="16" />确认修改</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
<div v-if="passwordChangedAt" class="password-result" role="status">
|
||||
<CheckCircle2 :size="18" />
|
||||
<div><strong>安全记录已更新</strong><p>{{ passwordChangedAt }};服务端已使旧会话失效。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="security-note">
|
||||
<ShieldCheck :size="22" />
|
||||
<div><strong>账户安全提示</strong><p>建议组合使用字母、数字和符号,不要与其他网站共用密码。</p></div>
|
||||
<p>当前密码仅用于本次服务端校验,前端不会持久化密码。修改成功后需要重新登录。</p>
|
||||
</aside>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account-page { display: flex; flex-direction: column; gap: 14px; }
|
||||
.account-workspace { min-height: 520px; padding: 0 24px 28px; }
|
||||
.account-tabs :deep(.el-tabs__header) { margin-bottom: 0; }
|
||||
.account-tabs :deep(.el-tabs__item) { min-height: 52px; }
|
||||
.account-tab-label { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.account-actions { display: flex; justify-content: flex-end; gap: 8px; margin: 20px 0; }
|
||||
.account-info-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--border); border-left: 1px solid var(--border); }
|
||||
.account-info-grid div { min-width: 0; min-height: 94px; padding: 18px 20px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||||
.account-info-grid dt { color: var(--muted); font-size: 11px; }
|
||||
.account-info-grid dd { margin: 9px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 14px; font-weight: 650; }
|
||||
.account-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 24px; max-width: 900px; }
|
||||
.account-form-grid :deep(.el-form-item) { align-content: start; margin-bottom: 21px; }
|
||||
.account-form-actions { display: flex; grid-column: 1 / -1; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
||||
.field-help { margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||
.password-layout { display: grid; grid-template-columns: minmax(0, 440px) minmax(260px, 1fr); align-items: start; max-width: 980px; gap: clamp(32px, 6vw, 72px); padding-top: 22px; }
|
||||
.password-form { max-width: 440px; }
|
||||
.password-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||
.password-form :deep(.el-form-item__error) { padding-top: 5px; line-height: 1.45; white-space: normal; }
|
||||
.password-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--border); }
|
||||
.password-result { display: flex; align-items: flex-start; gap: 9px; margin-top: 18px; padding: 14px; background: #edf6f4; color: var(--primary); }
|
||||
.password-result strong { color: var(--navy); font-size: 12px; }
|
||||
.password-result p { margin: 3px 0 0; color: #526b67; font-size: 11px; line-height: 1.6; }
|
||||
.password-strength { display: grid; width: 100%; min-height: 18px; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; margin-top: 9px; color: var(--muted); }
|
||||
.password-strength > span { display: grid; flex: 1; grid-template-columns: repeat(3, 1fr); gap: 4px; }
|
||||
.password-strength i { height: 3px; background: #dce3e6; }
|
||||
.password-strength[data-strength="weak"] i:first-child { background: #c2413b; }
|
||||
.password-strength[data-strength="medium"] i:nth-child(-n+2) { background: #c47913; }
|
||||
.password-strength[data-strength="strong"] i { background: var(--primary); }
|
||||
.password-strength small { white-space: nowrap; font-size: 10px; }
|
||||
.security-note { display: grid; grid-template-columns: auto 1fr; gap: 10px 12px; margin-top: 0; padding: 22px; background: #f2f7f6; color: var(--primary); }
|
||||
.security-note strong { color: var(--navy); font-size: 13px; }
|
||||
.security-note p { margin: 5px 0 0; color: #526b67; font-size: 11px; line-height: 1.7; }
|
||||
.security-note > p { grid-column: 1 / -1; margin-top: 8px; padding-top: 14px; border-top: 1px solid #d7e5e2; }
|
||||
.security-note code { color: var(--navy); font-weight: 700; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.password-layout { grid-template-columns: 1fr; gap: 24px; padding-top: 18px; }
|
||||
.security-note { margin-top: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.account-workspace { min-height: 0; padding: 0 16px 22px; }
|
||||
.account-actions { width: 100%; margin: 16px 0; }
|
||||
.account-actions .el-button { width: 100%; min-height: 44px; }
|
||||
.account-form-actions { width: 100%; }
|
||||
.account-form-actions .el-button { min-height: 44px; flex: 1; }
|
||||
.account-info-grid, .account-form-grid { grid-template-columns: 1fr; }
|
||||
.account-info-grid div { min-height: 82px; }
|
||||
.password-actions { width: 100%; }
|
||||
.password-actions .el-button { min-height: 44px; flex: 1; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user