C_Date 日期选择器组件
📅 基于 Naive UI 的智能日期选择器,让日期选择更智能、更便捷
🚀 特性
- 🔄 五种选择模式: 单日期、日期时间、日期范围、日期时间范围、智能范围
- 🧠 智能联动限制: 结束日期自动限制不早于开始日期
- 🚫 灵活禁用规则: 支持禁用今天之前/之后的日期
- ⚙️ 高度可配置: 支持占位符、格式化、额外属性传递
- 💪 TypeScript: 完整的类型定义和类型安全
- 📡 丰富事件系统: 多种日期变化事件回调
- 🔧 暴露接口: 提供清空方法和内部状态访问
- ⚡ 响应式设计: 自适应布局和样式
📦 安装
bash
# 基于 Naive UI,确保已安装依赖
npm install naive-ui
1
2
2
🎯 快速开始
基础使用
vue
<template>
<!-- 最简单的单日期选择 -->
<C_Date mode="date" @change="handleDateChange" />
<!-- 日期范围选择 -->
<C_Date mode="daterange" @change="handleRangeChange" />
<!-- 智能双日期选择 -->
<C_Date mode="smart-range" @update:smartRange="handleSmartRangeChange" />
</template>
<script setup>
const handleDateChange = (value) => {
console.log('日期变更:', value)
}
const handleRangeChange = (value) => {
console.log('范围变更:', value)
}
const handleSmartRangeChange = (value) => {
console.log('智能范围变更:', value)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
五种选择模式
vue
<template>
<div class="date-modes-demo">
<!-- 1. 单日期选择 -->
<div class="demo-section">
<h4>单日期选择</h4>
<C_Date
mode="date"
placeholder="请选择日期"
@update:singleDate="handleSingleDate"
/>
</div>
<!-- 2. 日期时间选择 -->
<div class="demo-section">
<h4>日期时间选择</h4>
<C_Date
mode="datetime"
placeholder="请选择日期时间"
value-format="yyyy-MM-dd HH:mm:ss"
@update:singleDateTime="handleSingleDateTime"
/>
</div>
<!-- 3. 日期范围选择 -->
<div class="demo-section">
<h4>日期范围选择</h4>
<C_Date
mode="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
@update:dateRange="handleDateRange"
/>
</div>
<!-- 4. 日期时间范围选择 -->
<div class="demo-section">
<h4>日期时间范围选择</h4>
<C_Date
mode="datetimerange"
start-placeholder="开始日期时间"
end-placeholder="结束日期时间"
value-format="yyyy-MM-dd HH:mm:ss"
@update:dateTimeRange="handleDateTimeRange"
/>
</div>
<!-- 5. 智能双日期选择 -->
<div class="demo-section">
<h4>智能双日期选择</h4>
<C_Date
mode="smart-range"
start-placeholder="选择开始日期"
end-placeholder="选择结束日期"
@update:smartRange="handleSmartRange"
/>
</div>
</div>
</template>
<script setup>
const handleSingleDate = (value) => {
console.log('单日期:', value)
}
const handleSingleDateTime = (value) => {
console.log('日期时间:', value)
}
const handleDateRange = (value) => {
console.log('日期范围:', value)
}
const handleDateTimeRange = (value) => {
console.log('日期时间范围:', value)
}
const handleSmartRange = (value) => {
console.log('智能范围:', value)
}
</script>
<style scoped>
.demo-section {
margin-bottom: 20px;
padding: 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
📖 API 文档
Props
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
mode | 'date' | 'datetime' | 'daterange' | 'datetimerange' | 'smart-range' | 'date' | 日期选择器模式 |
placeholder | string | '' | 占位符文本(单日期模式) |
startPlaceholder | string | '' | 开始日期占位符(范围模式) |
endPlaceholder | string | '' | 结束日期占位符(范围模式) |
disabled | boolean | false | 是否禁用 |
disabledBeforeToday | boolean | false | 是否禁用今天之前的日期 |
disabledAfterToday | boolean | false | 是否禁用今天之后的日期 |
valueFormat | string | 'yyyy-MM-dd' | 值格式化字符串 |
startDateProps | Record<string, any> | {} | 开始日期额外属性(智能范围) |
endDateProps | Record<string, any> | {} | 结束日期额外属性(智能范围) |
Events
事件名 | 参数 | 说明 |
---|---|---|
update:singleDate | (value: number | null) | 单日期变化事件 |
update:singleDateTime | (value: number | null) | 单日期时间变化事件 |
update:dateRange | (value: [number, number] | null) | 日期范围变化事件 |
update:dateTimeRange | (value: [number, number] | null) | 日期时间范围变化事件 |
update:smartRange | (value: [number, number] | null) | 智能范围变化事件 |
change | (value: number | [number, number] | null) | 通用变化事件 |
暴露方法
方法名 | 参数 | 返回值 | 说明 |
---|---|---|---|
clearAll | - | void | 清空所有日期值 |
暴露属性
属性名 | 类型 | 说明 |
---|---|---|
singleDate | Ref<number | null> | 单日期值 |
singleDateTime | Ref<number | null> | 单日期时间值 |
dateRange | Ref<[number, number] | null> | 日期范围值 |
dateTimeRange | Ref<[number, number] | null> | 日期时间范围值 |
startDate | Ref<number | null> | 智能范围开始日期 |
endDate | Ref<number | null> | 智能范围结束日期 |
类型定义
日期选择器模式
typescript
type DatePickerMode =
| 'date' // 单日期选择
| 'datetime' // 日期时间选择
| 'daterange' // 日期范围选择
| 'datetimerange' // 日期时间范围选择
| 'smart-range' // 智能双日期选择
1
2
3
4
5
6
2
3
4
5
6
日期值类型
typescript
type DateValue = number | null
type DateRangeValue = [number, number] | null
1
2
2
🎨 使用示例
场景 1: 假期申请系统
vue
<template>
<div class="vacation-application">
<h3>假期申请</h3>
<n-form :model="vacationForm" :rules="vacationRules" ref="formRef">
<n-form-item label="假期类型" path="type">
<n-select
v-model:value="vacationForm.type"
:options="vacationTypes"
placeholder="请选择假期类型"
/>
</n-form-item>
<n-form-item label="假期时间" path="dateRange">
<C_Date
mode="smart-range"
start-placeholder="假期开始日期"
end-placeholder="假期结束日期"
:disabled-before-today="true"
value-format="yyyy-MM-dd"
@update:smartRange="handleVacationDateChange"
/>
</n-form-item>
<n-form-item label="申请原因" path="reason">
<n-input
v-model:value="vacationForm.reason"
type="textarea"
placeholder="请填写申请原因"
:rows="3"
/>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit">提交申请</n-button>
<n-button @click="handleReset">重置</n-button>
</n-form-item>
</n-form>
<!-- 申请结果 -->
<div v-if="applicationResult" class="application-result">
<n-alert type="success" title="申请提交成功">
<p>假期类型: {{ applicationResult.typeName }}</p>
<p>假期时间: {{ applicationResult.dateRange }}</p>
<p>假期天数: {{ applicationResult.days }} 天</p>
</n-alert>
</div>
</div>
</template>
<script setup>
const formRef = ref()
const vacationForm = ref({
type: null,
dateRange: null,
reason: '',
})
const applicationResult = ref(null)
const vacationTypes = [
{ label: '年假', value: 'annual' },
{ label: '病假', value: 'sick' },
{ label: '事假', value: 'personal' },
{ label: '调休', value: 'compensatory' },
]
const vacationRules = {
type: {
required: true,
message: '请选择假期类型',
trigger: 'change',
},
dateRange: {
validator: (rule, value) => {
if (!value || value.length !== 2) {
return new Error('请选择完整的假期时间')
}
const [start, end] = value
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24))
if (daysDiff > 30) {
return new Error('单次假期申请不能超过30天')
}
return true
},
trigger: 'change',
},
reason: {
required: true,
message: '请填写申请原因',
trigger: 'blur',
},
}
const handleVacationDateChange = (value) => {
vacationForm.value.dateRange = value
}
const handleSubmit = () => {
formRef.value?.validate((errors) => {
if (!errors) {
const { type, dateRange, reason } = vacationForm.value
const typeName = vacationTypes.find((t) => t.value === type)?.label
const [start, end] = dateRange
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
applicationResult.value = {
typeName,
dateRange: `${formatDate(start)} 至 ${formatDate(end)}`,
days,
}
}
})
}
const handleReset = () => {
vacationForm.value = {
type: null,
dateRange: null,
reason: '',
}
applicationResult.value = null
}
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleDateString('zh-CN')
}
</script>
<style scoped>
.vacation-application {
max-width: 600px;
margin: 0 auto;
padding: 24px;
}
.application-result {
margin-top: 24px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
场景 2: 活动报名时间管理
vue
<template>
<div class="event-registration">
<h3>活动报名时间设置</h3>
<n-card title="活动基本信息">
<n-form :model="eventForm" label-placement="left" label-width="120px">
<n-form-item label="活动名称">
<n-input
v-model:value="eventForm.name"
placeholder="请输入活动名称"
/>
</n-form-item>
<n-form-item label="活动日期">
<C_Date
mode="daterange"
start-placeholder="活动开始日期"
end-placeholder="活动结束日期"
:disabled-before-today="true"
@update:dateRange="handleEventDateChange"
/>
</n-form-item>
<n-form-item label="报名时间">
<C_Date
mode="smart-range"
start-placeholder="报名开始时间"
end-placeholder="报名截止时间"
:disabled-before-today="true"
:end-date-props="registrationEndDateProps"
@update:smartRange="handleRegistrationDateChange"
/>
</n-form-item>
<n-form-item label="活动地点">
<n-input
v-model:value="eventForm.location"
placeholder="请输入活动地点"
/>
</n-form-item>
<n-form-item label="活动描述">
<n-input
v-model:value="eventForm.description"
type="textarea"
placeholder="请输入活动描述"
:rows="3"
/>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSaveEvent">保存活动</n-button>
<n-button @click="handlePreviewEvent">预览</n-button>
</n-form-item>
</n-form>
</n-card>
<!-- 活动预览 -->
<n-card v-if="eventPreview" title="活动预览" class="mt-4">
<div class="event-preview">
<h4>{{ eventPreview.name }}</h4>
<div class="event-info">
<p><strong>活动时间:</strong> {{ eventPreview.eventDate }}</p>
<p><strong>报名时间:</strong> {{ eventPreview.registrationDate }}</p>
<p><strong>活动地点:</strong> {{ eventPreview.location }}</p>
<p><strong>活动描述:</strong> {{ eventPreview.description }}</p>
<n-tag :type="eventPreview.registrationStatus.type">
{{ eventPreview.registrationStatus.text }}
</n-tag>
</div>
</div>
</n-card>
</div>
</template>
<script setup>
const eventForm = ref({
name: '',
eventDate: null,
registrationDate: null,
location: '',
description: '',
})
const eventPreview = ref(null)
// 报名截止时间不能晚于活动开始时间
const registrationEndDateProps = computed(() => ({
isDateDisabled: (timestamp) => {
if (!eventForm.value.eventDate || !eventForm.value.eventDate[0]) {
return false
}
const eventStartDate = eventForm.value.eventDate[0]
return timestamp >= eventStartDate
},
}))
const handleEventDateChange = (value) => {
eventForm.value.eventDate = value
// 如果活动日期改变,重置报名时间
eventForm.value.registrationDate = null
}
const handleRegistrationDateChange = (value) => {
eventForm.value.registrationDate = value
}
const handleSaveEvent = () => {
if (validateEventForm()) {
// 保存逻辑
$message.success('活动保存成功')
handlePreviewEvent()
}
}
const handlePreviewEvent = () => {
if (!validateEventForm()) return
const { name, eventDate, registrationDate, location, description } =
eventForm.value
eventPreview.value = {
name,
eventDate: `${formatDate(eventDate[0])} 至 ${formatDate(eventDate[1])}`,
registrationDate: `${formatDate(registrationDate[0])} 至 ${formatDate(
registrationDate[1]
)}`,
location,
description,
registrationStatus: getRegistrationStatus(registrationDate),
}
}
const validateEventForm = () => {
const { name, eventDate, registrationDate, location } = eventForm.value
if (!name || !eventDate || !registrationDate || !location) {
$message.warning('请填写完整的活动信息')
return false
}
return true
}
const getRegistrationStatus = (registrationDate) => {
if (!registrationDate) return { type: 'default', text: '未设置' }
const now = Date.now()
const [start, end] = registrationDate
if (now < start) {
return { type: 'info', text: '报名未开始' }
} else if (now >= start && now <= end) {
return { type: 'success', text: '报名进行中' }
} else {
return { type: 'error', text: '报名已截止' }
}
}
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleDateString('zh-CN')
}
</script>
<style scoped>
.event-registration {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.event-preview {
padding: 16px;
}
.event-info p {
margin: 8px 0;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
场景 3: 财务报表查询
vue
<template>
<div class="financial-report">
<h3>财务报表查询</h3>
<n-card>
<div class="query-form">
<div class="form-row">
<n-form-item label="报表类型">
<n-select
v-model:value="queryForm.reportType"
:options="reportTypes"
placeholder="请选择报表类型"
@update:value="handleReportTypeChange"
/>
</n-form-item>
<n-form-item label="查询时间">
<C_Date
:mode="datePickerMode"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
:placeholder="singlePlaceholder"
:disabled-after-today="true"
value-format="yyyy-MM-dd"
@change="handleDateChange"
/>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleQuery" :loading="querying">
查询报表
</n-button>
<n-button @click="handleExport" :disabled="!reportData">
导出报表
</n-button>
</n-form-item>
</div>
<!-- 快捷日期选择 -->
<div class="quick-dates">
<span class="quick-label">快捷选择:</span>
<n-button
v-for="quick in quickDates"
:key="quick.label"
size="small"
@click="handleQuickDate(quick)"
>
{{ quick.label }}
</n-button>
</div>
</div>
<!-- 报表结果 -->
<div v-if="reportData" class="report-result">
<n-divider title-placement="left">
{{ queryForm.reportTypeName }} - {{ reportPeriod }}
</n-divider>
<div class="report-summary">
<n-statistic
v-for="item in reportData.summary"
:key="item.label"
:label="item.label"
:value="item.value"
:precision="2"
/>
</div>
<n-data-table
:columns="reportColumns"
:data="reportData.details"
:pagination="pagination"
/>
</div>
</n-card>
</div>
</template>
<script setup>
const queryForm = ref({
reportType: 'daily',
reportTypeName: '日报表',
dateValue: null,
})
const reportData = ref(null)
const querying = ref(false)
const reportTypes = [
{ label: '日报表', value: 'daily' },
{ label: '周报表', value: 'weekly' },
{ label: '月报表', value: 'monthly' },
{ label: '季度报表', value: 'quarterly' },
{ label: '年度报表', value: 'yearly' },
{ label: '自定义区间', value: 'custom' },
]
const quickDates = computed(() => {
const today = new Date()
const getDateTimestamp = (date) => {
date.setHours(0, 0, 0, 0)
return date.getTime()
}
return [
{
label: '今天',
value: getDateTimestamp(new Date(today)),
mode: 'date',
},
{
label: '昨天',
value: getDateTimestamp(new Date(today.getTime() - 24 * 60 * 60 * 1000)),
mode: 'date',
},
{
label: '本周',
value: [
getDateTimestamp(
new Date(today.getTime() - today.getDay() * 24 * 60 * 60 * 1000)
),
getDateTimestamp(new Date(today)),
],
mode: 'range',
},
{
label: '本月',
value: [
getDateTimestamp(new Date(today.getFullYear(), today.getMonth(), 1)),
getDateTimestamp(new Date(today)),
],
mode: 'range',
},
{
label: '本季度',
value: [
getDateTimestamp(
new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 1)
),
getDateTimestamp(new Date(today)),
],
mode: 'range',
},
]
})
// 根据报表类型确定日期选择器模式
const datePickerMode = computed(() => {
const { reportType } = queryForm.value
if (reportType === 'daily') return 'date'
if (reportType === 'custom') return 'daterange'
return 'daterange'
})
const startPlaceholder = computed(() => {
const { reportType } = queryForm.value
const placeholders = {
weekly: '选择周开始日期',
monthly: '选择月份开始',
quarterly: '选择季度开始',
yearly: '选择年份开始',
custom: '自定义开始日期',
}
return placeholders[reportType] || '开始日期'
})
const endPlaceholder = computed(() => {
const { reportType } = queryForm.value
const placeholders = {
weekly: '选择周结束日期',
monthly: '选择月份结束',
quarterly: '选择季度结束',
yearly: '选择年份结束',
custom: '自定义结束日期',
}
return placeholders[reportType] || '结束日期'
})
const singlePlaceholder = computed(() => {
return '请选择日期'
})
const reportPeriod = computed(() => {
if (!queryForm.value.dateValue) return ''
if (Array.isArray(queryForm.value.dateValue)) {
const [start, end] = queryForm.value.dateValue
return `${formatDate(start)} 至 ${formatDate(end)}`
} else {
return formatDate(queryForm.value.dateValue)
}
})
const reportColumns = [
{ title: '日期', key: 'date' },
{
title: '收入',
key: 'income',
render: (row) => `¥${row.income.toFixed(2)}`,
},
{
title: '支出',
key: 'expense',
render: (row) => `¥${row.expense.toFixed(2)}`,
},
{
title: '净利润',
key: 'profit',
render: (row) => `¥${row.profit.toFixed(2)}`,
},
]
const pagination = {
pageSize: 10,
}
const handleReportTypeChange = (value) => {
const reportType = reportTypes.find((t) => t.value === value)
queryForm.value.reportTypeName = reportType?.label || ''
queryForm.value.dateValue = null
}
const handleDateChange = (value) => {
queryForm.value.dateValue = value
}
const handleQuickDate = (quick) => {
queryForm.value.dateValue = quick.value
// 如果需要,可以自动触发查询
handleQuery()
}
const handleQuery = async () => {
if (!queryForm.value.dateValue) {
$message.warning('请选择查询时间')
return
}
querying.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 1000))
// 模拟报表数据
reportData.value = {
summary: [
{ label: '总收入', value: 125680.5 },
{ label: '总支出', value: 89420.3 },
{ label: '净利润', value: 36260.2 },
{ label: '利润率', value: 28.86 },
],
details: generateMockReportData(),
}
$message.success('报表查询成功')
} catch (error) {
$message.error('报表查询失败')
} finally {
querying.value = false
}
}
const handleExport = () => {
// 导出逻辑
$message.success('报表导出成功')
}
const generateMockReportData = () => {
// 生成模拟报表数据
const data = []
for (let i = 0; i < 15; i++) {
const income = Math.random() * 10000 + 5000
const expense = Math.random() * 8000 + 3000
data.push({
date: formatDate(Date.now() - i * 24 * 60 * 60 * 1000),
income,
expense,
profit: income - expense,
})
}
return data.reverse()
}
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleDateString('zh-CN')
}
</script>
<style scoped>
.financial-report {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.query-form {
margin-bottom: 24px;
}
.form-row {
display: flex;
gap: 16px;
align-items: end;
margin-bottom: 16px;
}
.form-row .n-form-item {
margin-bottom: 0;
}
.quick-dates {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #fafafa;
border-radius: 6px;
}
.quick-label {
font-size: 14px;
color: #666;
margin-right: 8px;
}
.report-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.report-result {
margin-top: 24px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
场景 4: 任务进度跟踪
vue
<template>
<div class="task-tracking">
<h3>任务进度跟踪</h3>
<n-card>
<div class="task-form">
<n-form :model="taskForm" label-placement="left" label-width="100px">
<n-form-item label="任务名称">
<n-input
v-model:value="taskForm.name"
placeholder="请输入任务名称"
/>
</n-form-item>
<n-form-item label="计划时间">
<C_Date
mode="smart-range"
start-placeholder="计划开始时间"
end-placeholder="计划结束时间"
:disabled-before-today="false"
@update:smartRange="handlePlannedDateChange"
/>
</n-form-item>
<n-form-item label="实际时间">
<C_Date
mode="smart-range"
start-placeholder="实际开始时间"
end-placeholder="实际结束时间"
:disabled-before-today="false"
:start-date-props="actualStartDateProps"
:end-date-props="actualEndDateProps"
@update:smartRange="handleActualDateChange"
/>
</n-form-item>
<n-form-item label="任务状态">
<n-select
v-model:value="taskForm.status"
:options="taskStatusOptions"
placeholder="请选择任务状态"
/>
</n-form-item>
<n-form-item label="完成进度">
<n-slider
v-model:value="taskForm.progress"
:step="5"
:marks="progressMarks"
:tooltip="false"
/>
<span class="progress-text">{{ taskForm.progress }}%</span>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleUpdateTask"
>更新任务</n-button
>
<n-button @click="handleAnalyzeTask" :disabled="!canAnalyze"
>分析进度</n-button
>
</n-form-item>
</n-form>
</div>
<!-- 任务分析结果 -->
<div v-if="taskAnalysis" class="task-analysis">
<n-divider title-placement="left">任务分析</n-divider>
<div class="analysis-grid">
<n-card size="small" title="时间对比">
<div class="time-comparison">
<div class="time-item">
<span class="label">计划用时:</span>
<span class="value">{{ taskAnalysis.plannedDuration }} 天</span>
</div>
<div class="time-item">
<span class="label">实际用时:</span>
<span class="value">{{ taskAnalysis.actualDuration }} 天</span>
</div>
<div class="time-item">
<span class="label">时间偏差:</span>
<span :class="['value', taskAnalysis.timeVariance.type]">
{{ taskAnalysis.timeVariance.text }}
</span>
</div>
</div>
</n-card>
<n-card size="small" title="进度状态">
<div class="progress-status">
<n-progress
type="circle"
:percentage="taskForm.progress"
:status="getProgressStatus()"
/>
<div class="status-info">
<p>当前状态: {{ getStatusText(taskForm.status) }}</p>
<p>预计完成: {{ taskAnalysis.estimatedCompletion }}</p>
</div>
</div>
</n-card>
<n-card size="small" title="风险评估">
<div class="risk-assessment">
<n-tag :type="taskAnalysis.riskLevel.type" size="large">
{{ taskAnalysis.riskLevel.text }}
</n-tag>
<p class="risk-description">{{ taskAnalysis.riskDescription }}</p>
</div>
</n-card>
</div>
</div>
</n-card>
</div>
</template>
<script setup>
const taskForm = ref({
name: '',
plannedDate: null,
actualDate: null,
status: 'not-started',
progress: 0,
})
const taskAnalysis = ref(null)
const taskStatusOptions = [
{ label: '未开始', value: 'not-started' },
{ label: '进行中', value: 'in-progress' },
{ label: '已暂停', value: 'paused' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' },
]
const progressMarks = {
0: '0%',
25: '25%',
50: '50%',
75: '75%',
100: '100%',
}
// 实际开始时间限制
const actualStartDateProps = computed(() => ({
isDateDisabled: (timestamp) => {
// 实际开始时间不能早于计划开始时间
if (taskForm.value.plannedDate && taskForm.value.plannedDate[0]) {
return timestamp < taskForm.value.plannedDate[0]
}
return false
},
}))
// 实际结束时间限制
const actualEndDateProps = computed(() => ({
isDateDisabled: (timestamp) => {
// 实际结束时间不能早于实际开始时间
if (taskForm.value.actualDate && taskForm.value.actualDate[0]) {
return timestamp < taskForm.value.actualDate[0]
}
return false
},
}))
const canAnalyze = computed(() => {
return (
taskForm.value.plannedDate &&
taskForm.value.actualDate &&
taskForm.value.actualDate[0]
)
})
const handlePlannedDateChange = (value) => {
taskForm.value.plannedDate = value
// 重置实际时间
taskForm.value.actualDate = null
}
const handleActualDateChange = (value) => {
taskForm.value.actualDate = value
}
const handleUpdateTask = () => {
if (!taskForm.value.name || !taskForm.value.plannedDate) {
$message.warning('请填写任务名称和计划时间')
return
}
$message.success('任务更新成功')
// 如果可以分析,自动触发分析
if (canAnalyze.value) {
handleAnalyzeTask()
}
}
const handleAnalyzeTask = () => {
if (!canAnalyze.value) {
$message.warning('请先填写完整的时间信息')
return
}
const { plannedDate, actualDate, progress, status } = taskForm.value
// 计算时间对比
const plannedDuration =
Math.ceil((plannedDate[1] - plannedDate[0]) / (1000 * 60 * 60 * 24)) + 1
const actualStart = actualDate[0]
const actualEnd = actualDate[1] || Date.now()
const actualDuration =
Math.ceil((actualEnd - actualStart) / (1000 * 60 * 60 * 24)) + 1
const timeVariance = calculateTimeVariance(plannedDuration, actualDuration)
const riskLevel = assessRisk(progress, status, timeVariance.value)
const estimatedCompletion = calculateEstimatedCompletion(
actualStart,
progress
)
taskAnalysis.value = {
plannedDuration,
actualDuration,
timeVariance,
riskLevel,
riskDescription: getRiskDescription(riskLevel),
estimatedCompletion,
}
}
const calculateTimeVariance = (planned, actual) => {
const variance = actual - planned
const percentage = ((variance / planned) * 100).toFixed(1)
if (variance > 0) {
return {
value: variance,
type: 'error',
text: `延期 ${variance} 天 (+${percentage}%)`,
}
} else if (variance < 0) {
return {
value: variance,
type: 'success',
text: `提前 ${Math.abs(variance)} 天 (${percentage}%)`,
}
} else {
return {
value: 0,
type: 'info',
text: '按期进行',
}
}
}
const assessRisk = (progress, status, timeVariance) => {
if (status === 'completed') {
return { type: 'success', text: '已完成' }
}
if (status === 'cancelled') {
return { type: 'error', text: '已取消' }
}
if (timeVariance > 5 || progress < 30) {
return { type: 'error', text: '高风险' }
} else if (timeVariance > 2 || progress < 50) {
return { type: 'warning', text: '中等风险' }
} else {
return { type: 'success', text: '低风险' }
}
}
const getRiskDescription = (riskLevel) => {
const descriptions = {
高风险: '任务存在较大延期风险,建议调整计划或增加资源',
中等风险: '任务进度稍有滞后,需要密切关注',
低风险: '任务进展良好,按计划进行',
已完成: '任务已成功完成',
已取消: '任务已被取消',
}
return descriptions[riskLevel.text] || ''
}
const calculateEstimatedCompletion = (actualStart, progress) => {
if (progress >= 100) return '已完成'
if (progress === 0) return '未开始'
const now = Date.now()
const daysPassed = Math.ceil((now - actualStart) / (1000 * 60 * 60 * 24))
const totalEstimatedDays = Math.ceil((daysPassed / progress) * 100)
const remainingDays = totalEstimatedDays - daysPassed
const estimatedDate = new Date(now + remainingDays * 24 * 60 * 60 * 1000)
return estimatedDate.toLocaleDateString('zh-CN')
}
const getProgressStatus = () => {
const { progress, status } = taskForm.value
if (status === 'completed') return 'success'
if (status === 'cancelled') return 'error'
if (progress < 30) return 'error'
if (progress < 70) return 'warning'
return 'info'
}
const getStatusText = (status) => {
return (
taskStatusOptions.find((option) => option.value === status)?.label || '未知'
)
}
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleDateString('zh-CN')
}
</script>
<style scoped>
.task-tracking {
max-width: 1000px;
margin: 0 auto;
padding: 24px;
}
.task-form {
margin-bottom: 24px;
}
.progress-text {
margin-left: 12px;
font-weight: bold;
color: #1890ff;
}
.analysis-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.time-comparison {
display: flex;
flex-direction: column;
gap: 8px;
}
.time-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.label {
color: #666;
}
.value {
font-weight: bold;
&.success {
color: #52c41a;
}
&.error {
color: #ff4d4f;
}
&.info {
color: #1890ff;
}
}
.progress-status {
display: flex;
align-items: center;
gap: 16px;
}
.status-info p {
margin: 4px 0;
}
.risk-assessment {
text-align: center;
}
.risk-description {
margin-top: 12px;
color: #666;
font-size: 14px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
🛠️ 高级用法
动态禁用规则
vue
<template>
<div class="dynamic-disable">
<n-switch v-model:value="weekendsOnly" @update:value="handleWeekendToggle">
<template #checked>仅工作日</template>
<template #unchecked>全部日期</template>
</n-switch>
<C_Date
mode="daterange"
:start-date-props="dynamicStartProps"
:end-date-props="dynamicEndProps"
@change="handleDateChange"
/>
</div>
</template>
<script setup>
const weekendsOnly = ref(false)
const dynamicStartProps = computed(() => ({
isDateDisabled: (timestamp) => {
if (!weekendsOnly.value) return false
const date = new Date(timestamp)
const dayOfWeek = date.getDay()
// 禁用周末(0=周日, 6=周六)
return dayOfWeek === 0 || dayOfWeek === 6
},
}))
const dynamicEndProps = computed(() => ({
isDateDisabled: (timestamp) => {
if (!weekendsOnly.value) return false
const date = new Date(timestamp)
const dayOfWeek = date.getDay()
return dayOfWeek === 0 || dayOfWeek === 6
},
}))
const handleWeekendToggle = (value) => {
console.log('工作日模式:', value)
}
const handleDateChange = (value) => {
console.log('日期变更:', value)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
自定义格式化
vue
<template>
<div class="custom-format">
<n-radio-group
v-model:value="dateFormat"
@update:value="handleFormatChange"
>
<n-radio value="yyyy-MM-dd">标准格式 (2024-01-01)</n-radio>
<n-radio value="yyyy/MM/dd">斜杠格式 (2024/01/01)</n-radio>
<n-radio value="MM-dd-yyyy">美式格式 (01-01-2024)</n-radio>
<n-radio value="dd/MM/yyyy">欧式格式 (01/01/2024)</n-radio>
</n-radio-group>
<C_Date
mode="daterange"
:value-format="dateFormat"
@change="handleDateChange"
/>
<p v-if="formattedResult">格式化结果: {{ formattedResult }}</p>
</div>
</template>
<script setup>
const dateFormat = ref('yyyy-MM-dd')
const formattedResult = ref('')
const handleFormatChange = (format) => {
console.log('格式变更:', format)
}
const handleDateChange = (value) => {
if (value && Array.isArray(value)) {
const [start, end] = value
formattedResult.value = `${formatTimestamp(start)} 至 ${formatTimestamp(
end
)}`
}
}
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp)
return date.toLocaleDateString('zh-CN')
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
联动多个日期选择器
vue
<template>
<div class="cascading-dates">
<h4>项目阶段时间设置</h4>
<div class="phase-dates">
<div class="phase-item">
<label>需求分析阶段</label>
<C_Date
mode="smart-range"
start-placeholder="需求分析开始"
end-placeholder="需求分析结束"
:disabled-before-today="true"
@update:smartRange="handlePhase1Change"
/>
</div>
<div class="phase-item">
<label>开发阶段</label>
<C_Date
mode="smart-range"
start-placeholder="开发开始"
end-placeholder="开发结束"
:start-date-props="phase2StartProps"
:end-date-props="phase2EndProps"
@update:smartRange="handlePhase2Change"
/>
</div>
<div class="phase-item">
<label>测试阶段</label>
<C_Date
mode="smart-range"
start-placeholder="测试开始"
end-placeholder="测试结束"
:start-date-props="phase3StartProps"
:end-date-props="phase3EndProps"
@update:smartRange="handlePhase3Change"
/>
</div>
</div>
<!-- 时间线预览 -->
<div v-if="hasAllPhases" class="timeline-preview">
<h5>项目时间线预览</h5>
<div class="timeline">
<div
v-for="phase in phaseTimeline"
:key="phase.name"
class="timeline-item"
>
<div class="phase-name">{{ phase.name }}</div>
<div class="phase-duration">{{ phase.duration }}</div>
<div class="phase-dates">{{ phase.dates }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const phase1Date = ref(null) // 需求分析
const phase2Date = ref(null) // 开发
const phase3Date = ref(null) // 测试
// 开发阶段不能早于需求分析结束
const phase2StartProps = computed(() => ({
isDateDisabled: (timestamp) => {
if (!phase1Date.value || !phase1Date.value[1]) return false
return timestamp < phase1Date.value[1]
},
}))
const phase2EndProps = computed(() => ({
isDateDisabled: (timestamp) => {
// 开发结束时间限制逻辑
return false
},
}))
// 测试阶段不能早于开发结束
const phase3StartProps = computed(() => ({
isDateDisabled: (timestamp) => {
if (!phase2Date.value || !phase2Date.value[1]) return false
return timestamp < phase2Date.value[1]
},
}))
const phase3EndProps = computed(() => ({
isDateDisabled: (timestamp) => {
// 测试结束时间限制逻辑
return false
},
}))
const hasAllPhases = computed(() => {
return phase1Date.value && phase2Date.value && phase3Date.value
})
const phaseTimeline = computed(() => {
if (!hasAllPhases.value) return []
return [
{
name: '需求分析',
duration: calculateDuration(phase1Date.value),
dates: formatDateRange(phase1Date.value),
},
{
name: '开发阶段',
duration: calculateDuration(phase2Date.value),
dates: formatDateRange(phase2Date.value),
},
{
name: '测试阶段',
duration: calculateDuration(phase3Date.value),
dates: formatDateRange(phase3Date.value),
},
]
})
const handlePhase1Change = (value) => {
phase1Date.value = value
// 重置后续阶段
if (value) {
phase2Date.value = null
phase3Date.value = null
}
}
const handlePhase2Change = (value) => {
phase2Date.value = value
// 重置测试阶段
if (value) {
phase3Date.value = null
}
}
const handlePhase3Change = (value) => {
phase3Date.value = value
}
const calculateDuration = (dateRange) => {
if (!dateRange) return '未设置'
const [start, end] = dateRange
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
return `${days} 天`
}
const formatDateRange = (dateRange) => {
if (!dateRange) return '未设置'
const [start, end] = dateRange
return `${formatDate(start)} - ${formatDate(end)}`
}
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleDateString('zh-CN')
}
</script>
<style scoped>
.cascading-dates {
max-width: 600px;
}
.phase-dates {
display: flex;
flex-direction: column;
gap: 16px;
margin: 16px 0;
}
.phase-item {
display: flex;
align-items: center;
gap: 12px;
}
.phase-item label {
min-width: 100px;
font-weight: 500;
}
.timeline-preview {
margin-top: 24px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.timeline {
display: flex;
flex-direction: column;
gap: 8px;
}
.timeline-item {
display: grid;
grid-template-columns: 100px 80px 1fr;
gap: 12px;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e8e8e8;
}
.phase-name {
font-weight: 500;
}
.phase-duration {
color: #1890ff;
font-weight: 500;
}
.phase-dates {
color: #666;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
🔧 自定义样式
CSS 变量
scss
.c-date-wrapper {
--date-primary-color: #1890ff;
--date-border-color: #d9d9d9;
--date-hover-border-color: #40a9ff;
--date-focus-border-color: #1890ff;
--date-disabled-bg-color: #f5f5f5;
--date-disabled-text-color: #ccc;
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
响应式布局
vue
<template>
<C_Date mode="smart-range" class="responsive-date" />
</template>
<style scoped>
.responsive-date {
width: 100%;
:deep(.flex) {
@media (max-width: 768px) {
flex-direction: column;
gap: 8px;
}
}
:deep(.flex-1) {
@media (max-width: 768px) {
width: 100%;
}
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
自定义间距样式
vue
<template>
<C_Date mode="smart-range" class="custom-spacing" />
</template>
<style scoped>
.custom-spacing :deep(.gap-2\.5) {
gap: 16px;
@media (max-width: 768px) {
gap: 12px;
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
⚠️ 注意事项
1. 模式选择
vue
<!-- ✅ 推荐:根据需求选择合适的模式 -->
<C_Date mode="smart-range" />
<!-- 需要联动限制时 -->
<C_Date mode="daterange" />
<!-- 简单范围选择时 -->
<!-- ❌ 不推荐:所有场景都使用同一种模式 -->
<C_Date mode="daterange" />
<!-- 所有场景 -->
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
2. 禁用规则设置
vue
<!-- ✅ 推荐:合理设置禁用规则 -->
<C_Date
:disabled-before-today="true" <!-- 预约类业务 -->
:disabled-after-today="false"
/>
<!-- ❌ 不推荐:过度限制 -->
<C_Date
:disabled-before-today="true"
:disabled-after-today="true" <!-- 只能选今天 -->
/>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
3. 事件监听
vue
<!-- ✅ 推荐:监听具体事件 -->
<C_Date
mode="smart-range"
@update:smartRange="handleSmartRange"
/>
<!-- ❌ 不推荐:只监听通用事件 -->
<C_Date
mode="smart-range"
@change="handleChange" <!-- 不如具体事件清晰 -->
/>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
🐛 故障排除
常见问题
Q1: 智能范围模式结束日期无法选择?
A1: 检查以下几点:
- 确认已选择开始日期
- 检查禁用规则设置
- 确认事件监听正确
vue
<!-- 确保正确配置 -->
<C_Date mode="smart-range" @update:smartRange="handleSmartRange" />
<script setup>
const handleSmartRange = (value) => {
console.log('智能范围变更:', value)
// 确保事件处理函数存在
}
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Q2: 日期格式显示不正确?
A2: 检查 valueFormat 设置:
vue
<!-- ✅ 正确的格式设置 -->
<C_Date
mode="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
/>
<!-- ❌ 格式字符串错误 -->
<C_Date
mode="datetime"
value-format="YYYY-mm-dd" <!-- 应该是 yyyy-MM-dd -->
/>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Q3: 禁用规则不生效?
A3: 确保使用正确的属性:
vue
<template>
<C_Date
mode="smart-range"
:start-date-props="startProps"
:end-date-props="endProps"
/>
</template>
<script setup>
const startProps = computed(() => ({
isDateDisabled: (timestamp) => {
// 禁用逻辑
return false
},
}))
const endProps = computed(() => ({
isDateDisabled: (timestamp) => {
// 禁用逻辑
return false
},
}))
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Q4: 清空功能不工作?
A4: 使用暴露的 clearAll 方法:
vue
<template>
<C_Date ref="datePickerRef" />
<n-button @click="handleClear">清空日期</n-button>
</template>
<script setup>
const datePickerRef = ref()
const handleClear = () => {
datePickerRef.value?.clearAll()
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
🎯 最佳实践
1. 根据业务场景选择模式
vue
<!-- ✅ 推荐:根据业务特点选择 -->
<!-- 预约业务:使用智能范围,禁用过去日期 -->
<C_Date mode="smart-range" :disabled-before-today="true" />
<!-- 报表查询:使用普通范围,允许历史日期 -->
<C_Date mode="daterange" :disabled-after-today="true" />
<!-- 生日选择:使用单日期,禁用未来日期 -->
<C_Date mode="date" :disabled-after-today="true" />
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
2. 合理的默认值设置
vue
<script setup>
const getIntelligentDefaults = () => {
const now = new Date()
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000)
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
// 根据业务类型设置不同的默认值
return {
booking: [tomorrow.getTime(), nextWeek.getTime()], // 预约:明天到下周
report: [
new Date(now.getFullYear(), now.getMonth(), 1).getTime(),
now.getTime(),
], // 报表:本月
vacation: null, // 假期:无默认值,用户自选
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3. 表单验证集成
vue
<template>
<n-form :model="formData" :rules="dateRules">
<n-form-item label="活动时间" path="eventDate">
<C_Date mode="smart-range" @update:smartRange="handleEventDateChange" />
</n-form-item>
</n-form>
</template>
<script setup>
const formData = ref({
eventDate: null,
})
const dateRules = {
eventDate: {
validator: (rule, value) => {
if (!value || !Array.isArray(value) || value.length !== 2) {
return new Error('请选择完整的活动时间')
}
const [start, end] = value
const duration = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
if (duration < 1) {
return new Error('活动时间至少为1天')
}
if (duration > 30) {
return new Error('活动时间不能超过30天')
}
return true
},
trigger: 'change',
},
}
const handleEventDateChange = (value) => {
formData.value.eventDate = value
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
4. 性能优化
vue
<script setup>
// 使用计算属性优化禁用函数
const disabledDateCache = new Map()
const optimizedDisabledDate = computed(() => {
return (timestamp) => {
// 使用缓存避免重复计算
if (disabledDateCache.has(timestamp)) {
return disabledDateCache.get(timestamp)
}
const result = calculateDisabledDate(timestamp)
disabledDateCache.set(timestamp, result)
return result
}
})
// 清理缓存
onUnmounted(() => {
disabledDateCache.clear()
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
📝 更新日志
v1.0.0 (2025-07-14)
- ✨ 支持五种日期选择模式
- ✨ 智能联动限制功能
- ✨ 灵活的禁用规则配置
- ✨ 完整的 TypeScript 支持
- ✨ 丰富的事件系统
- ✨ 暴露清空方法和内部状态
🤝 贡献指南
- Fork 项目
- 创建功能分支 (
git checkout -b feature/amazing-feature
) - 提交更改 (
git commit -m 'Add amazing feature'
) - 推送到分支 (
git push origin feature/amazing-feature
) - 创建 Pull Request
📄 许可证
Copyright (c) 2025 by ChenYu, All Rights Reserved.
💡 提示: 这个组件设计用于团队协作,支持假期申请、活动报名、财务报表、任务跟踪等多种场景。如果遇到问题请先查看文档,或者在团队群里讨论。让我们一起打造更好的开发体验! 🚀