做一个vue3 v-model 双向绑定的弹窗

子组件

diolag.vue

复制代码
<template>
	<el-dialog
		v-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

相关推荐
前端付豪3 小时前
项目启动:搭建Vue 3工程化项目
前端·javascript·vue.js
小琴爱减肥3 小时前
Vue3 组合式 API 实战
vue.js
Giant1003 小时前
小白也能懂:网页一堆 JS 变慢了?附具体代码优化方案
javascript
秋田君4 小时前
3D热力图封装组件:Vue + Three.js 实现3D图表详解
javascript·vue.js·3d·three.js·热力图
Moment4 小时前
Next.js 16 新特性:如何启用 MCP 与 AI 助手协作 🤖🤖🤖
前端·javascript·node.js
吃饺子不吃馅4 小时前
Canvas高性能Table架构深度解析
前端·javascript·canvas
一枚前端小能手4 小时前
🔄 重学Vue之生命周期 - 从源码层面解析到实战应用的完整指南
前端·javascript·vue.js
一枚前端小能手4 小时前
「周更第9期」实用JS库推荐:mitt - 极致轻量的事件发射器深度解析
前端·javascript
Moment4 小时前
为什么 Electron 项目推荐使用 Monorepo 架构 🚀🚀🚀
前端·javascript·github