在Web应用中,日期选择器(Date Picker)是一个常见且重要的组件。无论是日历应用、预订系统还是数据分析工具,日期选择器都扮演着不可或缺的角色。然而,实现一个高效、易用且美观的日期选择器并非易事。本文将介绍如何使用DeepSeek和Vue框架,打造一个丝滑流畅的日期选择器,为用户提供极致的交互体验。
📚页面效果
📚指令输入
已经创建好了一个基于Vue3的组合式API的项目(Composition API),并能正常运行起来,请帮我用 Vue3的组合式API(Composition API) 生成一个 日期选择器(Date Picker) 的功能组件,所有代码都保存在components/DatePicker 下的文件夹中。功能组件的script标签中只有setup属性,使用普通 JavaScript 实现,不使用TypeScript。
功能要有,如下属性:
属性(Props)
modelValue
类型:Date | Date[] | null
默认值:null
说明:绑定的日期值(支持 v-model,范围选择时为数组)。
minDate
类型:Date | null
默认值:null
说明:最小可选日期,早于此日期的选项被禁用。
maxDate
类型:Date | null
默认值:null
说明:最大可选日期,晚于此日期的选项被禁用。
format
类型:string
默认值:'YYYY-MM-DD'
说明:日期显示格式(如 YYYY-MM-DD、MM/DD/YYYY)。
disabled
类型:boolean
默认值:false
说明:是否禁用组件。
placeholder
类型:string
默认值:'请选择日期'
说明:输入框占位符文本。
firstDayOfWeek
类型:number(0-6,0=周日)
默认值:1(周一)
说明:周起始日。
showWeekNumbers
类型:boolean
默认值:false
说明:是否显示周数。
isRange
类型:boolean
默认值:false
说明:是否为范围选择模式(modelValue 为 Date[])。
locale
类型:string | object
默认值:'en'
说明:本地化配置(语言、月份名等)。
isInline
类型:boolean
默认值:false
说明:是否内联显示日历面板(无需点击输入框)。
className
类型:string
默认值:''
说明:自定义容器类名。
showConfirmButton
类型:boolean
默认值:true
说明:是否显示确认按钮。
confirmText
类型:string
默认值:'确认'
说明:确认按钮文本。
disableDate
类型:(date: Date) => boolean
默认值:() => false
说明:动态禁用日期(返回 true 表示禁用)。
事件(Events)
update:modelValue
参数:Date | Date[] | null
说明:选中日期变化时触发(用于 v-model 同步)。
change
参数:{ value: Date | Date[] | null, isValid: boolean }
说明:日期变化时触发,附带有效性校验。
confirm
参数:Date | Date[] | null
说明:点击确认按钮时触发。
open
参数:无
说明:日历面板打开时触发。
close
参数:无
说明:日历面板关闭时触发。
invalid
参数:{ reason: string }
说明:用户输入或选择无效日期时触发。
你有更好的建议也可以添加,要注明。组件定义好后给出3个及以上的调用示例。
下面是现有目录
vueAndDeepseek/
├── 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/
│ │ ├── 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
│ │ ├── DatePickerView.vue
│ │ └── AboutView.vue
│ ├── App.vue # 根组件
│ └── main.js # 应用入口
├── public/ # 公共资源目录
├── index.html # HTML 模板
├── package.json # 项目配置
├── vite.config.js # Vite 配置
└── node_modules/ # 依赖包目录
📘组件代码,src\components\DatePicker\DatePicker.vue
<script setup> import { ref, computed, watch } from 'vue' // Props 定义 const props = defineProps({ modelValue: { type: [Date, Array, null], default: null }, minDate: { type: Date, default: null }, maxDate: { type: Date, default: null }, format: { type: String, default: 'YYYY-MM-DD' }, disabled: { type: Boolean, default: false }, placeholder: { type: String, default: '请选择日期' }, firstDayOfWeek: { type: Number, default: 1 }, showWeekNumbers: { type: Boolean, default: false }, isRange: { type: Boolean, default: false }, locale: { type: [String, Object], default: 'zh-CN' }, isInline: { type: Boolean, default: false }, className: { type: String, default: '' }, showConfirmButton: { type: Boolean, default: true }, confirmText: { type: String, default: '确认' }, disableDate: { type: Function, default: () => false } }) const emit = defineEmits(['update:modelValue', 'change', 'confirm', 'open', 'close', 'invalid']) // 初始化状态 const currentDate = ref(new Date()) const selectedDate = ref(props.isRange ? [] : null) const isOpen = ref(props.isInline) // 格式化单个日期 const formatSingleDate = (date) => { if (!(date instanceof Date) || isNaN(date.getTime())) return '' const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return props.format .replace('YYYY', year) .replace('MM', month) .replace('DD', day) } // 格式化显示日期 const formattedDate = computed(() => { if (!selectedDate.value) return '' if (Array.isArray(selectedDate.value)) { return selectedDate.value .map(date => date instanceof Date ? formatSingleDate(date) : '') .filter(Boolean) .join(' - ') } return formatSingleDate(selectedDate.value) }) // 日历数据 const calendarDays = computed(() => { const year = currentDate.value.getFullYear() const month = currentDate.value.getMonth() const firstDay = new Date(year, month, 1) const lastDay = new Date(year, month + 1, 0) const days = [] const firstDayOfWeek = firstDay.getDay() const prevMonthDays = (firstDayOfWeek - props.firstDayOfWeek + 7) % 7 // 上月日期 for (let i = prevMonthDays; i > 0; i--) { const date = new Date(year, month, 1 - i) days.push({ date, isCurrentMonth: false, isDisabled: isDateDisabled(date) }) } // 当月日期 for (let i = 1; i <= lastDay.getDate(); i++) { const date = new Date(year, month, i) days.push({ date, isCurrentMonth: true, isDisabled: isDateDisabled(date) }) } // 下月日期 const remainingDays = 42 - days.length for (let i = 1; i <= remainingDays; i++) { const date = new Date(year, month + 1, i) days.push({ date, isCurrentMonth: false, isDisabled: isDateDisabled(date) }) } return days }) // 星期标题 const weekDays = computed(() => { const days = ['日', '一', '二', '三', '四', '五', '六'] return [...days.slice(props.firstDayOfWeek), ...days.slice(0, props.firstDayOfWeek)] }) // 判断日期是否禁用 const isDateDisabled = (date) => { if (!(date instanceof Date)) return true if (props.minDate && date < props.minDate) return true if (props.maxDate && date > props.maxDate) return true return props.disableDate(date) } // 选择日期 const selectDate = (day) => { if (!day.date || day.isDisabled) { emit('invalid', { reason: 'date disabled' }) return } const newDate = new Date(day.date) if (props.isRange) { if (!Array.isArray(selectedDate.value)) { selectedDate.value = [] } if (selectedDate.value.length === 0 || selectedDate.value.length === 2) { selectedDate.value = [newDate] } else { selectedDate.value = [selectedDate.value[0], newDate].sort((a, b) => a - b) } } else { selectedDate.value = newDate } emit('update:modelValue', selectedDate.value) emit('change', { value: selectedDate.value, isValid: true }) } // 切换月份 const changeMonth = (delta) => { currentDate.value = new Date( currentDate.value.getFullYear(), currentDate.value.getMonth() + delta, 1 ) } // 确认选择 const confirm = () => { emit('confirm', selectedDate.value) isOpen.value = false emit('close') } // 切换日历显示 const toggleCalendar = () => { if (props.disabled) return isOpen.value = !isOpen.value emit(isOpen.value ? 'open' : 'close') } // 监听数据变化 watch(() => props.modelValue, (newVal) => { if (props.isRange) { selectedDate.value = Array.isArray(newVal) ? newVal.map(d => d instanceof Date ? new Date(d) : null) : [] } else { selectedDate.value = newVal instanceof Date ? new Date(newVal) : null } }, { immediate: true }) </script> <template> <div :class="['date-picker', className]"> <input type="text" :placeholder="placeholder" :value="formattedDate" @click="toggleCalendar" readonly :disabled="disabled" /> <div v-if="isOpen || isInline" class="calendar"> <div class="calendar-header"> <button @click="changeMonth(-1)"><</button> <span>{{ currentDate.getFullYear() }}年 {{ currentDate.getMonth() + 1 }}月</span> <button @click="changeMonth(1)">></button> </div> <div class="calendar-weeks"> <div v-for="day in weekDays" :key="day" class="week-day">{{ day }}</div> </div> <div class="calendar-days"> <div v-for="day in calendarDays" :key="day.date.getTime()" :class="[ 'calendar-day', { 'is-current-month': day.isCurrentMonth, 'is-disabled': day.isDisabled, 'is-selected': selectedDate && ( Array.isArray(selectedDate) ? selectedDate.some(d => d && d.getTime() === day.date.getTime()) : selectedDate.getTime() === day.date.getTime() ) } ]" @click="selectDate(day)" > {{ day.date.getDate() }} </div> </div> <div v-if="showConfirmButton" class="calendar-footer"> <button @click="confirm">{{ confirmText }}</button> </div> </div> </div> </template> <style scoped> .date-picker { display: inline-block; position: relative; } input { padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px; width: 200px; cursor: pointer; } .calendar { position: absolute; top: 100%; left: 0; margin-top: 5px; background: white; border: 1px solid #dcdfe6; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); z-index: 1000; } .calendar-header { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #ebeef5; } .calendar-weeks { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; border-bottom: 1px solid #ebeef5; } .week-day { padding: 8px; color: #606266; } .calendar-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; padding: 8px; } .calendar-day { padding: 8px; text-align: center; cursor: pointer; color: #606266; } .calendar-day:hover:not(.is-disabled) { background-color: #f5f7fa; } .is-current-month { color: #303133; } .is-selected { background-color: #409eff; color: white; } .is-disabled { color: #c0c4cc; cursor: not-allowed; background-color: #f5f7fa; } .calendar-footer { padding: 8px; text-align: right; border-top: 1px solid #ebeef5; } button { padding: 6px 12px; background: #409eff; border: none; border-radius: 4px; color: white; cursor: pointer; } button:hover { background: #66b1ff; } .is-inline { position: static; margin-top: 0; } </style>
📘调用 src\views\DatePickerView.vue
<script setup> import { ref } from 'vue' import DatePicker from '../components/DatePicker/DatePicker.vue' // 初始化状态 const basicDate = ref(null) const rangeDate = ref(null) const disabledDate = ref(null) const customDate = ref(null) const inlineDate = ref(null) const limitedDate = ref(null) // 日期范围 const minDate = new Date('2024-01-01') const maxDate = new Date('2024-12-31') // 格式化函数 const formatDate = (date) => { if (!date) return '未选择' if (Array.isArray(date)) { if (!date.length) return '未选择' return date .map(d => { if (!d) return '' return d instanceof Date ? d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }) : '' }) .filter(Boolean) .join(' - ') } return date instanceof Date ? date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }) : '未选择' } // 禁用周末 const isWeekend = (date) => { if (!(date instanceof Date)) return false const day = date.getDay() return day === 0 || day === 6 } // 事件处理 const handleChange = ({ value, isValid }) => { console.log('日期变化:', formatDate(value), '是否有效:', isValid) } const handleConfirm = (date) => { alert(`确认选择:${formatDate(date)}`) } </script> <template> <div class="datepicker-demo"> <!-- 基础示例 --> <section class="demo-block"> <h3>基础用法</h3> <DatePicker v-model="basicDate" placeholder="请选择日期" @change="handleChange" /> <div class="demo-value">选中值: {{ formatDate(basicDate) }}</div> </section> <!-- 日期范围选择 --> <section class="demo-block"> <h3>日期范围</h3> <DatePicker v-model="rangeDate" :is-range="true" placeholder="请选择日期范围" @change="handleChange" /> <div class="demo-value">选中值: {{ formatDate(rangeDate) }}</div> </section> <!-- 禁用周末 --> <section class="demo-block"> <h3>禁用周末</h3> <DatePicker v-model="disabledDate" :disable-date="isWeekend" placeholder="请选择工作日" @change="handleChange" /> <div class="demo-value">选中值: {{ formatDate(disabledDate) }}</div> </section> <!-- 自定义格式 --> <section class="demo-block"> <h3>自定义格式</h3> <DatePicker v-model="customDate" format="MM/DD/YYYY" placeholder="月/日/年" @change="handleChange" /> <div class="demo-value">选中值: {{ formatDate(customDate) }}</div> </section> <!-- 设置日期范围 --> <section style=""> <h3>限制可选日期范围</h3> <DatePicker v-model="limitedDate" :min-date="minDate" :max-date="maxDate" placeholder="选择2024年日期" @change="handleChange" @confirm="handleConfirm" /> <div class="demo-value">选中值: {{ formatDate(limitedDate) }}</div> </section> <!-- 内联显示 --> <section class="demo-block1"> <h3>内联显示</h3> <DatePicker v-model="inlineDate" :is-inline="true" @change="handleChange" /> <div class="demo-value">选中值: {{ formatDate(inlineDate) }}</div> </section> </div> </template> <style scoped> .datepicker-demo { padding: 20px; max-width: 800px; margin: 0 auto; } .demo-block { margin-bottom: 30px; padding: 20px; border: 1px solid #ebeef5; border-radius: 4px; background-color: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } .demo-block1{ margin-top: 300px; } h3 { margin-bottom: 15px; font-size: 18px; color: #333; font-weight: 500; } .demo-value { margin-top: 10px; padding: 10px; background: #f5f7fa; border-radius: 4px; font-size: 14px; color: #666; border: 1px solid #e4e7ed; } </style>
📚代码测试
正常
📚测试代码正常跑通,附其他基本代码
添加路由
页面展示入口
📘编写路由 src\router\index.js
import { createRouter, createWebHistory } from 'vue-router' 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') } ], }) 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> </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框架,我们成功打造了一个高效、易用且美观的日期选择器。DeepSeek的强大功能和Vue的响应式机制,使得日期选择器在用户体验上达到了新的高度。无论是日期的实时预览、精确的日期选择还是自定义日期范围的设置,都得到了完美的实现。希望本文的介绍能够为开发者提供有价值的参考,助力大家在Web应用开发中实现更加出色的日期选择功能。
本文来源于#宝码香车,由@蜜芽 整理发布。如若内容造成侵权/违法违规/事实不符,请联系本站客服处理!
该文章观点仅代表作者本人,不代表本站立场。本站不承担相关法律责任。
如若转载,请注明出处:https://www.zhanid.com/biancheng/3420.html