|
@@ -0,0 +1,97 @@
|
|
|
|
|
+"use client"
|
|
|
|
|
+
|
|
|
|
|
+import * as React from "react"
|
|
|
|
|
+import { useQuery } from "@tanstack/react-query"
|
|
|
|
|
+import { ActivityType } from "@/server/modules/activities/activity.entity"
|
|
|
|
|
+import { Combobox } from "@/client/components/ui/combobox"
|
|
|
|
|
+import { activityClient } from "@/client/api"
|
|
|
|
|
+import type { InferResponseType } from "hono/client"
|
|
|
|
|
+
|
|
|
|
|
+type ActivityResponse = InferResponseType<typeof activityClient.$get, 200>['data'][0]
|
|
|
|
|
+
|
|
|
|
|
+interface ActivitySelectProps {
|
|
|
|
|
+ value?: number
|
|
|
|
|
+ onValueChange?: (value: number) => void
|
|
|
|
|
+ placeholder?: string
|
|
|
|
|
+ className?: string
|
|
|
|
|
+ disabled?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function ActivitySelect({
|
|
|
|
|
+ value,
|
|
|
|
|
+ onValueChange,
|
|
|
|
|
+ placeholder = "请选择活动...",
|
|
|
|
|
+ className,
|
|
|
|
|
+ disabled = false,
|
|
|
|
|
+}: ActivitySelectProps) {
|
|
|
|
|
+ const [searchKeyword, setSearchKeyword] = React.useState("")
|
|
|
|
|
+
|
|
|
|
|
+ // 获取活动列表
|
|
|
|
|
+ const { data: activitiesData, isLoading } = useQuery({
|
|
|
|
|
+ queryKey: ['activities', searchKeyword],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const res = await activityClient.$get({
|
|
|
|
|
+ query: {
|
|
|
|
|
+ keyword: searchKeyword || undefined,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 50, // 获取足够多的数据用于搜索
|
|
|
|
|
+ filters: JSON.stringify({ isDisabled: 0 }), // 只显示启用的活动
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res.status !== 200) throw new Error('获取活动列表失败')
|
|
|
|
|
+ return await res.json()
|
|
|
|
|
+ },
|
|
|
|
|
+ staleTime: 5 * 60 * 1000, // 5分钟缓存
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 防抖搜索
|
|
|
|
|
+ React.useEffect(() => {
|
|
|
|
|
+ const timeoutId = setTimeout(() => {
|
|
|
|
|
+ setSearchKeyword(searchKeyword)
|
|
|
|
|
+ }, 300)
|
|
|
|
|
+
|
|
|
|
|
+ return () => clearTimeout(timeoutId)
|
|
|
|
|
+ }, [searchKeyword])
|
|
|
|
|
+
|
|
|
|
|
+ // 将活动数据转换为 Combobox 选项
|
|
|
|
|
+ const activityOptions = React.useMemo(() => {
|
|
|
|
|
+ if (!activitiesData?.data) return []
|
|
|
|
|
+
|
|
|
|
|
+ return activitiesData.data.map((activity: ActivityResponse) => ({
|
|
|
|
|
+ value: activity.id.toString(),
|
|
|
|
|
+ label: `${activity.name} (${getActivityTypeLabel(activity.type)})`,
|
|
|
|
|
+ }))
|
|
|
|
|
+ }, [activitiesData])
|
|
|
|
|
+
|
|
|
|
|
+ // 获取活动类型显示标签
|
|
|
|
|
+ const getActivityTypeLabel = (type: ActivityType) => {
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case ActivityType.DEPARTURE:
|
|
|
|
|
+ return "去程"
|
|
|
|
|
+ case ActivityType.RETURN:
|
|
|
|
|
+ return "返程"
|
|
|
|
|
+ default:
|
|
|
|
|
+ return "未知"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleValueChange = (newValue: string) => {
|
|
|
|
|
+ const numericValue = newValue ? parseInt(newValue, 10) : 0
|
|
|
|
|
+ onValueChange?.(numericValue)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const selectedValue = value ? value.toString() : ""
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Combobox
|
|
|
|
|
+ options={activityOptions}
|
|
|
|
|
+ value={selectedValue}
|
|
|
|
|
+ onValueChange={handleValueChange}
|
|
|
|
|
+ placeholder={isLoading ? "加载中..." : placeholder}
|
|
|
|
|
+ searchPlaceholder="搜索活动名称..."
|
|
|
|
|
+ emptyMessage="未找到匹配的活动"
|
|
|
|
|
+ className={className}
|
|
|
|
|
+ disabled={disabled || isLoading}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+}
|