在现代Web应用中,模态框(Modal)是实现弹出窗口、提示信息和表单交互等场景的重要UI组件。然而,传统的模态框实现方式往往缺乏灵活性和交互性。本文将介绍如何结合DeepSeek和Vue框架,打造一款丝滑、智能且高度可定制的模态框组件,为您的Web应用增添更多便捷和互动性。
📚前言
DeepSeek 有望在技术发展上取得更多突破,如持续提升模型性能、加强多模态融合技术的研发、提高模型的可解释性和安全性等。同时,DeepSeek 的发展将对 AI 行业格局和社会生产生活方式产生更为深远的影响,推动行业的创新和发展,为人们的生活带来更多的便利和智能化体验。
📚页面效果
📚指令输入
已经创建好了一个基于Vue3的组合式API的项目(Composition API),并能正常运行起来,请帮我用 Vue3的组合式API(Composition API) 生成一个 模态框(Modal) 的功能组件,所有代码都保存在components/Modal 下的文件夹中。功能组件的script标签中只有setup属性,使用普通 JavaScript 实现,不使用TypeScript。
功能要有,如下属性:
定义属性
显示与隐藏相关
visible
作用:控制模态框的显示与隐藏状态。
类型:Boolean
默认值:false
closeOnClickOverlay
作用:设置点击模态框的遮罩层是否关闭模态框。
类型:Boolean
默认值:true
closeOnEsc
作用:设置按下 ESC 键是否关闭模态框。
类型:Boolean
默认值:true
内容相关
title
作用:模态框的标题内容。
类型:String
默认值:''
content
作用:模态框的主体内容,可作为简单文本内容传入。
类型:String
默认值:''
样式与布局相关
width
作用:设置模态框的宽度。
类型:String 或 Number
默认值:可设为合适的默认宽度,如 '500px'
height
作用:设置模态框的高度。
类型:String 或 Number
默认值:可根据实际情况设定,如 'auto'
centered
作用:设置模态框是否垂直居中显示。
类型:Boolean
默认值:true
按钮相关
showOkButton
作用:是否显示确认按钮。
类型:Boolean
默认值:true
showCancelButton
作用:是否显示取消按钮。
类型:Boolean
默认值:true
okText
作用:确认按钮的文本。
类型:String
默认值:'确定'
cancelText
作用:取消按钮的文本。
类型:String
默认值:'取消'
定义事件
update:visible
作用:用于实现 v-model 双向绑定,当模态框的显示状态改变时触发,通常在关闭或打开模态框时触发。
参数:Boolean,表示新的显示状态。
ok
作用:点击确认按钮时触发,可用于执行确认操作。
cancel
作用:点击取消按钮、遮罩层或按下 ESC 键关闭模态框时触发,可用于执行取消操作。
open
作用:模态框打开时触发,可用于在模态框打开时执行一些初始化操作。
close
作用:模态框关闭时触发,可用于在模态框关闭时执行一些清理操作。
其他
提供插槽:使用 Vue 的插槽机制,允许用户自定义模态框的标题、内容、按钮等部分,增加组件的灵活性。
动画效果:添加淡入淡出、缩放等动画效果,提升用户体验。可以使用 CSS 动画或 Vue 的过渡组件实现。
可访问性:确保模态框在键盘操作和屏幕阅读器下能够正常使用,例如为按钮添加适当的 tabindex 和 aria-label 属性。
嵌套使用:考虑模态框可以嵌套使用的情况,处理好嵌套时的样式和交互问题。
样式定制:提供一些样式类名或 CSS 变量,方便用户自定义模态框的样式,如颜色、边框、字体等。
文档和示例:编写详细的文档,说明每个属性和事件的作用,并提供多种使用示例,方便其他开发者使用该组件。
你有更好的建议也可以添加,要注明。组件定义好后给出5个及以上的调用示例,示例中添加完整的数据和事件,确保每个示例是独立的。
下面是现有目录
DeepSeekAndVue/
├── src/ # 源代码目录
│ ├── assets/ # 静态资源
│ │ ├── base.css
│ │ ├── main.css
│ │ └── logo.svg
│ ├── components/ # 组件目录
│ │ ├── HelloWorld.vue
│ │ ├── TheWelcome.vue
│ │ ├── WelcomeItem.vue
│ │ ├── Progress/
│ │ │ └── Progress.vue
│ │ ├── Accordion/
│ │ ├── BackToTop/
│ │ ├── Card/
│ │ ├── InfiniteScroll/
│ │ ├── Notification/
│ │ ├── Timeline/
│ │ ├── Switch/
│ │ ├── Tabs/
│ │ ├── Sidebar/
│ │ ├── Breadcrumbs/
│ │ ├── MasonryLayout/
│ │ ├── Rating/
│ │ ├── ColorPicker/
│ │ ├── RightClickMenu/
│ │ ├── RangePicker/
│ │ ├── Navbar/
│ │ ├── FormValidation/
│ │ ├── CopyToClipboard/
│ │ ├── ClickAnimations/
│ │ ├── ThumbnailList/
│ │ ├── KeyboardShortcuts/
│ │ ├── CommentSystem/
│ │ ├── QRCode/
│ │ ├── RadioButton/
│ │ ├── Slider/
│ │ ├── ScrollAnimations/
│ │ ├── TextInput/
│ │ ├── Divider/
│ │ ├── Checkbox/
│ │ ├── TagInput/
│ │ ├── DropdownSelect/
│ │ ├── List/
│ │ ├── Header/
│ │ ├── Footer/
│ │ ├── Pagination/
│ │ ├── FloatingActionButton/
│ │ ├── PasswordInput/
│ │ ├── GridLayout/
│ │ ├── Flexbox/
│ │ ├── Modal/
│ │ ├── DatePicker/
│ │ └── icons/
│ ├── router/ # 路由配置
│ │ └── index.js
│ ├── stores/ # Pinia 状态管理
│ │ └── counter.js
│ ├── views/ # 页面组件
│ │ ├── ProgressView.vue
│ │ ├── TabsView.vue
│ │ ├── SwitchView.vue
│ │ ├── TimelineView.vue
│ │ ├── NotificationView.vue
│ │ ├── CardView.vue
│ │ ├── InfiniteScrollView.vue
│ │ ├── BackToTopView.vue
│ │ ├── AccordionView.vue
│ │ ├── SidebarView.vue
│ │ ├── BreadcrumbsView.vue
│ │ ├── MasonryLayoutView.vue
│ │ ├── RatingView.vue
│ │ ├── ColorPickerView.vue
│ │ ├── RightClickMenuView.vue
│ │ ├── RangePickerView.vue
│ │ ├── NavbarView.vue
│ │ ├── FormValidationView.vue
│ │ ├── CopyToClipboardView.vue
│ │ ├── ClickAnimationsView.vue
│ │ ├── ThumbnailListView.vue
│ │ ├── KeyboardShortcutsView.vue
│ │ ├── CommentSystemView.vue
│ │ ├── QRCodeView.vue
│ │ ├── RadioButtonView.vue
│ │ ├── SliderView.vue
│ │ ├── ScrollAnimationsView.vue
│ │ ├── TextInputView.vue
│ │ ├── DividerView.vue
│ │ ├── CheckboxView.vue
│ │ ├── TagInputView.vue
│ │ ├── DropdownSelectView.vue
│ │ ├── ListView.vue
│ │ ├── HeaderView.vue
│ │ ├── FooterView.vue
│ │ ├── PaginationView.vue
│ │ ├── FloatingActionButtonView.vue
│ │ ├── GridLayoutView.vue
│ │ ├── PasswordInputView.vue
│ │ ├── FlexboxView.vue
│ │ ├── ModalView.vue
│ │ ├── DatePickerView.vue
│ │ └── AboutView.vue
│ ├── App.vue # 根组件
│ └── main.js # 应用入口
├── public/ # 公共资源目录
├── index.html # HTML 模板
├── package.json # 项目配置
├── vite.config.js # Vite 配置
└── node_modules/ # 依赖包目录
📚think
📘组件代码
我将按照您的要求创建一个功能完善的Modal组件,并提供详细的调用示例。以下是组件实现:
<!-- components/Modal/Modal.vue --> <template> <teleport to="body"> <transition name="modal-fade"> <div v-if="visible" @click.self="handleOverlayClick" @keydown.esc="handleEsc" role="dialog" aria-modal="true" :aria-labelledby="title ? 'modal-title' : null" > <div : :style="{ width: typeof width === 'number' ? `${width}px` : width, height: typeof height === 'number' ? `${height}px` : height }" > <div class="modal-header"> <slot name="header"> <h2 v-if="title" class="modal-title">{{ title }}</h2> </slot> <button v-if="showCloseButton" @click="closeModal" aria-label="Close" > × </button> </div> <div class="modal-content"> <slot name="content"> <div v-if="content" class="modal-default-content">{{ content }}</div> </slot> </div> <div v-if="showFooter" class="modal-footer"> <slot name="footer"> <button v-if="showCancelButton" @click="handleCancel" > {{ cancelText }} </button> <button v-if="showOkButton" @click="handleOk" > {{ okText }} </button> </slot> </div> </div> </div> </transition> </teleport> </template> <script setup> import { computed, onMounted, onBeforeUnmount } from 'vue' const emit = defineEmits([ 'update:visible', 'ok', 'cancel', 'open', 'close' ]) const props = defineProps({ visible: { type: Boolean, default: false }, closeOnClickOverlay: { type: Boolean, default: true }, closeOnEsc: { type: Boolean, default: true }, title: { type: String, default: '' }, content: { type: String, default: '' }, width: { type: [String, Number], default: '500px' }, height: { type: [String, Number], default: 'auto' }, centered: { type: Boolean, default: true }, showOkButton: { type: Boolean, default: true }, showCancelButton: { type: Boolean, default: true }, okText: { type: String, default: '确定' }, cancelText: { type: String, default: '取消' }, showCloseButton: { type: Boolean, default: true } }) const showFooter = computed(() => props.showOkButton || props.showCancelButton ) const handleOverlayClick = () => { if (props.closeOnClickOverlay) { closeModal() } } const handleEsc = (event) => { if (props.closeOnEsc && event.key === 'Escape') { closeModal() } } const handleOk = () => { emit('ok') closeModal() } const handleCancel = () => { emit('cancel') closeModal() } const closeModal = () => { emit('update:visible', false) emit('close') } const openModal = () => { emit('open') } const handleKeydown = (e) => { if (props.visible) { handleEsc(e) } } onMounted(() => { window.addEventListener('keydown', handleKeydown) }) onBeforeUnmount(() => { window.removeEventListener('keydown', handleKeydown) }) </script> <style scoped> .modal-fade-enter-active, .modal-fade-leave-active { transition: opacity 0.3s ease; } .modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; } .modal-mask { position: fixed; z-index: 9998; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; transition: opacity 0.3s ease; } .modal-container { margin: auto; background-color: #fff; border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); transition: all 0.3s ease; overflow: hidden; display: flex; flex-direction: column; } .modal-container.centered { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); } .modal-header { padding: 16px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; } .modal-title { margin: 0; font-size: 1.25rem; } .modal-close { cursor: pointer; border: none; background: none; font-size: 1.5rem; padding: 0 8px; } .modal-content { padding: 16px; flex: 1; overflow-y: auto; } .modal-footer { padding: 16px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; } .modal-button { padding: 8px 16px; border-radius: 4px; cursor: pointer; border: 1px solid #ddd; } .ok-button { background-color: #007bff; color: white; border-color: #007bff; } .cancel-button { background-color: #6c757d; color: white; border-color: #6c757d; } </style>
以下是五个调用示例:
<!-- 示例1: 基础用法 --> <template> <button @click="showModal1 = true">打开基础模态框</button> <Modal v-model:visible="showModal1"> <template #header> <h3>基础标题</h3> </template> <template #content> <p>这是通过插槽自定义的内容</p> </template> </Modal> </template> <script setup> import { ref } from 'vue' import Modal from '@/components/Modal/Modal.vue' const showModal1 = ref(false) </script> <!-- 示例2: 禁用遮罩层关闭 --> <template> <button @click="showModal2 = true">打开安全模态框</button> <Modal v-model:visible="showModal2" :close-on-click-overlay="false" :close-on-esc="false" title="重要操作" content="此操作不可逆,请谨慎操作!" /> </template> <!-- 示例3: 自定义尺寸和按钮 --> <template> <button @click="showModal3 = true">打开设置面板</button> <Modal v-model:visible="showModal3" :centered="false" :show-cancel-button="false" ok-text="保存" > <template #content> <div class="settings-panel"> <!-- 自定义设置内容 --> </div> </template> </Modal> </template> <!-- 示例4: 事件处理 --> <template> <button @click="showModal4 = true">打开带事件的模态框</button> <Modal v-model:visible="showModal4" @ok="handleOk" @cancel="handleCancel" @open="handleOpen" @close="handleClose" /> </template> <script setup> const handleOk = () => { console.log('确认操作') // 执行数据提交等操作 } const handleCancel = () => { console.log('取消操作') // 执行清理操作 } const handleOpen = () => { console.log('模态框已打开') // 初始化操作 } const handleClose = () => { console.log('模态框已关闭') // 清理操作 } </script> <!-- 示例5: 完全自定义布局 --> <template> <button @click="showModal5 = true">打开自定义布局</button> <Modal v-model:visible="showModal5"> <template #header> <div class="custom-header"> <img src="https://blog.csdn.net/qq_33650655/article/details/logo.png" alt="logo"> <h3>自定义头部</h3> </div> </template> <template #content> <div class="custom-content"> <video controls src="https://blog.csdn.net/qq_33650655/article/details/demo.mp4"></video> </div> </template> <template #footer> <div class="custom-footer"> <button @click="showModal5 = false">关闭</button> <button @click="submitForm">提交</button> </div> </template> </Modal> </template>
补充说明:
新增了 showCloseButton 属性控制右上角关闭按钮的显示
使用 Teleport 将模态框渲染到 body 元素下,避免父级样式影响
添加了 ARIA 属性提升可访问性
支持键盘导航(ESC 键关闭)
使用 CSS 变量方便样式定制(可在全局样式中覆盖):
:root { --modal-z-index: 9999; --modal-bg-color: #fff; --modal-border-radius: 4px; }
建议在全局样式文件中添加以下基础样式:
/* 在 main.css 中添加 */ body.modal-open { overflow: hidden; padding-right: 15px; /* 防止滚动条消失导致的布局偏移 */ }
📚代码测试
运行正常
📚测试代码正常跑通,附其他基本代码
添加路由
页面展示入口
📘编写路由 src\router\index.js
import { createRouter, createWebHistory } from 'vue-router' import RightClickMenuView from '../views/RightClickMenuView.vue' import RangePickerView from '../views/RangePickerView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'progress', component: () => import('../views/ProgressView.vue'), }, { path: '/tabs', name: 'tabs', // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. // 标签页(Tabs) component: () => import('../views/TabsView.vue'), }, { path: '/accordion', name: 'accordion', // 折叠面板(Accordion) component: () => import('../views/AccordionView.vue'), }, { path: '/timeline', name: 'timeline', // 时间线(Timeline) component: () => import('../views/TimelineView.vue'), }, { path: '/backToTop', name: 'backToTop', component: () => import('../views/BackToTopView.vue') }, { path: '/notification', name: 'notification', component: () => import('../views/NotificationView.vue') }, { path: '/card', name: 'card', component: () => import('../views/CardView.vue') }, { path: '/infiniteScroll', name: 'infiniteScroll', component: () => import('../views/InfiniteScrollView.vue') }, { path: '/switch', name: 'switch', component: () => import('../views/SwitchView.vue') }, { path: '/sidebar', name: 'sidebar', component: () => import('../views/SidebarView.vue') }, { path: '/breadcrumbs', name: 'breadcrumbs', component: () => import('../views/BreadcrumbsView.vue') }, { path: '/masonryLayout', name: 'masonryLayout', component: () => import('../views/MasonryLayoutView.vue') }, { path: '/rating', name: 'rating', component: () => import('../views/RatingView.vue') }, { path: '/datePicker', name: 'datePicker', component: () => import('../views/DatePickerView.vue') }, { path: '/colorPicker', name: 'colorPicker', component: () => import('../views/ColorPickerView.vue') }, { path: '/rightClickMenu', name: 'rightClickMenu', component: RightClickMenuView }, { path: '/rangePicker', name: 'rangePicker', component: () => import('../views/RangePickerView.vue') }, { path: '/navbar', name: 'navbar', component: () => import('../views/NavbarView.vue') }, { path: '/formValidation', name: 'formValidation', component: () => import('../views/FormValidationView.vue') }, { path: '/copyToClipboard', name: 'copyToClipboard', component: () => import('../views/CopyToClipboardView.vue') }, { path: '/clickAnimations', name: 'clickAnimations', component: () => import('../views/ClickAnimationsView.vue') }, { path: '/thumbnailList', name: 'thumbnailList', component: () => import('../views/ThumbnailListView.vue') }, { path: '/keyboardShortcuts', name: 'keyboardShortcuts', component: () => import('../views/KeyboardShortcutsView.vue') }, { path: '/commentSystem', name: 'commentSystem', component: () => import('../views/CommentSystemView.vue') }, { path: '/qRCode', name: 'qRCode', component: () => import('../views/QRCodeView.vue') }, { path: '/radioButton', name: 'radioButton', component: () => import('../views/RadioButtonView.vue') }, { path: '/slider', name: 'slider', component: () => import('../views/SliderView.vue') }, { path: '/scrollAnimations', name: 'scrollAnimations', component: () => import('../views/ScrollAnimationsView.vue') }, { path: '/textInputView', name: 'textInputView', component: () => import('../views/TextInputView.vue') }, { path: '/divider', name: 'divider', component: () => import('../views/DividerView.vue') }, { path: '/checkbox', name: 'checkbox', component: () => import('../views/CheckboxView.vue') }, { path: '/tagInput', name: 'tagInput', component: () => import('../views/TagInputView.vue') }, { path: '/dropdownSelect', name: 'dropdownSelect', component: () => import('../views/DropdownSelectView.vue') }, { path: '/list', name: 'list', component: () => import('../views/ListView.vue') }, { path: '/header', name: 'header', component: () => import('../views/HeaderView.vue') }, { path: '/footer', name: 'footer', component: () => import('../views/FooterView.vue') }, { path: '/pagination', name: 'pagination', component: () => import('../views/PaginationView.vue') }, { path: '/floatingActionButton', name: 'floatingActionButton', component: () => import('../views/FloatingActionButtonView.vue') }, { path: '/gridLayout', name: 'gridLayout', component: () => import('../views/GridLayoutView.vue') }, { path: '/passwordInput', name: 'passwordInput', component: () => import('../views/PasswordInputView.vue') }, { path: '/flexbox', name: 'flexbox', component: () => import('../views/FlexboxView.vue') }, { path: '/modal', name: 'modal', component: () => import('../views/ModalView.vue') } ], }) export default router
📘编写展示入口 src\App.vue
<script setup> import { RouterLink, RouterView } from 'vue-router' import HelloWorld from './components/HelloWorld.vue' </script> <template> <header> <img alt="Vue logo" src="https://blog.csdn.net/qq_33650655/article/details/@/assets/logo.svg" /> <div class="wrapper"> <HelloWorld msg="You did it!" /> <nav> <RouterLink to="/">Progress</RouterLink> <RouterLink to="/tabs">Tabs</RouterLink> <RouterLink to="/accordion">Accordion</RouterLink> <RouterLink to="/timeline">Timeline</RouterLink> <RouterLink to="/backToTop">BackToTop</RouterLink> <RouterLink to="/notification">Notification</RouterLink> <RouterLink to="/card">Card</RouterLink> <RouterLink to="/infiniteScroll">InfiniteScroll</RouterLink> <RouterLink to="/switch">Switch</RouterLink> <RouterLink to="/sidebar">Sidebar</RouterLink> <RouterLink to="/breadcrumbs">Breadcrumbs</RouterLink> <RouterLink to="/masonryLayout">MasonryLayout</RouterLink> <RouterLink to="/rating">Rating</RouterLink> <RouterLink to="/datePicker">DatePicker</RouterLink> <RouterLink to="/colorPicker">ColorPicker</RouterLink> <RouterLink to="/rightClickMenu">RightClickMenu</RouterLink> <RouterLink to="/rangePicker">RangePicker</RouterLink> <RouterLink to="/navbar">Navbar</RouterLink> <RouterLink to="/formValidation">FormValidation</RouterLink> <RouterLink to="/copyToClipboard">CopyToClipboard</RouterLink> <RouterLink to="/clickAnimations">ClickAnimations</RouterLink> <RouterLink to="/thumbnailList">ThumbnailList</RouterLink> <RouterLink to="/keyboardShortcuts">KeyboardShortcuts</RouterLink> <RouterLink to="/commentSystem">CommentSystem</RouterLink> <RouterLink to="/qRCode">QRCode</RouterLink> <RouterLink to="/radioButton">RadioButton</RouterLink> <RouterLink to="/slider">Slider</RouterLink> <RouterLink to="/scrollAnimations">ScrollAnimations</RouterLink> <RouterLink to="/textInputView">TextInput</RouterLink> <RouterLink to="/divider">Divider</RouterLink> <RouterLink to="/checkbox">Checkbox</RouterLink> <RouterLink to="/tagInput">TagInput</RouterLink> <RouterLink to="/dropdownSelect">DropdownSelect</RouterLink> <RouterLink to="/list">List</RouterLink> <RouterLink to="/header">Header</RouterLink> <RouterLink to="/footer">Footer</RouterLink> <RouterLink to="/pagination">Pagination</RouterLink> <RouterLink to="/floatingActionButton">FloatingActionButton</RouterLink> <RouterLink to="/gridLayout">GridLayout</RouterLink> <RouterLink to="/passwordInput">PasswordInput</RouterLink> <RouterLink to="/flexbox">Flexbox</RouterLink> <RouterLink to="/modal">Modal</RouterLink> </nav> </div> </header> <RouterView /> </template> <style scoped> header { line-height: 1.5; max-height: 100vh; } .logo { display: block; margin: 0 auto 2rem; } nav { width: 100%; font-size: 12px; text-align: center; margin-top: 2rem; } nav a.router-link-exact-active { color: var(--color-text); } nav a.router-link-exact-active:hover { background-color: transparent; } nav a { display: inline-block; padding: 0 1rem; border-left: 1px solid var(--color-border); } nav a:first-of-type { border: 0; } @media (min-width: 1024px) { header { display: flex; place-items: center; padding-right: calc(var(--section-gap) / 2); } .logo { margin: 0 2rem 0 0; } header .wrapper { display: flex; place-items: flex-start; flex-wrap: wrap; } nav { text-align: left; margin-left: -1rem; font-size: 1rem; padding: 1rem 0; margin-top: 1rem; } } </style>
总结
通过结合DeepSeek的强大搜索和推荐能力,以及Vue框架的响应式和组件化特性,我们成功打造了一款丝滑、智能且高度可定制的模态框组件。这款组件不仅能够根据用户操作动态调整样式和内容,还能与用户交互,提供更加个性化的体验。希望本文的介绍能够为您的Web开发带来新的灵感和思路,让您的应用在细节之处更加出彩。
本文来源于#宝码香车,由@蜜芽 整理发布。如若内容造成侵权/违法违规/事实不符,请联系本站客服处理!
该文章观点仅代表作者本人,不代表本站立场。本站不承担相关法律责任。
如若转载,请注明出处:https://www.zhanid.com/biancheng/3479.html