做一个vue3 v-model 双向绑定的弹窗
子组件
diolag.vue
<template><el-dialogv-model="dialogVisible"title="设置登录密码"width="400px"height="400px"align-center:close-on-click-modal="true":close-on-press-escape="true":show-close="true"style="border-radius: 8px"><template #header="{ close, titleId, titleClass }"><div class="my-header"><div class="title">设置登录密码</div></div></template><div class="password-setup-dialog"><div class="description">为了您的账户安全,请设置登录密码</div><el-form ref="formRef" :model="formData" :rules="rules" label-position="top" size="large" class="password-form"><el-form-item prop="password"><el-input v-model="formData.password" :type="showPassword ? 'text' : 'password'" placeholder="请输入新密码" clearable><template #prefix><div class="iconfont icon-mima" style="font-size: 14px"></div></template><template #suffix><el-icon class="password-icon" @click="showPassword = !showPassword"><View v-if="showPassword" /><Hide v-else /></el-icon></template></el-input></el-form-item><el-form-item prop="confirmPassword"><el-input v-model="formData.confirmPassword" :type="showConfirmPassword ? 'text' : 'password'" placeholder="请再次输入密码" clearable><template #prefix><div class="iconfont icon-mima" style="font-size: 14px"></div></template><template #suffix><el-icon class="password-icon" @click="showConfirmPassword = !showConfirmPassword"><View v-if="showConfirmPassword" /><Hide v-else /></el-icon></template></el-input></el-form-item><!-- <div class="password-requirements"><div class="requirement-title">密码要求:</div><ul><li :class="{ 'met': passwordLength }">长度至少8位</li><li :class="{ 'met': hasUpperCase }">包含大写字母</li><li :class="{ 'met': hasLowerCase }">包含小写字母</li><li :class="{ 'met': hasNumber }">包含数字</li><li :class="{ 'met': hasSpecialChar }">包含特殊字符</li></ul></div> --></el-form><div class="dialog-footer"><el-button type="primary" size="large" class="confirm-btn" :loading="loading" @click="handleConfirm"> 确认设置 </el-button></div></div></el-dialog>
</template><script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { View, Hide } from '@element-plus/icons-vue';
import { useLoginApi } from '../../../api/login/index';
import { storeToRefs } from 'pinia';
import { useRoute, useRouter } from 'vue-router';
import { Session } from '/@/utils/storage';
import { NextLoading } from '/@/utils/loading';
import { useI18n } from 'vue-i18n';
import Cookies from 'js-cookie';
import { useThemeConfig } from '/@/stores/themeConfig';
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
import { initBackEndControlRoutes } from '/@/router/backEnd';
import { formatAxis } from '/@/utils/formatTime';// 定义变量内容
const { t } = useI18n();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const router = useRouter();// 使用登录API
const { setPassword } = useLoginApi();// 定义props和emits
interface Props {visible: boolean;
}interface Emits {(e: 'update:visible', value: boolean): void;(e: 'confirm', password: string): void;
}const props = defineProps<Props>();
const emit = defineEmits<Emits>();// 响应式数据
const dialogVisible = computed({get: () => props.visible,set: (value) => emit('update:visible', value),
});const formRef = ref<FormInstance>();
const loading = ref(false);
const showPassword = ref(false);
const showConfirmPassword = ref(false);const formData = reactive({password: '',confirmPassword: '',
});// 密码强度验证
const passwordLength = computed(() => formData.password.length >= 8);
const hasUpperCase = computed(() => /[A-Z]/.test(formData.password));
const hasLowerCase = computed(() => /[a-z]/.test(formData.password));
const hasNumber = computed(() => /[0-9]/.test(formData.password));
const hasSpecialChar = computed(() => /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(formData.password));const isPasswordValid = computed(() => passwordLength.value && hasUpperCase.value && hasLowerCase.value && hasNumber.value && hasSpecialChar.value);// 表单验证规则
const validatePassword = (rule: any, value: string, callback: any) => {if (!value) {callback(new Error('请输入密码'));} else if (!isPasswordValid.value) {callback(new Error('密码不符合要求'));} else {callback();}
};const validateConfirmPassword = (rule: any, value: string, callback: any) => {if (!value) {callback(new Error('请确认密码'));} else if (value !== formData.password) {callback(new Error('两次输入的密码不一致'));} else {callback();}
};const rules: FormRules = {password: [{ required: true, validator: validatePassword, trigger: 'blur' }],confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
};// 监听密码变化,实时验证
watch(() => formData.password,() => {if (formRef.value) {formRef.value.validateField('password');}}
);
// 时间获取
const currentTime = computed(() => {return formatAxis(new Date());
});// 方法
const handleConfirm = async () => {if (!formRef.value) return;try {const valid = await formRef.value.validate();if (valid) {loading.value = true;const loginRes = await setPassword({password: formData.password,});if (loginRes.code == 200) {signInSuccess();// if (!themeConfig.value.isRequestRoutes) {// // 前端控制路由,2、请注意执行顺序// const isNoPower = await initFrontEndControlRoutes();// signInSuccess(isNoPower);// } else {// // 模拟后端控制路由,isRequestRoutes 为 true,则开启后端控制路由// // 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"// const isNoPower = await initBackEndControlRoutes();// // 执行完 initBackEndControlRoutes,再执行 signInSuccess// signInSuccess(isNoPower);// }} else {ElMessage.success(loginRes.message);}// 模拟API调用// setTimeout(() => {// ElMessage.success('密码设置成功');// emit('confirm', formData.password);// resetForm();// loading.value = false;// }, 1000);}} catch (error) {console.log('表单验证失败', error);}
};
// 登录成功后的跳转
// isNoPower: boolean | undefined
const signInSuccess = () => {// 初始化登录成功时间问候语let currentTimeInfo = currentTime.value;// 登录成功,跳到转首页// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中if (route.query?.redirect) {router.push({path: <string>route.query?.redirect,query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',});} else {router.push('/');}// 登录成功提示const signInText = t('message.signInText');ElMessage.success(`${currentTimeInfo},${signInText}`);// 添加 loading,防止第一次进入界面时出现短暂空白NextLoading.start();
};const resetForm = () => {formData.password = '';formData.confirmPassword = '';showPassword.value = false;showConfirmPassword.value = false;if (formRef.value) {formRef.value.clearValidate();}
};
</script><style scoped lang="scss">
:deep(.el-dialog__header) {color: #4c62d1 !important;
}
:deep(.el-input__wrapper) {border-radius: 8px;
}
.my-header {color: #4c62d1;display: flex;justify-content: flex-start;font-weight: 700;font-size: 14px;padding: 0 20px;
}.password-setup-dialog {padding: 0 10px;.description {text-align: left;color: #999;margin-bottom: 10px;font-size: 14px;}.password-form {margin-bottom: 20px;:deep(.el-form-item__label) {font-weight: 500;margin-bottom: 8px;}.password-icon {cursor: pointer;color: #c0c4cc;&:hover {color: #909399;}}}.password-requirements {background-color: #f8f9fa;border-radius: 4px;padding: 12px 16px;margin-bottom: 20px;.requirement-title {font-size: 13px;color: #666;margin-bottom: 8px;}ul {list-style: none;padding: 0;margin: 0;li {font-size: 12px;color: #f56c6c;margin-bottom: 4px;transition: color 0.3s;&.met {color: #67c23a;}&::before {content: '•';margin-right: 6px;}}}}.dialog-footer {text-align: center;margin-top: 20px;.confirm-btn {width: 100%;height: 40px;font-size: 16px;}}
}
</style>
父组件 使用
<PasswordUpdateDiolag v-model:visible="showDialog" @confirm="handlePasswordUpdated" />
const showDialog = ref(false); //
这样 就是最标准的 v-model 双向绑定 可以有多个v-model

