c653ed071d
- 所有 lucide: 前缀的图标字符串替换为 lucide-vue-next 组件直接引用
- 动态图标绑定改为 <component :is> 渲染
- h(Icon, { icon: 'lucide:xxx' }) 改为 h(Xxx, { class: ... })
- app-card.vue 的 icon_url 改为 <img> 标签(实际是图片路径)
- 修复 Webhook 标识符冲突(重命名为 WebhookIcon)
- payment-channels 保留 @iconify/vue 用于品牌图标(支付宝、微信、Stripe、PayPal、USDT)
142 lines
4.2 KiB
Vue
142 lines
4.2 KiB
Vue
<script setup lang="ts">
|
|
import type { DateValue } from '@internationalized/date'
|
|
import type { HTMLAttributes } from 'vue'
|
|
import { parseDate } from '@internationalized/date'
|
|
import { cn } from '@/lib/utils'
|
|
import { CalendarClock } from 'lucide-vue-next'
|
|
import { computed, ref, watch } from 'vue'
|
|
import UiButton from '@/components/ui/button/Button.vue'
|
|
import UiPopover from '@/components/ui/popover/Popover.vue'
|
|
import UiPopoverTrigger from '@/components/ui/popover/PopoverTrigger.vue'
|
|
import UiPopoverContent from '@/components/ui/popover/PopoverContent.vue'
|
|
import UiCalendar from '@/components/ui/calendar/Calendar.vue'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
|
|
const props = defineProps<{
|
|
modelValue?: string
|
|
placeholder?: string
|
|
class?: HTMLAttributes['class']
|
|
disabled?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
}>()
|
|
|
|
const open = ref(false)
|
|
const selectedDate = ref<DateValue | undefined>()
|
|
const selectedHour = ref('00')
|
|
const selectedMinute = ref('00')
|
|
|
|
const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'))
|
|
const minutes = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'))
|
|
|
|
const displayText = computed(() => {
|
|
if (props.modelValue) {
|
|
return props.modelValue.replace('T', ' ')
|
|
}
|
|
return ''
|
|
})
|
|
|
|
function handleDateSelect(date: DateValue | undefined) {
|
|
if (!date)
|
|
return
|
|
selectedDate.value = date
|
|
emitValue()
|
|
}
|
|
|
|
function emitValue() {
|
|
if (selectedDate.value) {
|
|
emit('update:modelValue', `${selectedDate.value.toString()}T${selectedHour.value}:${selectedMinute.value}`)
|
|
}
|
|
}
|
|
|
|
function resetTime() {
|
|
selectedDate.value = undefined
|
|
selectedHour.value = '00'
|
|
selectedMinute.value = '00'
|
|
emit('update:modelValue', '')
|
|
open.value = false
|
|
}
|
|
|
|
function initFromModelValue() {
|
|
if (props.modelValue) {
|
|
const parts = props.modelValue.split('T')
|
|
try {
|
|
selectedDate.value = parseDate(parts[0])
|
|
if (parts[1]) {
|
|
const [h, m] = parts[1].split(':')
|
|
selectedHour.value = h || '00'
|
|
selectedMinute.value = m || '00'
|
|
}
|
|
}
|
|
catch {}
|
|
}
|
|
}
|
|
|
|
watch(() => props.modelValue, initFromModelValue, { immediate: true })
|
|
</script>
|
|
|
|
<template>
|
|
<UiPopover v-model:open="open">
|
|
<UiPopoverTrigger as-child>
|
|
<UiButton
|
|
variant="outline"
|
|
:class="cn(
|
|
'justify-start text-left font-normal truncate h-8',
|
|
!displayText && 'text-muted-foreground',
|
|
props.class,
|
|
)"
|
|
:disabled="disabled"
|
|
>
|
|
<CalendarClock class="mr-2 h-4 w-4 shrink-0" />
|
|
<span class="truncate text-sm">{{ displayText || placeholder || '选择日期时间' }}</span>
|
|
</UiButton>
|
|
</UiPopoverTrigger>
|
|
<UiPopoverContent class="w-auto p-0" align="start">
|
|
<div class="p-3">
|
|
<UiCalendar
|
|
:model-value="selectedDate"
|
|
@update:model-value="handleDateSelect"
|
|
/>
|
|
</div>
|
|
<div class="border-t px-3 py-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<Select v-model="selectedHour" @update:model-value="emitValue">
|
|
<SelectTrigger size="sm" class="w-[65px] h-8">
|
|
<SelectValue placeholder="时" />
|
|
</SelectTrigger>
|
|
<SelectContent class="max-h-[180px]">
|
|
<SelectItem v-for="h in hours" :key="h" :value="h">
|
|
{{ h }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<span class="text-muted-foreground font-bold text-sm">:</span>
|
|
<Select v-model="selectedMinute" @update:model-value="emitValue">
|
|
<SelectTrigger size="sm" class="w-[65px] h-8">
|
|
<SelectValue placeholder="分" />
|
|
</SelectTrigger>
|
|
<SelectContent class="max-h-[180px]">
|
|
<SelectItem v-for="m in minutes" :key="m" :value="m">
|
|
{{ m }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<UiButton variant="ghost" size="sm" class="h-8 px-2 text-xs" @click="resetTime">
|
|
重置
|
|
</UiButton>
|
|
</div>
|
|
</div>
|
|
</UiPopoverContent>
|
|
</UiPopover>
|
|
</template>
|