mini-demo-migration-guide.md 22 KB

mini-demo 迁移指导规范

版本信息

版本 日期 描述 作者
2.1 2025-10-15 修正Taro测试位置,统一使用mini/tests/目录 Winston
2.0 2025-10-15 整合精确样式迁移内容,优化文档结构 Winston
1.0 2025-10-15 初始mini-demo迁移指导规范 Winston

概述

本文档提供从原生微信小程序(mini-demo)迁移到Taro + React技术栈的完整指导,包含百分百样式保留的精确迁移方案,确保迁移过程顺利且保持用户体验一致性。

迁移策略

整体迁移方法

  • 渐进式迁移: 逐个页面迁移,保持业务连续性
  • 技术栈转换: 原生小程序 → Taro + React + TypeScript
  • 样式重构: WXSS → Tailwind CSS + shadcn/ui(精确样式保留)
  • 数据集成: 模拟数据 → 真实后端API

迁移优先级

  1. 高优先级: 核心出行流程页面
  2. 中优先级: 用户管理相关页面
  3. 低优先级: 辅助功能和设置页面

页面迁移对照表

核心页面迁移

原页面 目标页面 状态 复杂度
mini-demo/pages/home/home mini/src/pages/home/index.tsx ✅ 待迁移 中等
mini-demo/pages/select-activity/select-activity mini/src/pages/activity-select/index.tsx ✅ 待迁移 中等
mini-demo/pages/schedule-list/schedule-list mini/src/pages/schedule-list/index.tsx ✅ 待迁移 中等
mini-demo/pages/order-detail/order-detail mini/src/pages/order-detail/index.tsx 🔄 规划中
mini-demo/pages/passenger/passenger mini/src/pages/passenger/index.tsx 🔄 规划中 中等

组件映射规范

基础组件映射

原生小程序组件 Taro组件 说明
<view> <View> 视图容器
<text> <Text> 文本组件
<button> <Button> 按钮组件
<input> <Input> 输入框组件
<image> <Image> 图片组件
<scroll-view> <ScrollView> 滚动视图
<swiper> <Swiper> 轮播组件

自定义组件映射

原组件 新组件 位置
轮播图组件 BannerCarousel mini/src/components/home/BannerCarousel.tsx
出行方式选择 TravelTypeSelector mini/src/components/home/TravelTypeSelector.tsx
地点选择器 LocationPicker mini/src/components/home/LocationPicker.tsx
活动筛选器 ActivityFilter mini/src/components/home/ActivityFilter.tsx
路线卡片 RouteCard mini/src/components/home/RouteCard.tsx

样式转换规范

WXSS → Tailwind CSS 转换

原WXSS样式:

/* mini-demo/pages/home/home.wxss */
.container {
  padding: 20rpx;
  background-color: #ffffff;
}

.title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333333;
}

.button {
  background-color: #007AFF;
  color: #ffffff;
  border-radius: 8rpx;
  padding: 20rpx 40rpx;
}

转换后的Tailwind样式:

// mini/src/pages/home/index.tsx
<View className="p-5 bg-white">
  <Text className="text-[32rpx] font-bold text-gray-900">标题</Text>
  <Button className="bg-blue-500 text-white rounded-[8rpx] px-10 py-5">
    按钮
  </Button>
</View>

常用样式转换表

WXSS属性 Tailwind类名 示例
width: 100% w-full w-full
height: 100% h-full h-full
margin: 20rpx m-5 m-5
padding: 20rpx p-5 p-5
background-color: #fff bg-white bg-white
color: #333 text-gray-900 text-gray-900
font-size: 32rpx text-[32rpx] text-[32rpx]
border-radius: 8rpx rounded-[8rpx] rounded-[8rpx]

数据层迁移

数据模型转换

原数据格式:

// mini-demo 模拟数据
const routeData = {
  id: 1,
  startPoint: "北京",
  endPoint: "上海",
  departureTime: "2025-10-15 08:00:00",
  price: 200,
  vehicleType: "商务车",
  travelMode: "包车"
}

新数据格式:

// TypeScript接口定义
interface Route {
  id: number;
  startPoint: string;
  endPoint: string;
  departureTime: Date;
  price: number;
  vehicleType: VehicleType;
  travelMode: TravelMode; // 新增:出行方式(拼车/包车)
}

// API调用
const { data: routes } = useQuery({
  queryKey: ['routes', queryParams],
  queryFn: () => api.routes.list(queryParams)
})

API集成

数据获取模式:

// 使用React Query管理服务端状态
import { useQuery } from '@tanstack/react-query'
import { api } from '@/utils/api'

export function useRoutes(queryParams: RouteQueryParams) {
  return useQuery({
    queryKey: ['routes', queryParams],
    queryFn: () => api.routes.list(queryParams),
    staleTime: 5 * 60 * 1000, // 5分钟缓存
  })
}

页面迁移详细指导

首页迁移 (home/home)

原页面结构分析:

<!-- mini-demo/pages/home/home.wxml -->
<view class="container">
  <!-- 轮播海报 -->
  <swiper class="banner-swiper" indicator-dots="true" autoplay="true">
    <swiper-item wx:for="{{bannerList}}" wx:key="id">
      <image src="{{item.imageUrl}}" mode="aspectFill"></image>
    </swiper-item>
  </swiper>

  <!-- 出行方式选择 -->
  <view class="travel-type-selector">
    <view class="type-item {{activeType === 'carpool' ? 'active' : ''}}"
          bindtap="onTypeSelect" data-type="carpool">
      大巴拼车
    </view>
    <!-- 其他出行方式 -->
  </view>

  <!-- 地点选择 -->
  <view class="location-picker">
    <view class="start-location" bindtap="onStartLocationSelect">
      {{startLocation || '选择出发地'}}
    </view>
    <view class="end-location" bindtap="onEndLocationSelect">
      {{endLocation || '选择目的地'}}
    </view>
  </view>

  <!-- 日期选择 -->
  <view class="date-picker" bindtap="onDateSelect">
    {{selectedDate || '选择日期'}}
  </view>

  <!-- 查询按钮 -->
  <button class="search-btn" bindtap="onSearch">查询路线</button>
</view>

迁移后的React组件:

// mini/src/pages/home/index.tsx
import { View, Text, Button } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { BannerCarousel } from '@/components/home/BannerCarousel'
import { TravelTypeSelector } from '@/components/home/TravelTypeSelector'
import { LocationPicker } from '@/components/home/LocationPicker'
import { DatePicker } from '@/components/home/DatePicker'

export default function HomePage() {
  const [travelType, setTravelType] = useState<TravelType>('carpool')
  const [startLocation, setStartLocation] = useState('')
  const [endLocation, setEndLocation] = useState('')
  const [selectedDate, setSelectedDate] = useState<Date>(new Date())

  useLoad(() => {
    console.log('HomePage loaded')
  })

  const handleSearch = () => {
    // 导航到活动选择页面
    navigateTo({
      url: `/pages/activity-select?type=${travelType}&start=${startLocation}&end=${endLocation}&date=${selectedDate.toISOString()}`
    })
  }

  return (
    <View className="min-h-screen bg-gray-50">
      {/* 轮播海报 - MVP限制:使用固定静态图片 */}
      <BannerCarousel banners={[{ id: 1, imageUrl: '/static/home-banner.jpg' }]} />

      {/* 出行方式选择 */}
      <TravelTypeSelector
        value={travelType}
        onChange={setTravelType}
      />

      {/* 地点选择器 */}
      <LocationPicker
        startLocation={startLocation}
        endLocation={endLocation}
        onStartChange={setStartLocation}
        onEndChange={setEndLocation}
      />

      {/* 日期选择器 */}
      <DatePicker
        value={selectedDate}
        onChange={setSelectedDate}
      />

      {/* 查询按钮 */}
      <Button
        className="w-full bg-blue-500 text-white py-3 rounded-lg mt-6"
        onClick={handleSearch}
        disabled={!startLocation || !endLocation}
      >
        查询路线
      </Button>
    </View>
  )
}

活动选择页面迁移 (select-activity/select-activity)

迁移要点:

// mini/src/pages/activity-select/index.tsx
export default function ActivitySelectPage() {
  const router = useRouter()
  const { type, start, end, date } = router.params

  // 根据查询参数获取活动列表
  const { data: activities, isLoading } = useActivities({
    type: type as ActivityType,
    startPoint: start,
    endPoint: end,
    date: date ? new Date(date) : new Date()
  })

  return (
    <View className="min-h-screen bg-white">
      {/* 活动类型筛选 */}
      <ActivityFilter
        value={type as ActivityType}
        onChange={(newType) => {
          // 更新URL参数并重新查询
        }}
      />

      {/* 活动列表 */}
      {isLoading ? (
        <LoadingSpinner />
      ) : (
        <View className="space-y-4 p-4">
          {activities?.map(activity => (
            <ActivityCard
              key={activity.id}
              activity={activity}
              onSelect={() => {
                // 导航到班次列表页面
                navigateTo({
                  url: `/pages/schedule-list?activityId=${activity.id}`
                })
              }}
            />
          ))}
        </View>
      )}
    </View>
  )
}

班次列表页面迁移 (schedule-list/schedule-list)

迁移要点:

// mini/src/pages/schedule-list/index.tsx
export default function ScheduleListPage() {
  const router = useRouter()
  const { activityId } = router.params

  const [sortBy, setSortBy] = useState<'price' | 'departureTime'>('departureTime')
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')

  // 获取路线列表
  const { data: routes, isLoading } = useRoutes({
    activityId: Number(activityId),
    sortBy,
    sortOrder
  })

  return (
    <View className="min-h-screen bg-gray-50">
      {/* 排序和筛选工具栏 */}
      <Toolbar
        sortBy={sortBy}
        sortOrder={sortOrder}
        onSortChange={setSortBy}
        onOrderChange={setSortOrder}
      />

      {/* 路线列表 */}
      <View className="space-y-3 p-4">
        {routes?.map(route => (
          <RouteCard
            key={route.id}
            route={route}
            onSelect={() => {
              // 导航到订单确认页面
              navigateTo({
                url: `/pages/order-confirm?routeId=${route.id}`
              })
            }}
          />
        ))}
      </View>
    </View>
  )
}

事件处理迁移

事件绑定转换

原事件 React事件 示例
bindtap onClick <Button onClick={handleClick}>
bindinput onInput <Input onInput={handleInput}>
bindchange onChange <Picker onChange={handleChange}>
catchtap onClick + 阻止冒泡 在事件处理函数中处理

数据传递转换

原小程序方式:

<view bindtap="onItemTap" data-id="{{item.id}}" data-name="{{item.name}}">
  点击我
</view>
// 页面JS
onItemTap: function(e) {
  const id = e.currentTarget.dataset.id
  const name = e.currentTarget.dataset.name
  // 处理逻辑
}

React方式:

<View onClick={() => onItemTap(item.id, item.name)}>
  点击我
</View>
// 组件函数
const onItemTap = (id: number, name: string) => {
  // 处理逻辑
}

状态管理迁移

页面状态转换

原小程序状态:

// mini-demo页面JS
Page({
  data: {
    bannerList: [],
    activeType: 'carpool',
    startLocation: '',
    endLocation: '',
    selectedDate: ''
  },

  onLoad: function() {
    this.setData({
      bannerList: mockBannerData
    })
  },

  onTypeSelect: function(e) {
    this.setData({
      activeType: e.currentTarget.dataset.type
    })
  }
})

React状态管理:

// React组件
import { useState, useEffect } from 'react'

export default function HomePage() {
  const [bannerList, setBannerList] = useState<Banner[]>([])
  const [activeType, setActiveType] = useState<TravelType>('carpool')
  const [startLocation, setStartLocation] = useState('')
  const [endLocation, setEndLocation] = useState('')
  const [selectedDate, setSelectedDate] = useState<Date>(new Date())

  useEffect(() => {
    // 初始化数据
    setBannerList([{ id: 1, imageUrl: '/static/home-banner.jpg' }])
  }, [])

  const handleTypeSelect = (type: TravelType) => {
    setActiveType(type)
  }

  return (
    // JSX内容
  )
}

测试迁移指导

单元测试迁移

原测试结构:

// mini-demo测试(通常较少)

新测试结构:

// mini/tests/pages/home/index.test.tsx
import TestUtils from '@tarojs/test-utils-react'
import HomePage from '@/pages/home/index'

const testUtils = new TestUtils()

describe('HomePage', () => {
  it('renders travel type selector', async () => {
    await testUtils.mount(HomePage)
    const travelTypeSelector = await testUtils.queries.waitForQuerySelector('.travel-type-selector')
    expect(travelTypeSelector).toBeInTheDocument()
  })

  it('handles location selection', async () => {
    // 测试地点选择功能
    await testUtils.mount(HomePage)
    const locationPicker = await testUtils.queries.waitForQuerySelector('.location-picker')
    expect(locationPicker).toBeInTheDocument()
  })
})

E2E测试迁移

Playwright测试:

// tests/e2e/travel-flow/home-page.spec.ts
test('首页查询流程', async ({ page }) => {
  await page.goto('/pages/home/index')

  // 选择出行方式
  await page.click('[data-testid="travel-type-carpool"]')

  // 选择出发地和目的地
  await page.fill('[data-testid="start-location"]', '北京')
  await page.fill('[data-testid="end-location"]', '上海')

  // 点击查询
  await page.click('[data-testid="search-button"]')

  // 验证跳转到活动选择页面
  await expect(page).toHaveURL(/.*pages\/activity-select.*/)
})

常见问题解决

样式兼容性问题

问题: 某些Tailwind类名在小程序中不生效 解决方案: 使用 weapp-tailwindcss 插件进行适配

// tailwind.config.js
module.exports = {
  corePlugins: {
    preflight: false, // 禁用preflight
  },
  // ...其他配置
}

平台差异处理

问题: 某些API在不同平台行为不一致 解决方案: 使用平台检测和条件渲染

import { isWeapp, isH5 } from '@/utils/platform'

export function PlatformSpecificComponent() {
  return (
    <View>
      {isWeapp && <Text>微信小程序特有功能</Text>}
      {isH5 && <Text>H5特有功能</Text>}
    </View>
  )
}

性能优化

问题: 页面加载性能问题 解决方案:

  • 使用React.memo优化组件重渲染
  • 合理使用React Query缓存
  • 图片懒加载和压缩

迁移检查清单

页面迁移检查

  • 组件结构正确迁移
  • 样式正确转换
  • 事件处理正确绑定
  • 状态管理正确实现
  • 数据获取正确集成
  • 路由跳转正常工作
  • 平台兼容性测试通过

功能验证检查

  • 用户交互流程正常
  • 数据展示正确
  • 错误处理完善
  • 加载状态处理
  • 空状态处理

质量保证检查

  • 单元测试覆盖
  • 集成测试通过
  • E2E测试通过
  • 代码审查完成
  • 性能测试通过

精确样式迁移指南

设计系统核心规范

颜色系统

/* 主色调 */
--primary-color: #4A90C2
--primary-gradient: linear-gradient(135deg, #4A90C2 0%, #357ABD 100%)
--secondary-color: #5B9BD5

/* 包车主题 */
--charter-color: #d4af37
--charter-gradient: linear-gradient(135deg, #d4af37 0%, #f4d03f 100%)
--charter-bg: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)

/* 功能色 */
--success-color: #52c41a
--warning-color: #fa8c16
--error-color: #ff4d4f
--info-color: #1890ff

/* 中性色 */
--text-color: #333
--text-secondary: #666
--text-placeholder: #8E8E93
--border-color: #E5E5EA
--bg-color: #F8F9FA
--card-bg: #FFFFFF

阴影系统

--shadow-light: 0 2rpx 12rpx rgba(0, 0, 0, 0.08)
--shadow-medium: 0 4rpx 20rpx rgba(0, 0, 0, 0.12)
--shadow-heavy: 0 8rpx 32rpx rgba(0, 0, 0, 0.16)

Tailwind CSS 精确配置

tailwind.config.js 扩展配置

module.exports = {
  theme: {
    extend: {
      colors: {
        // 精确匹配 mini-demo 颜色
        'primary': '#4A90C2',
        'primary-dark': '#357ABD',
        'secondary': '#5B9BD5',
        'charter': '#d4af37',
        'charter-light': '#f4d03f',
        'charter-dark': '#1a1a1a',
        'charter-bg': '#2d2d2d',
        'success': '#52c41a',
        'warning': '#fa8c16',
        'error': '#ff4d4f',
        'info': '#1890ff',
        'text-primary': '#333',
        'text-secondary': '#666',
        'text-placeholder': '#8E8E93',
        'border': '#E5E5EA',
        'bg': '#F8F9FA',
        'card': '#FFFFFF',
      },
      borderRadius: {
        'card': '20rpx',
        'button': '50rpx',
        'small': '12rpx',
        'medium': '16rpx',
      },
      boxShadow: {
        'light': '0 2rpx 12rpx rgba(0, 0, 0, 0.08)',
        'medium': '0 4rpx 20rpx rgba(0, 0, 0, 0.12)',
        'heavy': '0 8rpx 32rpx rgba(0, 0, 0, 0.16)',
        'primary': '0 8rpx 24rpx rgba(74, 144, 194, 0.4)',
        'charter': '0 8rpx 24rpx rgba(212, 175, 55, 0.4)',
      },
      spacing: {
        'card': '32rpx',
        'section': '24rpx',
        'button': '24rpx',
      }
    }
  }
}

样式迁移对照表

通用样式类

原WXSS类名 Tailwind类名 说明
.card bg-card rounded-card shadow-medium p-card border border-border 卡片样式
.big-btn bg-gradient-to-br from-primary to-primary-dark text-white rounded-button p-button shadow-primary 大按钮
.text-primary text-primary 主色调文字
.text-charter text-charter 包车主题文字

首页样式迁移

顶部轮播图

// 原WXSS
.banner-container { height: 25vh; }
.banner-swiper { width: 100%; height: 100%; }

// Tailwind迁移
<View className="h-[25vh] w-full">
  <Swiper className="w-full h-full">
    {/* 轮播内容 */}
  </Swiper>
</View>

出行方式选择

// 原WXSS
.service-tabs {
  background: rgba(255, 255, 255, 0.95);
  margin: 12rpx 20rpx;
  border-radius: 50rpx;
  padding: 6rpx;
}

// Tailwind迁移
<View className="bg-white/95 m-[12rpx] mx-[20rpx] rounded-button p-[6rpx]">
  {/* tab内容 */}
</View>

地点选择器

// 原WXSS
.route-item {
  background: #ffffff;
  border-radius: 16rpx;
  padding: 20rpx 16rpx;
  border: 2rpx solid #e1e8ed;
  min-height: 88rpx;
}

// Tailwind迁移
<View className="bg-white rounded-medium p-[20rpx_16rpx] border-2 border-[#e1e8ed] min-h-[88rpx]">
  {/* 地点选择内容 */}
</View>

活动选择页面样式迁移

头部信息

// 原WXSS
.header-info {
  background: linear-gradient(135deg, #4A90C2 0%, #357ABD 100%);
  padding: 40rpx 32rpx;
  color: #fff;
}

// Tailwind迁移
<View className="bg-gradient-to-br from-primary to-primary-dark p-[40rpx_32rpx] text-white">
  {/* 头部内容 */}
</View>

行程区域

// 原WXSS
.trip-header.departure {
  background: linear-gradient(135deg, #4A90C2 0%, #357ABD 100%);
  color: #fff;
}

// Tailwind迁移
<View className="bg-gradient-to-br from-primary to-primary-dark text-white">
  {/* 去程头部 */}
</View>

班次列表页面样式迁移

包车卡片

// 原WXSS
.charter-card {
  background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
  border: 2rpx solid #d4af37;
  box-shadow: 0 8rpx 32rpx rgba(212, 175, 55, 0.3);
}

// Tailwind迁移
<View className="bg-gradient-to-br from-charter-dark to-charter-bg border-2 border-charter shadow-charter">
  {/* 包车卡片内容 */}
</View>

预订按钮

// 原WXSS
.book-btn {
  background: linear-gradient(135deg, #4A90C2 0%, #357ABD 100%);
  color: #fff;
  border-radius: 50rpx;
  padding: 24rpx 0;
  box-shadow: 0 8rpx 24rpx rgba(74, 144, 194, 0.4);
}

// Tailwind迁移
<Button className="bg-gradient-to-br from-primary to-primary-dark text-white rounded-button p-[24rpx_0] shadow-primary">
  预订
</Button>

订单页面样式迁移

包车主题

// 原WXSS
.charter-theme {
  background: linear-gradient(180deg, #1a1a1a 0%, #2d2d2d 100%);
}

// Tailwind迁移
<View className="bg-gradient-to-b from-charter-dark to-charter-bg">
  {/* 包车主题内容 */}
</View>

支付区域

// 原WXSS
.pay-section {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: #fff;
  padding: 24rpx 32rpx;
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.1);
}

// Tailwind迁移
<View className="fixed bottom-0 left-0 right-0 bg-white p-[24rpx_32rpx] shadow-[0_-4rpx_20rpx_rgba(0,0,0,0.1)]">
  {/* 支付内容 */}
</View>

我的页面样式迁移

用户信息区域

// 原WXSS
.user-section {
  background: linear-gradient(135deg, #4A90C2 0%, #357ABD 100%);
  padding: 40rpx 32rpx 32rpx 32rpx;
  color: #fff;
}

// Tailwind迁移
<View className="bg-gradient-to-br from-primary to-primary-dark p-[40rpx_32rpx_32rpx_32rpx] text-white">
  {/* 用户信息内容 */}
</View>

会员卡片

// 原WXSS
.member-card {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 20rpx;
  padding: 24rpx;
  color: #fff;
  box-shadow: 0 6rpx 24rpx rgba(102, 126, 234, 0.3);
}

// Tailwind迁移
<View className="bg-gradient-to-br from-[#667eea] to-[#764ba2] rounded-card p-[24rpx] text-white shadow-[0_6rpx_24rpx_rgba(102,126,234,0.3)]">
  {/* 会员卡片内容 */}
</View>

响应式设计迁移

媒体查询转换

// 原WXSS
@media screen and (max-height: 667px) {
  .banner-container { height: 22vh; }
  .booking-section { padding: 16rpx; }
}

// Tailwind迁移
<View className="h-[25vh] sm:h-[22vh]">
  {/* 响应式内容 */}
</View>

动画效果迁移

渐入动画

// 原WXSS
.fade-in {
  animation: fadeIn 0.3s ease-in-out;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20rpx); }
  to { opacity: 1; transform: translateY(0); }
}

// Tailwind迁移
<View className="animate-[fadeIn_0.3s_ease-in-out]">
  {/* 动画内容 */}
</View>

// 在全局CSS中添加
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20rpx); }
  to { opacity: 1; transform: translateY(0); }
}

视觉一致性检查清单

  • 颜色系统完全匹配
  • 渐变效果一致
  • 阴影层次相同
  • 圆角设计一致
  • 间距布局相同
  • 字体大小和权重一致
  • 按钮交互效果相同
  • 包车主题效果一致

文档状态: 正式版 下次评审: 2025-11-15