Are you an LLM? You can read better optimized documentation at /cat-kit/packages/core/date.md for this page in Markdown format
日期处理
Dater 类和 date 工厂函数提供日期的构造、格式化、对齐、比较与判定等能力,适用于浏览器和 Node.js 环境。
基础知识
- UTC:协调世界时,不带夏令时偏移的统一时间标准;本地时间 = UTC + 时区偏移。
- 时区与偏移:例如东八区为 UTC+8,西五区为 UTC-5;同一绝对时刻在不同地区呈现的本地时间不同。
- DST(夏令时):部分地区会在一年内切换偏移,导致“重复或缺失”小时,应尽量在运算中使用 UTC 避免歧义。
- ISO 8601:推荐的时间字符串格式,如
2024-03-01T12:00:00.000Z(Z 表示 UTC);带偏移示例2024-03-01T12:00:00+08:00。 - 时间戳:通常指自 1970-01-01T00:00:00Z 起的毫秒/秒计数(Unix 时间);与时区无关,适合存储和比较。
数据库存储建议
- 统一存储为 UTC,读取后按需要再转换为本地时区,避免跨地区/夏令时误差。
- 推荐格式
- 文本:ISO 8601 UTC 字符串(带
Z),便于跨语言解析与排序。 - 数值:Unix 时间戳(秒或毫秒),类型可用
BIGINT(毫秒更精确,需前端/后端一致约定)。
- 文本:ISO 8601 UTC 字符串(带
- 字段类型示例
- PostgreSQL:
timestamptz(存储时自动归一到 UTC,查询按会话时区展示)。 - MySQL/MariaDB:
timestamp默认以 UTC 存储;如用datetime请确保写入即为 UTC。
- PostgreSQL:
- 避免做法
- 存本地时间且不带偏移信息(会在迁移/跨区时产生歧义)。
- 将字符串与时间戳混用而无明确约定。
- 在数据库层做 DST 相关加减法,建议先在应用层转换到 UTC 再入库。
快速上手
ts
import { date, Dater } from '@cat-kit/core'
const d = date('2024-01-15 10:30:45')
d.format('yyyy-MM-dd HH:mm:ss') // 2024-01-15 10:30:45
d.clone().addDays(1).startOf('day').format() // 2024-01-161
2
3
4
5
6
2
3
4
5
6
构造与克隆
- 接受
number | string | Date | Dater,不传则使用当前时间。 - 无效输入行为与
new Date一致:返回Invalid Date(时间戳为NaN),不会抛错。 clone()生成独立实例,避免共享可变引用。
ts
const d1 = new Dater('2024-01-15')
const d2 = date(1700000000000)
const copy = d1.clone()1
2
3
2
3
获取属性
ts
const d = date('2024-01-15 10:30:45')
d.year // 2024
d.month // 1 (从 1 开始)
d.day // 15
d.weekDay // 1 (周一,0=周日)
d.hours // 10
d.minutes // 30
d.seconds // 45
d.timestamp // 毫秒时间戳
d.raw // 原生 Date1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
修改日期
- 可变:
setYear、setMonth、setDay、setHours、setMinutes、setSeconds、setTime,返回自身可链式调用。 - 不可变:
addDays、addWeeks、addMonths、addYears、calc返回新实例;clone可配合使用。
ts
const base = date('2024-01-15')
base.setYear(2025).setMonth(6).setDay(20)
base.format() // 2025-06-20
const nextWeek = base.clone().addWeeks(1) // base 未被修改1
2
3
4
5
6
2
3
4
5
6
格式化
vue
📅 日期格式化演示
选择日期和格式模板,实时查看格式化结果
格式化结果:
2025-12-29 08:59:00代码:
date('2025-12-29T08:59').format('yyyy-MM-dd HH:mm:ss')常用格式模板
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
<template>
<div class="demo-box">
<div class="demo-section">
<h4>📅 日期格式化演示</h4>
<p class="demo-desc">选择日期和格式模板,实时查看格式化结果</p>
</div>
<div class="demo-controls">
<div class="control-group">
<label>选择日期</label>
<input type="datetime-local" v-model="dateInput" class="demo-input" />
</div>
<div class="control-group">
<label>格式模板</label>
<select v-model="formatTemplate" class="demo-select">
<option v-for="fmt in formats" :key="fmt.value" :value="fmt.value">
{{ fmt.label }}
</option>
</select>
</div>
<div class="control-group">
<label class="checkbox-label">
<input type="checkbox" v-model="useUTC" />
<span>使用 UTC 时间</span>
</label>
</div>
</div>
<div class="demo-result">
<div class="result-row">
<span class="result-label">格式化结果:</span>
<code class="result-value">{{ formattedResult }}</code>
</div>
<div class="result-row code-preview">
<span class="result-label">代码:</span>
<code class="result-code">date('{{ dateInput }}').format('{{ formatTemplate }}'{{ useUTC ? ", { utc: true }" : "" }})</code>
</div>
</div>
<div class="demo-formats">
<h5>常用格式模板</h5>
<div class="format-chips">
<button
v-for="fmt in formats"
:key="fmt.value"
:class="['format-chip', { active: formatTemplate === fmt.value }]"
@click="formatTemplate = fmt.value"
>
{{ fmt.value }}
</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { date } from '@cat-kit/core/src'
const dateInput = ref(new Date().toISOString().slice(0, 16))
const formatTemplate = ref('yyyy-MM-dd HH:mm:ss')
const useUTC = ref(false)
const formats = [
{ label: '完整日期时间', value: 'yyyy-MM-dd HH:mm:ss' },
{ label: '日期 (斜杠)', value: 'yyyy/MM/dd' },
{ label: '日期 (中文)', value: 'yyyy年M月d日' },
{ label: '时间 (24小时)', value: 'HH:mm:ss' },
{ label: '时间 (12小时)', value: 'hh:mm:ss' },
{ label: '年月', value: 'yyyy-MM' },
{ label: '月日', value: 'MM-dd' },
]
const formattedResult = computed(() => {
try {
const d = date(dateInput.value)
return d.format(formatTemplate.value, { utc: useUTC.value })
} catch {
return '无效日期'
}
})
</script>
<style scoped>
.demo-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-section h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.demo-desc {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-group label {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.demo-input,
.demo-select {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
min-width: 200px;
}
.demo-input:focus,
.demo-select:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
flex-direction: row !important;
padding-top: 8px;
}
.checkbox-label input {
width: 16px;
height: 16px;
accent-color: var(--vp-c-brand-1);
}
.demo-result {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.result-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.result-label {
font-size: 13px;
color: var(--vp-c-text-2);
min-width: 80px;
}
.result-value {
font-size: 18px;
font-weight: 600;
color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
padding: 4px 12px;
border-radius: 4px;
}
.result-code {
font-size: 13px;
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.demo-formats h5 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.format-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.format-chip {
padding: 4px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-size: 12px;
font-family: var(--vp-font-family-mono);
cursor: pointer;
transition: all 0.2s;
}
.format-chip:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.format-chip.active {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
</style>
ts
const d = date('2024-01-15 14:30:45')
d.format() // 默认 'yyyy-MM-dd'
d.format('yyyy/MM/dd') // 2024/01/15
d.format('yyyy年M月d日') // 2024年1月15日
d.format('hh:mm') // 02:30 (12 小时制)
d.format('yyyy-MM-dd HH:mm:ss', { utc: true }) // 按 UTC 输出1
2
3
4
5
6
7
2
3
4
5
6
7
占位符
| 占位符 | 说明 | 示例 |
|---|---|---|
yyyy/YYYY | 4 位年份 | 2024 |
M/MM | 月份(1-12) | 1 / 01 |
d/dd/D/DD | 日期(1-31) | 5 / 05 |
H/HH | 24 小时制 | 0 / 00 |
h/hh | 12 小时制 | 1 / 01 |
m/mm | 分钟 | 3 / 03 |
s/ss | 秒 | 7 / 07 |
解析
Dater.parse(value, format?, { utc? }) 使用与 format 相同的占位符模板解析字符串;未传模板时等价于 new Dater(value)。
- 当提供
format时会先按模板校验并提取字段,未匹配成功直接返回Invalid Date。 - 模板缺失的字段使用默认值:年=当前年、月=1、日=1、时分秒=0。
- 解析结果会进行溢出校验(例如 2024-02-30 会返回
Invalid Date而不是自动进位)。
vue
🔍 日期解析演示
输入日期字符串和格式模板,查看 Dater.parse() 的解析结果
示例预设
✅ 解析成功
年份
2024月份
3日期
15小时
14分钟
30秒
0时间戳:
1710513000000格式化输出:
2024-03-15 14:30:00代码:
Dater.parse('2024-03-15 14:30:00', 'yyyy-MM-dd HH:mm:ss')💡 常见问题
2024-02-30→ 无效日期(2月没有30日)2024-13-01→ 无效日期(没有13月)- 格式模板与字符串不匹配 → 无效日期
- 无效日期不会抛错,需检查
timestamp是否为NaN
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
<template>
<div class="demo-box">
<div class="demo-section">
<h4>🔍 日期解析演示</h4>
<p class="demo-desc">输入日期字符串和格式模板,查看 Dater.parse() 的解析结果</p>
</div>
<div class="demo-controls">
<div class="control-group">
<label>日期字符串</label>
<input type="text" v-model="dateString" class="demo-input wide" placeholder="例如: 2024-03-15 14:30:00" />
</div>
<div class="control-group">
<label>格式模板</label>
<input type="text" v-model="formatTemplate" class="demo-input wide" placeholder="例如: yyyy-MM-dd HH:mm:ss" />
</div>
<div class="control-group">
<label class="checkbox-label">
<input type="checkbox" v-model="useUTC" />
<span>按 UTC 解析</span>
</label>
</div>
</div>
<div class="preset-section">
<h5>示例预设</h5>
<div class="preset-chips">
<button
v-for="preset in presets"
:key="preset.label"
class="preset-chip"
@click="applyPreset(preset)"
>
{{ preset.label }}
</button>
</div>
</div>
<div :class="['demo-result', { error: !isValid }]">
<div v-if="isValid" class="parse-success">
<h5>✅ 解析成功</h5>
<div class="parsed-grid">
<div class="parsed-item">
<span class="parsed-label">年份</span>
<code class="parsed-value">{{ parsedDate?.year }}</code>
</div>
<div class="parsed-item">
<span class="parsed-label">月份</span>
<code class="parsed-value">{{ parsedDate?.month }}</code>
</div>
<div class="parsed-item">
<span class="parsed-label">日期</span>
<code class="parsed-value">{{ parsedDate?.day }}</code>
</div>
<div class="parsed-item">
<span class="parsed-label">小时</span>
<code class="parsed-value">{{ parsedDate?.hours }}</code>
</div>
<div class="parsed-item">
<span class="parsed-label">分钟</span>
<code class="parsed-value">{{ parsedDate?.minutes }}</code>
</div>
<div class="parsed-item">
<span class="parsed-label">秒</span>
<code class="parsed-value">{{ parsedDate?.seconds }}</code>
</div>
</div>
<div class="parsed-extra">
<div class="extra-item">
<span class="extra-label">时间戳:</span>
<code class="extra-value">{{ parsedDate?.timestamp }}</code>
</div>
<div class="extra-item">
<span class="extra-label">格式化输出:</span>
<code class="extra-value">{{ formattedOutput }}</code>
</div>
</div>
</div>
<div v-else class="parse-error">
<h5>❌ 解析失败</h5>
<p class="error-message">输入的字符串无法按照指定格式解析为有效日期</p>
<div class="error-details">
<div class="detail-item">
<span class="detail-label">timestamp:</span>
<code class="detail-value error">NaN</code>
</div>
<div class="detail-item">
<span class="detail-label">检测方法:</span>
<code class="detail-value">Number.isNaN(parsed.timestamp)</code>
</div>
</div>
</div>
</div>
<div class="code-preview">
<span class="code-label">代码:</span>
<code class="code-value">Dater.parse('{{ dateString }}', '{{ formatTemplate }}'{{ useUTC ? ", { utc: true }" : "" }})</code>
</div>
<div class="tips-section">
<h5>💡 常见问题</h5>
<ul class="tips-list">
<li><code>2024-02-30</code> → 无效日期(2月没有30日)</li>
<li><code>2024-13-01</code> → 无效日期(没有13月)</li>
<li>格式模板与字符串不匹配 → 无效日期</li>
<li>无效日期不会抛错,需检查 <code>timestamp</code> 是否为 <code>NaN</code></li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { Dater } from '@cat-kit/core/src'
const dateString = ref('2024-03-15 14:30:00')
const formatTemplate = ref('yyyy-MM-dd HH:mm:ss')
const useUTC = ref(false)
interface Preset {
label: string
date: string
format: string
}
const presets: Preset[] = [
{ label: '标准日期时间', date: '2024-03-15 14:30:00', format: 'yyyy-MM-dd HH:mm:ss' },
{ label: '仅日期', date: '2024-03-15', format: 'yyyy-MM-dd' },
{ label: '中文格式', date: '2024年3月15日', format: 'yyyy年M月d日' },
{ label: '斜杠分隔', date: '2024/03/15', format: 'yyyy/MM/dd' },
{ label: '无效日期 (2月30日)', date: '2024-02-30', format: 'yyyy-MM-dd' },
{ label: '无效日期 (13月)', date: '2024-13-01', format: 'yyyy-MM-dd' },
]
function applyPreset(preset: Preset) {
dateString.value = preset.date
formatTemplate.value = preset.format
}
const parsedDater = computed(() => {
try {
return Dater.parse(dateString.value, formatTemplate.value, { utc: useUTC.value })
} catch {
return null
}
})
const isValid = computed(() => {
if (!parsedDater.value) return false
return !Number.isNaN(parsedDater.value.timestamp)
})
const parsedDate = computed(() => {
if (!parsedDater.value || !isValid.value) return null
const d = parsedDater.value
return {
year: d.year,
month: d.month,
day: d.day,
hours: d.hours,
minutes: d.minutes,
seconds: d.seconds,
timestamp: d.timestamp,
}
})
const formattedOutput = computed(() => {
if (!parsedDater.value || !isValid.value) return ''
return parsedDater.value.format('yyyy-MM-dd HH:mm:ss')
})
</script>
<style scoped>
.demo-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-section h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.demo-desc {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-group label {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.demo-input {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
font-family: var(--vp-font-family-mono);
}
.demo-input.wide {
min-width: 250px;
}
.demo-input:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
flex-direction: row !important;
padding-top: 8px;
}
.checkbox-label input {
width: 16px;
height: 16px;
accent-color: var(--vp-c-brand-1);
}
.preset-section h5 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.preset-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.preset-chip {
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.preset-chip:hover {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.demo-result {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
}
.demo-result.error {
background: var(--vp-c-danger-soft);
}
.parse-success h5,
.parse-error h5 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
}
.parse-success h5 {
color: var(--vp-c-green-1);
}
.parse-error h5 {
color: var(--vp-c-danger-1);
}
.parsed-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
margin-bottom: 16px;
}
.parsed-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
text-align: center;
}
.parsed-label {
font-size: 12px;
color: var(--vp-c-text-2);
}
.parsed-value {
font-size: 18px;
font-weight: 600;
color: var(--vp-c-brand-1);
}
.parsed-extra {
display: flex;
flex-direction: column;
gap: 8px;
}
.extra-item {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.extra-label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.extra-value {
font-size: 13px;
background: var(--vp-c-bg);
padding: 4px 8px;
border-radius: 4px;
}
.error-message {
margin: 0 0 12px 0;
font-size: 14px;
color: var(--vp-c-danger-1);
}
.error-details {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
}
.detail-label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.detail-value {
font-size: 13px;
background: var(--vp-c-bg);
padding: 4px 8px;
border-radius: 4px;
}
.detail-value.error {
color: var(--vp-c-danger-1);
font-weight: 600;
}
.code-preview {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.code-label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.code-value {
font-size: 13px;
background: var(--vp-c-bg);
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.tips-section {
padding: 12px 16px;
background: var(--vp-c-tip-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-tip-1);
}
.tips-section h5 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-tip-1);
}
.tips-list {
margin: 0;
padding-left: 20px;
font-size: 13px;
color: var(--vp-c-text-2);
display: flex;
flex-direction: column;
gap: 4px;
}
.tips-list code {
font-size: 12px;
background: var(--vp-c-bg);
padding: 1px 4px;
border-radius: 3px;
}
</style>
ts
const parsed = Dater.parse('2024-03-05 14:20:10', 'yyyy-MM-dd HH:mm:ss')
parsed.format('yyyy/MM/dd HH:mm') // 2024/03/05 14:20
const utcParsed = Dater.parse('2024-01-01 00:00:00', 'yyyy-MM-dd HH:mm:ss', { utc: true })
utcParsed.format('yyyy-MM-dd HH:mm:ss', { utc: true }) // 2024-01-01 00:00:00
// 无效日期:timestamp 为 NaN(不抛错)
const invalid = Dater.parse('2024-02-30', 'yyyy-MM-dd')
Number.isNaN(invalid.timestamp) // true1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
计算与对齐
vue
🧮 日期计算演示
选择日期并进行加减运算,观察不可变操作特性
原始日期
2025-12-29+7 天
计算结果
2026-01-05代码:
date('2025-12-29').addDays(7)addDays() 返回新实例,原日期 不会 被修改快捷操作
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
<template>
<div class="demo-box">
<div class="demo-section">
<h4>🧮 日期计算演示</h4>
<p class="demo-desc">选择日期并进行加减运算,观察不可变操作特性</p>
</div>
<div class="demo-controls">
<div class="control-group">
<label>基准日期</label>
<input type="date" v-model="baseDate" class="demo-input" />
</div>
<div class="control-group">
<label>偏移量</label>
<input type="number" v-model.number="offset" class="demo-input number-input" />
</div>
<div class="control-group">
<label>单位</label>
<select v-model="unit" class="demo-select">
<option value="days">天 (Days)</option>
<option value="weeks">周 (Weeks)</option>
<option value="months">月 (Months)</option>
<option value="years">年 (Years)</option>
</select>
</div>
</div>
<div class="demo-result">
<div class="result-comparison">
<div class="result-item">
<span class="result-tag original">原始日期</span>
<code class="result-date">{{ formattedBase }}</code>
</div>
<div class="result-arrow">
<span>{{ offset >= 0 ? '+' : '' }}{{ offset }} {{ unitLabels[unit] }}</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
<div class="result-item">
<span class="result-tag calculated">计算结果</span>
<code class="result-date highlight">{{ calculatedResult }}</code>
</div>
</div>
<div class="result-row code-preview">
<span class="result-label">代码:</span>
<code class="result-code">date('{{ baseDate }}').{{ methodName }}({{ offset }})</code>
</div>
<div class="immutable-note">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
</svg>
<span><code>{{ methodName }}()</code> 返回新实例,原日期 <strong>不会</strong> 被修改</span>
</div>
</div>
<div class="quick-actions">
<h5>快捷操作</h5>
<div class="action-chips">
<button class="action-chip" @click="setQuickOffset(1, 'days')">+1 天</button>
<button class="action-chip" @click="setQuickOffset(7, 'days')">+7 天</button>
<button class="action-chip" @click="setQuickOffset(-7, 'days')">-7 天</button>
<button class="action-chip" @click="setQuickOffset(1, 'months')">+1 月</button>
<button class="action-chip" @click="setQuickOffset(-1, 'months')">-1 月</button>
<button class="action-chip" @click="setQuickOffset(1, 'years')">+1 年</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { date } from '@cat-kit/core/src'
const baseDate = ref(new Date().toISOString().slice(0, 10))
const offset = ref(7)
const unit = ref<'days' | 'weeks' | 'months' | 'years'>('days')
const unitLabels: Record<string, string> = {
days: '天',
weeks: '周',
months: '月',
years: '年'
}
const methodName = computed(() => {
const methods: Record<string, string> = {
days: 'addDays',
weeks: 'addWeeks',
months: 'addMonths',
years: 'addYears'
}
return methods[unit.value]
})
const formattedBase = computed(() => {
try {
return date(baseDate.value).format('yyyy-MM-dd')
} catch {
return '无效日期'
}
})
const calculatedResult = computed(() => {
try {
const d = date(baseDate.value)
let result: ReturnType<typeof date>
switch (unit.value) {
case 'days':
result = d.addDays(offset.value)
break
case 'weeks':
result = d.addWeeks(offset.value)
break
case 'months':
result = d.addMonths(offset.value)
break
case 'years':
result = d.addYears(offset.value)
break
default:
result = d
}
return result.format('yyyy-MM-dd')
} catch {
return '计算错误'
}
})
function setQuickOffset(value: number, unitType: 'days' | 'weeks' | 'months' | 'years') {
offset.value = value
unit.value = unitType
}
</script>
<style scoped>
.demo-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-section h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.demo-desc {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-group label {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.demo-input,
.demo-select {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
}
.number-input {
width: 100px;
}
.demo-input:focus,
.demo-select:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.demo-result {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.result-comparison {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.result-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.result-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
.result-tag.original {
background: var(--vp-c-default-soft);
color: var(--vp-c-text-2);
}
.result-tag.calculated {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.result-date {
font-size: 16px;
font-weight: 500;
padding: 8px 16px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.result-date.highlight {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.result-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: var(--vp-c-text-2);
font-size: 13px;
font-weight: 500;
}
.result-arrow svg {
color: var(--vp-c-brand-1);
}
.result-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.result-label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.result-code {
font-size: 13px;
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.immutable-note {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
padding: 10px 12px;
background: var(--vp-c-tip-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-tip-1);
}
.immutable-note svg {
flex-shrink: 0;
color: var(--vp-c-tip-1);
}
.immutable-note code {
background: var(--vp-c-bg);
padding: 1px 4px;
border-radius: 3px;
font-size: 12px;
}
.quick-actions h5 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.action-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.action-chip {
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.action-chip:hover {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
</style>
ts
const d = date('2024-03-15 10:20:30')
d.calc(7) // +7 天
d.addMonths(1) // +1 月(不可变)
d.startOf('day') // 2024-03-15 00:00:00
d.endOf('day') // 2024-03-15 23:59:59
d.startOf('week') // 周一为一周开始
// calc 支持天/周/月/年
date('2024-01-10').calc(2, 'weeks') // 2024-01-24
// startOf/endOf 返回新的 Dater;endOf 返回该区间最后 1ms
date('2024-02-01 12:00').endOf('month').format('yyyy-MM-dd HH:mm:ss') // 2024-02-29 23:59:591
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
比较、差值与范围
vue
⚖️ 日期比较演示
选择两个日期,查看差值计算和范围判断结果
📊 差值计算
compare()
-7 天diff('days')
-7 天diff('weeks')
-1 周diff('months')
0 月diff('hours')
-168 小时diff('years')
0 年🔍 判断方法
❌isSameDay()
❌isSameMonth()
❌isSameYear()
❌A.isWeekend()
❌A.isLeapYear()
📏 范围判断 (isBetween)
2025-12-29[]2026-01-05
2026-01-01
✅ 在区间内
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
<template>
<div class="demo-box">
<div class="demo-section">
<h4>⚖️ 日期比较演示</h4>
<p class="demo-desc">选择两个日期,查看差值计算和范围判断结果</p>
</div>
<div class="demo-controls">
<div class="control-group">
<label>日期 A</label>
<input type="date" v-model="dateA" class="demo-input" />
</div>
<div class="control-group">
<label>日期 B</label>
<input type="date" v-model="dateB" class="demo-input" />
</div>
</div>
<div class="demo-result">
<h5>📊 差值计算</h5>
<div class="diff-grid">
<div class="diff-item">
<span class="diff-label">compare()</span>
<code class="diff-value">{{ compareResult }} 天</code>
</div>
<div class="diff-item">
<span class="diff-label">diff('days')</span>
<code class="diff-value">{{ diffDays }} 天</code>
</div>
<div class="diff-item">
<span class="diff-label">diff('weeks')</span>
<code class="diff-value">{{ diffWeeks }} 周</code>
</div>
<div class="diff-item">
<span class="diff-label">diff('months')</span>
<code class="diff-value">{{ diffMonths }} 月</code>
</div>
<div class="diff-item">
<span class="diff-label">diff('hours')</span>
<code class="diff-value">{{ diffHours }} 小时</code>
</div>
<div class="diff-item">
<span class="diff-label">diff('years')</span>
<code class="diff-value">{{ diffYears }} 年</code>
</div>
</div>
</div>
<div class="demo-result">
<h5>🔍 判断方法</h5>
<div class="check-grid">
<div :class="['check-item', { success: isSameDay }]">
<span class="check-icon">{{ isSameDay ? '✅' : '❌' }}</span>
<span class="check-label">isSameDay()</span>
</div>
<div :class="['check-item', { success: isSameMonth }]">
<span class="check-icon">{{ isSameMonth ? '✅' : '❌' }}</span>
<span class="check-label">isSameMonth()</span>
</div>
<div :class="['check-item', { success: isSameYear }]">
<span class="check-icon">{{ isSameYear ? '✅' : '❌' }}</span>
<span class="check-label">isSameYear()</span>
</div>
<div :class="['check-item', { success: isWeekend }]">
<span class="check-icon">{{ isWeekend ? '✅' : '❌' }}</span>
<span class="check-label">A.isWeekend()</span>
</div>
<div :class="['check-item', { success: isLeapYear }]">
<span class="check-icon">{{ isLeapYear ? '✅' : '❌' }}</span>
<span class="check-label">A.isLeapYear()</span>
</div>
</div>
</div>
<div class="demo-result">
<h5>📏 范围判断 (isBetween)</h5>
<div class="range-section">
<div class="range-controls">
<div class="control-group">
<label>测试日期</label>
<input type="date" v-model="testDate" class="demo-input" />
</div>
<div class="control-group">
<label>区间类型</label>
<select v-model="rangeType" class="demo-select">
<option value="[]">[] 闭区间</option>
<option value="()">(()) 开区间</option>
<option value="[)">[) 左闭右开</option>
<option value="(]">(] 左开右闭</option>
</select>
</div>
</div>
<div class="range-result">
<div class="range-visual">
<span class="range-date">{{ dateA }}</span>
<span class="range-bracket">{{ rangeType[0] }}</span>
<div class="range-line">
<div
class="range-point"
:class="{ inside: isBetweenResult }"
:style="{ left: pointPosition + '%' }"
>
<span class="point-date">{{ testDate }}</span>
</div>
</div>
<span class="range-bracket">{{ rangeType[1] }}</span>
<span class="range-date">{{ dateB }}</span>
</div>
<div :class="['range-verdict', { success: isBetweenResult }]">
{{ isBetweenResult ? '✅ 在区间内' : '❌ 不在区间内' }}
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { date } from '@cat-kit/core/src'
const today = new Date()
const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)
const dateA = ref(today.toISOString().slice(0, 10))
const dateB = ref(nextWeek.toISOString().slice(0, 10))
const testDate = ref(new Date(today.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10))
const rangeType = ref<'[]' | '()' | '[)' | '(]'>('[]')
const daterA = computed(() => date(dateA.value))
const daterB = computed(() => date(dateB.value))
const daterTest = computed(() => date(testDate.value))
const compareResult = computed(() => {
try {
return daterA.value.compare(daterB.value)
} catch {
return 'N/A'
}
})
const diffDays = computed(() => {
try {
return daterA.value.diff(daterB.value, 'days')
} catch {
return 'N/A'
}
})
const diffWeeks = computed(() => {
try {
return daterA.value.diff(daterB.value, 'weeks')
} catch {
return 'N/A'
}
})
const diffMonths = computed(() => {
try {
return daterA.value.diff(daterB.value, 'months')
} catch {
return 'N/A'
}
})
const diffHours = computed(() => {
try {
return daterA.value.diff(daterB.value, 'hours')
} catch {
return 'N/A'
}
})
const diffYears = computed(() => {
try {
return daterA.value.diff(daterB.value, 'years')
} catch {
return 'N/A'
}
})
const isSameDay = computed(() => {
try {
return daterA.value.isSameDay(daterB.value)
} catch {
return false
}
})
const isSameMonth = computed(() => {
try {
return daterA.value.isSameMonth(daterB.value)
} catch {
return false
}
})
const isSameYear = computed(() => {
try {
return daterA.value.isSameYear(daterB.value)
} catch {
return false
}
})
const isWeekend = computed(() => {
try {
return daterA.value.isWeekend()
} catch {
return false
}
})
const isLeapYear = computed(() => {
try {
return daterA.value.isLeapYear()
} catch {
return false
}
})
const isBetweenResult = computed(() => {
try {
return daterTest.value.isBetween(dateA.value, dateB.value, { inclusive: rangeType.value })
} catch {
return false
}
})
const pointPosition = computed(() => {
try {
const a = daterA.value.timestamp
const b = daterB.value.timestamp
const t = daterTest.value.timestamp
const range = b - a
if (range === 0) return 50
const pos = ((t - a) / range) * 100
return Math.max(0, Math.min(100, pos))
} catch {
return 50
}
})
</script>
<style scoped>
.demo-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-section h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.demo-desc {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-group label {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.demo-input,
.demo-select {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
}
.demo-input:focus,
.demo-select:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.demo-result {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
}
.demo-result h5 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.diff-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.diff-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.diff-label {
font-size: 12px;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.diff-value {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-brand-1);
}
.check-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.check-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
font-size: 13px;
}
.check-item.success {
border-color: var(--vp-c-green-1);
background: var(--vp-c-green-soft);
}
.check-label {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1);
}
.range-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.range-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.range-result {
display: flex;
flex-direction: column;
gap: 12px;
}
.range-visual {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.range-date {
font-size: 12px;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.range-bracket {
font-size: 20px;
font-weight: bold;
color: var(--vp-c-brand-1);
}
.range-line {
flex: 1;
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
position: relative;
min-width: 100px;
}
.range-point {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
background: var(--vp-c-danger-1);
border-radius: 50%;
border: 2px solid var(--vp-c-bg);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.range-point.inside {
background: var(--vp-c-green-1);
}
.point-date {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
white-space: nowrap;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.range-verdict {
padding: 10px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
background: var(--vp-c-danger-soft);
color: var(--vp-c-danger-1);
text-align: center;
}
.range-verdict.success {
background: var(--vp-c-green-soft);
color: var(--vp-c-green-1);
}
</style>
ts
const a = date('2024-01-15')
const b = date('2024-01-20')
a.compare(b) // -5(天数差,向零取整)
a.diff(b, 'hours') // -120
a.diff(b, 'weeks', { float: true }) // -0.714...
a.diff(b, 'days', { absolute: true }) // 5
// 范围与判定
a.isBetween('2024-01-10', '2024-01-31') // true
a.isSameDay('2024-01-15 23:00:00') // true
a.isSameMonth('2024-01-01') // true
a.isSameYear('2024-12-01') // true
a.isWeekend() // 根据星期判断
a.isLeapYear() // 2024 -> true1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
compare(date, reducer?)默认返回向零取整的天数差;传入 reducer 可基于毫秒差自定义结果。diff支持毫秒至年,absolute取绝对值,float允许非整数(仅毫秒-周)。months/years差值按日历月/年计算并截断(不会假设固定 30/365 天)。isBetween可通过inclusive指定区间类型:[](默认)、()、[)、(]。
月份工具
ts
const d = date('2024-02-15')
d.toEndOfMonth() // 2024-02-29
d.getDays() // 29
// 支持偏移跳转到未来/过去月末(可变操作)
date('2024-02-15').toEndOfMonth(1).format() // 2024-03-31
// getDays 会短暂修改日期后恢复原时间戳1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
注意事项
- 月份从 1 开始,星期从周日=0 起算,
startOf('week')以周一为一周开始。 set*、toEndOfMonth属于可变操作;add*/calc/startOf/endOf/clone返回新实例。format/parse默认使用本地时区,可通过utc: true切换为 UTC。- 无效日期不会抛错,需在业务侧检查
Number.isNaN(d.timestamp)。 - 复杂跨月/跨年的差值请优先使用
diff而非手写算法。
