Răsfoiți Sursa

✨ feat(activity): 优化活动选择流程,移除精确地点选择需求

- 更新活动选择页面:接收省市区参数,直接根据省市区查询活动,无需精确地点选择
- 修改 `ActivitySelectPage.tsx`:移除地点选择区域和相关状态管理
- 更新查询参数:使用省市区ID数组而非地点ID进行活动查询
- 优化页面布局:简化活动选择页面结构,直接显示根据省市区查询的活动结果

♻️ refactor(schedule): 适配新的活动查询参数格式

- 更新 `ScheduleListPage.tsx`:接收并处理省市区参数,而非地点ID
- 修改API调用:使用省市区ID数组进行路线查询
- 优化查询条件:使用JSON.stringify处理省市区ID数组参数
- 调整查询启用条件:验证省市区参数完整性后才执行查询

📝 docs(story): 更新用户故事文档,反映新的交互流程

- 修改活动选择页面需求:移除精确地点选择相关任务
- 添加UI/UX优化部分:详细描述活动选择页面简化内容
- 更新交互流程说明:明确首页只传递省市区参数,活动选择页面直接查询结果
- 补充数据流调整说明:首页仅传递省市区ID数组,活动选择页面直接使用该参数查询

🐛 fix(api): 修复活动查询时关联数据不完整问题

- 修改 `route.service.ts`:查询活动时关联查询地点及其省市区信息
- 添加relations配置:获取活动地点的province、city和district关联数据
- 优化活动地点信息显示:在活动选择页面正确展示完整的省市区信息
yourname 3 luni în urmă
părinte
comite
c637e43570

+ 22 - 4
docs/stories/005.002.story.md

@@ -93,9 +93,9 @@ Ready for Review
     - [ ] 修改 `SearchParams` 接口:移除 `startLocation`/`endLocation`,添加 `startAreaIds`/`endAreaIds`
     - [ ] 更新查询验证:只需验证省市区选择,不需要精确地点
     - [ ] 修改导航参数:传递省市区ID数组而不是地点ID
-    - [ ] 更新活动选择页面:接收省市区参数,在该页面进行精确地点选择
+    - [ ] 更新活动选择页面:接收省市区参数,直接根据省市区查询活动,无需精确地点选择
   - [ ] 更新相关页面和组件
-    - [ ] 更新 `ActivitySelectPage.tsx`:接收省市区参数,在该页面集成精确地点选择
+    - [ ] 更新 `ActivitySelectPage.tsx`:接收省市区参数,直接根据省市区查询活动,无需精确地点选择
     - [ ] 更新 `ScheduleListPage.tsx`:适配新的查询参数格式
     - [ ] 更新API调用:修改查询参数格式
   - [ ] 编写组件单元测试和集成测试
@@ -103,6 +103,22 @@ Ready for Review
     - [ ] 首页集成测试:测试完整的省市区选择流程
     - [ ] 更新E2E测试:验证新的交互流程
 
+- [ ] 优化活动选择页面用户体验 (UI/UX优化)
+  - [ ] 移除活动选择页面的地点选择区域
+    - [ ] 删除 `LocationSearch` 组件在活动选择页面的使用
+    - [ ] 移除地点选择相关的UI元素和状态管理
+  - [ ] 修改活动查询逻辑
+    - [ ] 直接根据省市区参数查询活动,无需精确地点ID
+    - [ ] 更新查询条件:使用省市区ID而非地点ID
+    - [ ] 优化查询性能:减少不必要的API调用
+  - [ ] 更新页面布局和交互
+    - [ ] 简化页面结构,移除地点选择区域
+    - [ ] 直接显示根据省市区查询的活动结果
+    - [ ] 优化加载状态和空状态显示
+  - [ ] 更新相关测试
+    - [ ] 更新活动选择页面集成测试
+    - [ ] 更新E2E测试:验证简化的交互流程
+
 ## Dev Notes
 
 ### MVP限制说明
@@ -215,14 +231,14 @@ Ready for Review
 
 **数据流调整**:
 - 首页只传递省市区ID数组:`startAreaIds`, `endAreaIds`
-- 精确地点选择移至活动选择页面,在该页面集成 `LocationSearch` 组件
+- 活动选择页面直接根据省市区查询活动,无需精确地点选择
 - 查询参数格式:`startAreaIds=[provinceId,cityId,districtId]`
 
 **交互流程**:
 1. 用户点击出发地/目的地按钮 → 弹出 `AreaPicker` 选择省市区
 2. 选择完成后显示省市区文本(如"北京市朝阳区")
 3. 点击查询按钮 → 导航到活动选择页面,传递省市区参数
-4. 在活动选择页面进行精确地点选择
+4. 活动选择页面直接根据省市区查询并展示相关活动
 
 **API集成**:
 - 使用现有省市区API:`/api/v1/areas/provinces`, `/api/v1/areas/cities`, `/api/v1/areas/districts`
@@ -340,6 +356,8 @@ James (Developer Agent)
 - ✅ 迁移班次列表页面:显示路线详细信息,支持日期选择和排序
 - ✅ 编写完整的测试套件:包含组件单元测试、页面集成测试和E2E流程测试
 - ✅ 遵循MVP限制:使用固定轮播图,不实现热门路线和司机位置显示
+- ⚠️ **优化发现**:活动选择页面无需重新选择地点,应直接使用首页传递的省市区参数查询活动
+- ⚠️ **待优化**:移除活动选择页面的地点选择区域,简化用户操作流程
 
 ### File List
 - `src/server/api/areas/index.ts` - 省市区API路由

+ 50 - 23
mini/src/pages/schedule-list/ScheduleListPage.tsx

@@ -1,22 +1,30 @@
 import React, { useState, useEffect } from 'react'
 import { View, Text, ScrollView, Button } from '@tarojs/components'
-import { useRouter, navigateTo } from '@tarojs/taro'
+import { useRouter } from '@tarojs/taro'
 import { useQuery } from '@tanstack/react-query'
 import { routeClient } from '../../api'
 
 interface Route {
   id: number
+  name: string
+  description: string | null
+  startLocationId: number
+  endLocationId: number
   startLocation: {
+    id: number
     name: string
-    province?: string
-    city?: string
-    district?: string
+    provinceId: number
+    cityId: number
+    districtId: number
+    address: string
   }
   endLocation: {
+    id: number
     name: string
-    province?: string
-    city?: string
-    district?: string
+    provinceId: number
+    cityId: number
+    districtId: number
+    address: string
   }
   pickupPoint: string
   dropoffPoint: string
@@ -25,11 +33,28 @@ interface Route {
   price: number
   seatCount: number
   availableSeats: number
-  routeType: 'departure' | 'return'
-  activities: Array<{
+  activityId: number
+  activity: {
     id: number
     name: string
-  }>
+    description: string | null
+    venueLocationId: number
+    venueLocation: {
+      id: number
+      name: string
+      provinceId: number
+      cityId: number
+      districtId: number
+      address: string
+    }
+    startDate: string
+    endDate: string
+  }
+  routeType: 'departure' | 'return'
+  isDisabled: number
+  isDeleted: number
+  createdAt: string
+  updatedAt: string
 }
 
 export const ScheduleListPage: React.FC = () => {
@@ -39,8 +64,8 @@ export const ScheduleListPage: React.FC = () => {
 
   // 从路由参数获取查询条件
   const searchParams = {
-    startLocationId: Number(router.params.startLocationId),
-    endLocationId: Number(router.params.endLocationId),
+    startAreaIds: router.params.startAreaIds ? JSON.parse(router.params.startAreaIds) as number[] : [],
+    endAreaIds: router.params.endAreaIds ? JSON.parse(router.params.endAreaIds) as number[] : [],
     date: router.params.date || new Date().toISOString().split('T')[0],
     vehicleType: router.params.vehicleType || 'bus',
     activityId: Number(router.params.activityId),
@@ -50,7 +75,7 @@ export const ScheduleListPage: React.FC = () => {
   // 生成日期选项
   useEffect(() => {
     const today = new Date()
-    const dates = []
+    const dates: string[] = []
     for (let i = 0; i < 7; i++) {
       const date = new Date(today)
       date.setDate(today.getDate() + i)
@@ -61,28 +86,31 @@ export const ScheduleListPage: React.FC = () => {
   }, [searchParams.date])
 
   // 查询路线
-  const { data: routes = [], isLoading } = useQuery({
+  const { data: routeData, isLoading } = useQuery({
     queryKey: ['routes', 'search', { ...searchParams, date: selectedDate }],
     queryFn: async () => {
       const res = await routeClient.search.$get({
         query: {
-          startLocationId: searchParams.startLocationId,
-          endLocationId: searchParams.endLocationId,
+          startAreaIds: JSON.stringify(searchParams.startAreaIds),
+          endAreaIds: JSON.stringify(searchParams.endAreaIds),
           date: selectedDate,
           routeType: searchParams.routeType,
           sortBy: 'departureTime',
-          sortOrder: 'asc'
+          sortOrder: 'ASC'
         }
       })
       if (res.status !== 200) throw new Error('查询路线失败')
-      return await res.json()
+      const data = await res.json()
+      return data.data || { routes: [], activities: [] }
     },
-    enabled: !!selectedDate && !!searchParams.routeType
+    enabled: !!selectedDate && !!searchParams.routeType && searchParams.startAreaIds?.length > 0 && searchParams.endAreaIds?.length > 0
   })
 
+  const routes = routeData?.routes || []
+
   // 过滤包含指定活动的路线
   const filteredRoutes = routes.filter((route: Route) =>
-    route.activities.some(activity => activity.id === searchParams.activityId)
+    route.activityId === searchParams.activityId
   )
 
   // 处理日期选择
@@ -135,9 +163,8 @@ export const ScheduleListPage: React.FC = () => {
 
   // 获取活动名称
   const getActivityName = () => {
-    if (routes.length === 0) return ''
-    const route = routes[0] as Route
-    const activity = route.activities.find(a => a.id === searchParams.activityId)
+    if (!routeData?.activities || routeData.activities.length === 0) return ''
+    const activity = routeData.activities.find(a => a.id === searchParams.activityId)
     return activity?.name || ''
   }
 

+ 80 - 119
mini/src/pages/select-activity/ActivitySelectPage.tsx

@@ -1,61 +1,82 @@
-import React, { useState, useEffect } from 'react'
+import React from 'react'
 import { View, Text, Image, ScrollView } from '@tarojs/components'
 import { useRouter, navigateTo } from '@tarojs/taro'
 import { useQuery } from '@tanstack/react-query'
 import { routeClient } from '../../api'
-import { LocationSearch } from '../../components/LocationSearch'
 
 interface Activity {
   id: number
   name: string
   description?: string
+  type: 'departure' | 'return'
+  startDate: string
+  endDate: string
+  imageUrl?: string
   venueLocationId: number
-  venueLocation?: {
+  venueLocation: {
     id: number
     name: string
     provinceId: number
     cityId: number
     districtId: number
     address: string
+    latitude: string
+    longitude: string
+    province: {
+      id: number
+      name: string
+      level: number
+      code: string
+      isDisabled: number
+      isDeleted: number
+      createdBy?: any
+      updatedBy?: any
+      createdAt: string
+      updatedAt: string
+    }
+    city: {
+      id: number
+      name: string
+      level: number
+      code: string
+      isDisabled: number
+      isDeleted: number
+      createdBy?: any
+      updatedBy?: any
+      createdAt: string
+      updatedAt: string
+    }
+    district: {
+      id: number
+      name: string
+      level: number
+      code: string
+      isDisabled: number
+      isDeleted: number
+      createdBy?: any
+      updatedBy?: any
+      createdAt: string
+      updatedAt: string
+    }
+    isDisabled: number
+    isDeleted: number
+    createdBy?: any
+    updatedBy?: any
+    createdAt: string
+    updatedAt: string
   }
-  startDate: string
-  endDate: string
-  type: 'departure' | 'return'
-  imageUrl?: string
+  isDisabled: number
+  isDeleted: number
+  createdBy?: any
+  updatedBy?: any
+  createdAt: string
+  updatedAt: string
 }
 
-interface Route {
-  id: number
-  startLocation: {
-    name: string
-    province?: string
-    city?: string
-    district?: string
-  }
-  endLocation: {
-    name: string
-    province?: string
-    city?: string
-    district?: string
-  }
-  activities: Activity[]
-  routeType: 'departure' | 'return'
-}
 
-interface Location {
-  id: number
-  name: string
-  province?: string
-  city?: string
-  district?: string
-  address?: string
-}
 
 const ActivitySelectPage: React.FC = () => {
   const router = useRouter()
-  const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null)
-  const [startLocation, setStartLocation] = useState<Location | null>(null)
-  const [endLocation, setEndLocation] = useState<Location | null>(null)
 
   // 从路由参数获取查询条件
   const searchParams = {
@@ -67,40 +88,35 @@ const ActivitySelectPage: React.FC = () => {
 
   // 查询路线和关联活动
   const { data: routeData, isLoading } = useQuery({
-    queryKey: ['routes', 'search', searchParams, startLocation, endLocation],
+    queryKey: ['routes', 'search', searchParams],
     queryFn: async () => {
-      if (!startLocation?.id || !endLocation?.id) {
-        return { routes: [], activities: [] }
-      }
-
+      // 根据省市区查询该地区的路线和活动
       const res = await routeClient.search.$get({
         query: {
-          startLocationId: startLocation.id,
-          endLocationId: endLocation.id,
+          startAreaIds: JSON.stringify(searchParams.startAreaIds),
+          endAreaIds: JSON.stringify(searchParams.endAreaIds),
           date: searchParams.date,
-          routeType: 'all',
           sortBy: 'departureTime',
-          sortOrder: 'asc'
+          sortOrder: 'ASC'
         }
       })
       if (res.status !== 200) throw new Error('查询路线失败')
       const data = await res.json()
       return data.data || { routes: [], activities: [] }
     },
-    enabled: !!startLocation?.id && !!endLocation?.id
+    enabled: searchParams.startAreaIds?.length > 0 && searchParams.endAreaIds?.length > 0
   })
 
-  const routes = routeData?.routes || []
   const activities = routeData?.activities || []
 
   // 分离去程和返程活动
-  const departureActivities = activities
+  const departureActivities = (activities as Activity[])
     .filter((activity: Activity) => activity.type === 'departure')
     .filter((activity, index, self) =>
       index === self.findIndex(a => a.id === activity.id)
     )
 
-  const returnActivities = activities
+  const returnActivities = (activities as Activity[])
     .filter((activity: Activity) => activity.type === 'return')
     .filter((activity, index, self) =>
       index === self.findIndex(a => a.id === activity.id)
@@ -108,18 +124,11 @@ const ActivitySelectPage: React.FC = () => {
 
   // 选择活动
   const handleSelectActivity = (activity: Activity, routeType: 'departure' | 'return') => {
-    setSelectedActivity(activity)
-
-    if (!startLocation?.id || !endLocation?.id) {
-      console.log('请先选择出发地和目的地')
-      return
-    }
-
     // 导航到班次列表页面
     navigateTo({
       url: `/pages/schedule-list/ScheduleListPage?` +
-        `startLocationId=${startLocation.id}&` +
-        `endLocationId=${endLocation.id}&` +
+        `startAreaIds=${JSON.stringify(searchParams.startAreaIds)}&` +
+        `endAreaIds=${JSON.stringify(searchParams.endAreaIds)}&` +
         `date=${searchParams.date}&` +
         `vehicleType=${searchParams.vehicleType}&` +
         `activityId=${activity.id}&` +
@@ -130,23 +139,24 @@ const ActivitySelectPage: React.FC = () => {
   // 获取活动显示信息
   const getActivityDisplayInfo = (activity: Activity) => {
     const venue = activity.venueLocation
-    const locationParts = []
-    if (venue?.district) locationParts.push(venue.district)
-    if (venue?.city) locationParts.push(venue.city)
-    if (venue?.province) locationParts.push(venue.province)
+    const locationParts: string[] = []
+    if (venue.district?.name) locationParts.push(venue.district.name)
+    if (venue.city?.name) locationParts.push(venue.city.name)
+    if (venue.province?.name) locationParts.push(venue.province.name)
 
     return {
       location: locationParts.join(' · '),
-      address: venue?.address,
+      address: venue.address,
       date: new Date(activity.startDate).toLocaleDateString('zh-CN')
     }
   }
 
   // 获取路线信息显示
   const getRouteInfo = () => {
+    // 这里可以根据省市区ID获取地区名称,暂时使用默认值
     return {
-      fromCity: startLocation?.name || '出发地',
-      toCity: endLocation?.name || '目的地'
+      fromCity: '出发地',
+      toCity: '目的地'
     }
   }
 
@@ -172,62 +182,15 @@ const ActivitySelectPage: React.FC = () => {
         </Text>
       </View>
 
-      {/* 地点选择区域 */}
-      <View className="bg-white p-4 border-b border-gray-200">
-        <Text className="text-sm font-medium text-gray-700 mb-2 block">
-          请选择具体地点
-        </Text>
-        <View className="space-y-3">
-          {/* 出发地点选择 */}
-          <View>
-            <Text className="text-sm text-gray-600 mb-1 block">出发地点</Text>
-            <LocationSearch
-              value={startLocation}
-              onChange={setStartLocation}
-              placeholder="搜索出发地点"
-              areaFilter={{
-                provinceId: searchParams.startAreaIds?.[0],
-                cityId: searchParams.startAreaIds?.[1],
-                districtId: searchParams.startAreaIds?.[2]
-              }}
-            />
-          </View>
-
-          {/* 目的地点选择 */}
-          <View>
-            <Text className="text-sm text-gray-600 mb-1 block">目的地点</Text>
-            <LocationSearch
-              value={endLocation}
-              onChange={setEndLocation}
-              placeholder="搜索目的地点"
-              areaFilter={{
-                provinceId: searchParams.endAreaIds?.[0],
-                cityId: searchParams.endAreaIds?.[1],
-                districtId: searchParams.endAreaIds?.[2]
-              }}
-            />
-          </View>
-        </View>
-      </View>
 
       <ScrollView className="flex-1">
         <View className="p-4">
-          {!startLocation?.id || !endLocation?.id ? (
-            <View className="bg-white rounded-lg border border-gray-200 p-8 text-center">
-              <Text className="text-4xl mb-4">📍</Text>
-              <Text className="text-lg text-gray-600 block mb-2">请先选择出发地和目的地</Text>
-              <Text className="text-sm text-gray-500">
-                在上方搜索框中选择具体的出发地和目的地
-              </Text>
-            </View>
-          ) : (
-            <>
-              <Text className="text-lg font-bold text-gray-800 mb-2 block">
-                选择观看活动
-              </Text>
-              <Text className="text-sm text-gray-500 mb-4 block">
-                系统已为您自动匹配出发地和目的地的热门活动
-              </Text>
+          <Text className="text-lg font-bold text-gray-800 mb-2 block">
+            选择观看活动
+          </Text>
+          <Text className="text-sm text-gray-500 mb-4 block">
+            系统已为您自动匹配出发地和目的地的热门活动
+          </Text>
 
           {/* 去程活动区域 */}
           <View className="mb-6">
@@ -363,8 +326,6 @@ const ActivitySelectPage: React.FC = () => {
               </Text>
             </View>
           )}
-            </>
-          )}
         </View>
       </ScrollView>
     </View>

+ 2 - 1
src/server/modules/routes/route.service.ts

@@ -144,7 +144,8 @@ export class RouteService extends GenericCrudService<RouteEntity> {
     const activityIds = Array.from(new Set(filteredRoutes.map(route => route.activityId)));
     const activities = activityIds.length > 0
       ? await this.dataSource.getRepository(ActivityEntity).find({
-          where: { id: In(activityIds) }
+          where: { id: In(activityIds) },
+          relations: ['venueLocation', 'venueLocation.province', 'venueLocation.city', 'venueLocation.district']
         })
       : [];