|
|
@@ -2,6 +2,7 @@
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
import type { PickerOption } from 'vant'
|
|
|
+import dayjs from 'dayjs'
|
|
|
|
|
|
type DateRange = {
|
|
|
start?: string // YYYY-MM-DD
|
|
|
@@ -10,6 +11,9 @@ type DateRange = {
|
|
|
|
|
|
type PickerValue = [string, string, string] // day, month, year
|
|
|
|
|
|
+const ISO_DATE_FORMAT = 'YYYY-MM-DD'
|
|
|
+const DISPLAY_DATE_FORMAT = 'DD/MM/YYYY'
|
|
|
+
|
|
|
const props = withDefaults(
|
|
|
defineProps<{
|
|
|
show: boolean
|
|
|
@@ -38,10 +42,16 @@ const isIsoDate = (value?: string): value is string => {
|
|
|
return !!value && /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
|
}
|
|
|
|
|
|
+const isValidIsoDate = (iso?: string) => {
|
|
|
+ if (!isIsoDate(iso)) return false
|
|
|
+ // Strict parse to avoid overflow dates like 2026-02-31 being treated as valid.
|
|
|
+ return dayjs(iso, ISO_DATE_FORMAT, true).isValid()
|
|
|
+}
|
|
|
+
|
|
|
const toDisplay = (iso?: string) => {
|
|
|
if (!isIsoDate(iso)) return ''
|
|
|
- const [y, m, d] = iso.split('-')
|
|
|
- return `${d}-${m}-${y}`
|
|
|
+
|
|
|
+ return dayjs(iso, ISO_DATE_FORMAT).format(DISPLAY_DATE_FORMAT)
|
|
|
}
|
|
|
|
|
|
const toIso = (d: string, m: string, y: string) => `${y}-${m}-${d}`
|
|
|
@@ -114,13 +124,16 @@ const formatter = (type: string, option: PickerOption) => {
|
|
|
return option
|
|
|
}
|
|
|
|
|
|
-const handleChange = (values: string[]) => {
|
|
|
- // values order: day, month, year
|
|
|
- const d = values[0]
|
|
|
- const m = values[1]
|
|
|
- const y = values[2]
|
|
|
+const handleChange = (values: unknown) => {
|
|
|
+ const { selectedValues } = values as { selectedValues: string[] }
|
|
|
+ const d = selectedValues[0]
|
|
|
+ const m = selectedValues[1]
|
|
|
+ const y = selectedValues[2]
|
|
|
+
|
|
|
if (!d || !m || !y) return
|
|
|
+
|
|
|
const iso = toIso(pad2(d), pad2(m), y)
|
|
|
+
|
|
|
if (activeField.value === 'start') startDate.value = iso
|
|
|
else endDate.value = iso
|
|
|
}
|
|
|
@@ -135,16 +148,28 @@ const handleReset = () => {
|
|
|
emit('reset')
|
|
|
}
|
|
|
|
|
|
+const isRangeValid = computed(() => {
|
|
|
+ const start = startDate.value
|
|
|
+ const end = endDate.value
|
|
|
+
|
|
|
+ if (start && !isValidIsoDate(start)) return false
|
|
|
+ if (end && !isValidIsoDate(end)) return false
|
|
|
+
|
|
|
+ if (!start || !end) return true
|
|
|
+
|
|
|
+ const s = dayjs(start, ISO_DATE_FORMAT, true)
|
|
|
+ const e = dayjs(end, ISO_DATE_FORMAT, true)
|
|
|
+ if (!s.isValid() || !e.isValid()) return false
|
|
|
+
|
|
|
+ return !e.isBefore(s, 'day')
|
|
|
+})
|
|
|
+
|
|
|
+const canSave = computed(() => (startDate.value && endDate.value) && isRangeValid.value)
|
|
|
+
|
|
|
const handleSave = () => {
|
|
|
+ if (!canSave.value) return
|
|
|
const next: DateRange = { start: startDate.value, end: endDate.value }
|
|
|
|
|
|
- // If both set and end < start, swap.
|
|
|
- if (next.start && next.end && next.end < next.start) {
|
|
|
- const tmp = next.start
|
|
|
- next.start = next.end
|
|
|
- next.end = tmp
|
|
|
- }
|
|
|
-
|
|
|
emit('update:value', next)
|
|
|
emit('confirm', next)
|
|
|
emit('update:show', false)
|
|
|
@@ -183,11 +208,15 @@ const handleUpdateShow = (value: boolean) => {
|
|
|
<button
|
|
|
type="button"
|
|
|
class="date-select-popup__pill"
|
|
|
- :class="{ 'date-select-popup__pill--active': activeField === 'start' }"
|
|
|
+ :class="{ 'date-select-popup__pill--active': activeField === 'start',
|
|
|
+ 'date-select-popup__pill--invalid': activeField === 'start' && !isRangeValid }"
|
|
|
@click="activeField = 'start'"
|
|
|
>
|
|
|
- <span class="date-select-popup__pill-text">
|
|
|
- {{ toDisplay(startDate) || t('wallet.dateSelect.startPlaceholder') }}
|
|
|
+ <span
|
|
|
+ class="date-select-popup__pill-text"
|
|
|
+ :class="{ 'date-select-popup__pill-text--placeholder': !startDate }"
|
|
|
+ >
|
|
|
+ {{ startDate ? toDisplay(startDate) : t('wallet.dateSelect.startPlaceholder') }}
|
|
|
</span>
|
|
|
</button>
|
|
|
|
|
|
@@ -196,7 +225,8 @@ const handleUpdateShow = (value: boolean) => {
|
|
|
<button
|
|
|
type="button"
|
|
|
class="date-select-popup__pill"
|
|
|
- :class="{ 'date-select-popup__pill--active': activeField === 'end' }"
|
|
|
+ :class="{ 'date-select-popup__pill--active': activeField === 'end',
|
|
|
+ 'date-select-popup__pill--invalid': activeField === 'end' && !isRangeValid }"
|
|
|
@click="activeField = 'end'"
|
|
|
>
|
|
|
<span
|
|
|
@@ -222,6 +252,8 @@ const handleUpdateShow = (value: boolean) => {
|
|
|
<button
|
|
|
type="button"
|
|
|
class="date-select-popup__save"
|
|
|
+ :class="{ 'date-select-popup__save--disabled': !canSave }"
|
|
|
+ :disabled="!canSave"
|
|
|
@click="handleSave"
|
|
|
>
|
|
|
{{ t('wallet.dateSelect.save') }}
|
|
|
@@ -302,6 +334,10 @@ const handleUpdateShow = (value: boolean) => {
|
|
|
border-color: #1789ff;
|
|
|
}
|
|
|
|
|
|
+ &__pill--invalid {
|
|
|
+ border-color: #ff4d4f !important;
|
|
|
+ }
|
|
|
+
|
|
|
&__pill-text {
|
|
|
font-size: 14px;
|
|
|
font-weight: 600;
|
|
|
@@ -351,6 +387,11 @@ const handleUpdateShow = (value: boolean) => {
|
|
|
font-weight: 600;
|
|
|
-webkit-tap-highlight-color: transparent;
|
|
|
}
|
|
|
+
|
|
|
+ &__save--disabled,
|
|
|
+ &__save:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* Vant picker style overrides */
|