# 日期输入框

## 使用方法

`DateInput` 是一个日期输入组件，结合了文本输入框和日历弹出层。用户可以手动输入日期文本，也可以点击打开日历选择。

基本用法只需要绑定一个日期值：

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { ref } from 'vue'

const date = ref(null)
</script>

<template>
  <DateInput v-model="date" placeholder="选择日期" />
</template>
```

绑定值是本地日期。写回字段时默认排成完整日期，例如 March 21, 2026。也可以改成短日期、带星期、系统日期，或自己排版。标签下面可以放说明。校验可以只改边框，也可以在字段下面写出原因。

日历可以从弹出层或底部面板打开。底部面板一次显示一个月，点一天就写入，保存才收起；点月份标题可以改月份和年份。系统日期控件可以一直用，也可以只在触摸设备上用。不指定时，鼠标走弹出层，触摸走系统控件。字段正在保存时不能改，保存会在新值写进去之后进行。标签可以带一条提示，并标明选填或必填。弹出的月历可以一次看两个月，一周从哪一天开始也能指定。

### 结构

- **标签**: 输入框上方的文字，说明期望的是什么日期。
- **文本输入**: 可以直接打日期的字段。能解析 MM/DD/YYYY 这类常见格式。
- **日历图标**: 打开日历浮层，看着日历选日期的按钮。
- **日历浮层**: 点击图标或输入框聚焦时出现的月份网格。
- **清除按钮**: 把日期清掉的 × 按钮。clearable 打开且已经有日期时显示。
- **状态消息**: 输入框下面的错误、警告或成功提示。

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | 为非必填的日期字段提供清除功能。 |
| 推荐 | 设置合理的日期范围限制，防止用户选择无效日期。 |
| 推荐 | 使用清晰的占位符提示日期格式。 |
| 避免 | 在需要精确到时分秒的场景使用日期输入框。 |

## 示例

### 可清除

可选的活动日期，带清除按钮。字段上方显示当前选中的值。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const value = ref<Date | null>(new Date(2026, 3, 6))

function iso(date: Date | null): string {
  if (!date)
    return ''
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${date.getFullYear()}-${month}-${day}`
}
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      {{ value ? `Selected: ${iso(value)}` : 'No date selected' }}
    </Text>
    <DateInput
      v-model="value"
      label="Event date"
      description="Pick a date for your event"
      placeholder="Select a date"
      clearable
    />
  </VStack>
</template>
```

### 说明文字

标签下方的说明用来交代这个日期的用途。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const value = ref<Date | null>(null)
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      Helper text explains what the field expects
    </Text>
    <DateInput
      v-model="value"
      label="Start date"
      description="Your subscription begins on this date"
      placeholder="Select a start date"
    />
  </VStack>
</template>
```

### 显示格式

同一天可以显示为短月份、长月份、带星期、ISO 日期，或自定义文案。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const value = ref<Date | null>(new Date(2026, 2, 21))

function shipBy(iso: string): string {
  return `Ship by ${iso}`
}
</script>

<template>
  <VStack :gap="4" style="width: 320px; max-width: 100%">
    <Text type="supporting" color="secondary">
      The same committed date, displayed with different formats.
    </Text>
    <DateInput v-model="value" label="Short month (date)" format="date" />
    <DateInput v-model="value" label="Long month (date_long, default)" format="date_long" />
    <DateInput v-model="value" label="With weekday (date_weekday)" format="date_weekday" />
    <DateInput v-model="value" label="ISO 8601 (system_date)" format="system_date" />
    <DateInput v-model="value" label="Custom function" :format="shipBy" />
  </VStack>
</template>
```

### 最小和最大

只能选择本月 8 日到 21 日。粗指针设备会在底部面板里打开日历。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const today = new Date()
const year = today.getFullYear()
const month = today.getMonth()
const startDay = 8
const endDay = 21
const min = new Date(year, month, startDay)
const max = new Date(year, month, endDay)
const monthName = new Date(year, month, 1).toLocaleString('en-US', { month: 'short' })
const description = `Available dates: ${monthName} ${startDay} – ${endDay}, ${year}`
const value = ref<Date | null>(null)

function iso(date: Date | null): string {
  if (!date)
    return ''
  const monthNumber = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${date.getFullYear()}-${monthNumber}-${day}`
}
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      {{ value ? `Booked: ${iso(value)}` : 'Pick a date in the available range' }}
    </Text>
    <DateInput
      v-model="value"
      label="Booking date"
      :min="min"
      :max="max"
      :description="description"
      placeholder="Select a booking date"
      presentation="adaptive-bottom-sheet"
    />
  </VStack>
</template>
```

### 校验状态

错误、警告和成功，各自带一条说明。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { ref } from 'vue'

const errorVal = ref<Date | null>(new Date(2026, 0, 25))
const warningVal = ref<Date | null>(new Date(2026, 11, 25))
const successVal = ref<Date | null>(new Date(2026, 2, 10))
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <DateInput
      v-model="errorVal"
      label="Event date"
      :status="{ type: 'error', message: 'This date is already booked' }"
    />
    <DateInput
      v-model="warningVal"
      label="Preferred date"
      :status="{ type: 'warning', message: 'This date falls on a holiday' }"
    />
    <DateInput
      v-model="successVal"
      label="Start date"
      :status="{ type: 'success', message: 'Date confirmed' }"
    />
  </VStack>
</template>
```

### 保存与标签

日期先写进字段，保存结束前不能再改。周末不可选。另一个字段标明选填。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { ref } from 'vue'

const ship = ref<Date | null>(new Date(2026, 2, 21))
const due = ref<Date | null>(null)

function save(value: Date | null): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, value ? 900 : 400)
  })
}
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <DateInput
      v-model="ship"
      label="Ship date"
      description="Saved when the date changes."
      label-tooltip="Warehouse days are Monday to Friday."
      is-required
      clearable
      :change-action="save"
      :date-constraints="[(date: Date) => date.getDay() !== 0 && date.getDay() !== 6]"
    />
    <DateInput
      v-model="due"
      label="Follow-up"
      is-optional
      placeholder="Select a date"
    />
  </VStack>
</template>
```

### 系统日期

直接使用系统日期控件，字段上仍按完整日期显示。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { ref } from 'vue'

const date = ref<Date | null>(new Date(2026, 2, 21))
</script>

<template>
  <div style="width: 240px; max-width: 100%">
    <DateInput
      v-model="date"
      label="System date"
      description="This field opens the browser date control."
      presentation="native"
      clearable
    />
  </div>
</template>
```

### 底部面板

不论鼠标还是触屏，都从底部面板选日期。点一天就写入，保存才收起。

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { ref } from 'vue'

const date = ref<Date | null>(new Date(2026, 2, 21))
</script>

<template>
  <div style="width: 240px; max-width: 100%">
    <DateInput
      v-model="date"
      label="Due"
      description="Opens a one-month sheet. Save closes it."
      presentation="bottom-sheet"
    />
  </div>
</template>
```

## DateInput Props

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `Date \| null` | `null` | 当前选中的日期。 |
| `placeholder` | `string` | `Select a date` / `选择日期` | 占位文本。未设置语言时为英文，提供 `zh-CN` 后为中文。 |
| `min` | `Date` | — | 最早可选日期。 |
| `max` | `Date` | — | 最晚可选日期。 |
| `isDateDisabled` | `(date: Date) => boolean` | — | 返回 true 时该日期不可选。 |
| `dateConstraints` | `Array<(date: Date) => boolean>` | — | 任一函数返回 false 时该日期不可选。 |
| `name` | `string` | — | 表单字段名。 |
| `label` | `string` | — | 字段标签。 |
| `description` | `string` | — | 标签下方的说明。 |
| `isLabelHidden` | `boolean` | `false` | 标签只留给读屏，页面上不显示。旧名 `labelHidden` 同样可用，两者任一为真就隐藏。 |
| `isOptional` | `boolean` | — | 在标签旁标成可选。 |
| `isRequired` | `boolean` | — | 把字段标成必填，输入框带上 aria-required。 |
| `labelTooltip` | `string` | — | 标签末尾信息图标里的说明。 |
| `format` | `'date' \| 'date_long' \| 'date_weekday' \| 'system_date' \| ((iso: string) => string)` | `'date_long'` | 已提交日期的显示方式。函数收到的是 `YYYY-MM-DD`。 |
| `presentation` | `'popover' \| 'bottom-sheet' \| 'native' \| 'adaptive-bottom-sheet' \| 'adaptive-native'` | 未设置时为 adaptive-native | 弹出层、底部面板、系统日期控件，或按指针在它们之间切换。同时传了 nativePicker 时以这项为准。 |
| `nativePicker` | `'touch' \| 'always' \| 'never'` | — | 旧的打开方式。touch 跟随指针，always 用系统控件，never 按指针在弹出层和底部面板之间切换。同时传了 presentation 时以 presentation 为准。 |
| `numberOfMonths` | `1 \| 2` | `1` | 弹出日历并排的月份数。系统日期控件表达不了这项。 |
| `weekStartsOn` | `0-6 \| 'sun' \| 'mon' \| ...` | — | 弹出日历从哪一天开始一周。系统日期控件表达不了这项。 |
| `disabled` | `boolean` | `false` | 是否禁用。 |
| `isLoading` | `boolean` | `false` | 字段不可改，并显示加载指示。 |
| `changeAction` | `(value: Date \| null) => void \| Promise<void>` | — | 值提交之后再跑。返回 Promise 时，字段保持加载直到它结束。 |
| `width` | `number \| string` | — | 整段字段的宽度。数字按像素算。 |
| `disabledMessage` | `string` | — | 禁用原因。和 `disabled` 一起用时，悬停或键盘聚焦会显示这段提示，输入框仍可聚焦（带 `aria-disabled`），但改不了值。旧名 `disabledReason` 同样可用。 |
| `hasClear` | `boolean` | `false` | 有值时显示清除按钮，清空后焦点回到输入框。旧名 `clearable` 同样可用。 |
| `clearLabel` | `string` | `Clear` / `清除` | 清除按钮的无障碍名称。设置了 `label` 时会带上它。未设置语言时为英文，提供 `zh-CN` 后为中文。 |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 尺寸。 |
| `status` | `'default' \| 'error' \| 'warning' \| 'success' \| { type, message? }` | — | 校验状态。对象带 `message` 时，说明会显示在字段下方。 |
| `statusVariant` | `'attached' \| 'detached' \| 'tooltip'` | `'attached'` | 状态说明的位置。 |
| `containerClass` | `string \| object \| array` | — | 加到字段外壳的 class。在 InputGroup 和横向标签的 FormLayout 里没有外壳，此时不生效，开发环境会警告。 |
| `containerStyle` | `StyleValue` | — | 加到字段外壳的行内样式。落点规则同 containerClass。 |
| `controlClass` | `string \| object \| array` | — | 加到带边框本体的 class。直接写在组件上的 class 也落在本体，不会落到内部输入框上。 |
| `controlStyle` | `StyleValue` | — | 加到带边框本体的行内样式。 |

## DateInput 事件

| 名称 | 载荷 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `value: Date \| null` | 日期变化时触发。 |
| `change` | `value: Date \| null` | 日期变化时触发。 |

## DateInput 插槽

| 名称 | 说明 |
| --- | --- |
| `prefix` | 输入框前面的图标。 |
| `suffix` | 输入框后面的内容。 |
