Files
everything-is-suitable/everything-is-suitable-uniapp/docs/ITERATION_PLAN_UNIAPP.md
T
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

17 KiB

Uniapp模块迭代计划 - 下一阶段优化

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 实现首页搜索功能,完善组件测试覆盖率,提升用户体验

Architecture: 基于uni-app + Vue 3 + TypeScript架构,实现跨平台搜索功能,增强组件可测试性

Tech Stack: uni-app, Vue 3.5, TypeScript 5.x, Pinia, Playwright, Vitest


背景

根据全系统功能对接综合评估,Uniapp模块存在以下待优化事项:

  • 首页搜索功能入口存在但未实现(测试失败)
  • 组件测试覆盖率可进一步提升
  • 搜索服务已实现但未与UI集成

Task 1: 实现首页搜索功能入口

Files:

  • Modify: src/pages/index/index.vue
  • Modify: src/components/SearchConditionPanel/index.vue
  • Test: src/__tests__/components/SearchConditionPanel.test.ts

Step 1: 编写搜索功能测试

// src/__tests__/components/SearchConditionPanel.test.ts
import { mount } from '@vue/test-utils';
import SearchConditionPanel from '@/components/SearchConditionPanel/index.vue';

describe('SearchConditionPanel', () => {
  it('应该显示搜索输入框', () => {
    const wrapper = mount(SearchConditionPanel);
    expect(wrapper.find('[data-testid="search-input"]').exists()).toBe(true);
  });

  it('点击搜索按钮应触发搜索事件', async () => {
    const wrapper = mount(SearchConditionPanel);
    const searchInput = wrapper.find('[data-testid="search-input"]');
    await searchInput.setValue('测试搜索');
    
    const searchButton = wrapper.find('[data-testid="search-button"]');
    await searchButton.trigger('click');
    
    expect(wrapper.emitted('search')).toBeTruthy();
    expect(wrapper.emitted('search')![0]).toEqual(['测试搜索']);
  });

  it('空搜索应显示提示信息', async () => {
    const wrapper = mount(SearchConditionPanel);
    const searchButton = wrapper.find('[data-testid="search-button"]');
    await searchButton.trigger('click');
    
    expect(wrapper.find('.search-hint').exists()).toBe(true);
  });
});

Step 2: 运行测试验证失败

Run: cd everything-is-suitable-uniapp && npm run test -- SearchConditionPanel

Expected: FAIL (功能未实现)

Step 3: 更新首页添加搜索入口

<!-- src/pages/index/index.vue -->
<template>
  <view class="index-page">
    <view class="search-section">
      <view class="search-bar" @click="handleSearchClick">
        <text class="search-icon">🔍</text>
        <text class="search-placeholder">搜索老黄历日历...</text>
      </view>
    </view>
    
    <SearchConditionPanel
      v-if="showSearchPanel"
      :visible="showSearchPanel"
      @search="handleSearch"
      @close="showSearchPanel = false"
    />
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import SearchConditionPanel from '@/components/SearchConditionPanel/index.vue';

const showSearchPanel = ref(false);

const handleSearchClick = () => {
  showSearchPanel.value = true;
};

const handleSearch = (keyword: string) => {
  uni.navigateTo({
    url: `/pages/almanac-search/index?keyword=${encodeURIComponent(keyword)}`
  });
  showSearchPanel.value = false;
};
</script>

<style lang="scss" scoped>
.search-section {
  padding: 20rpx 30rpx;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.search-bar {
  display: flex;
  align-items: center;
  padding: 20rpx 30rpx;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 40rpx;
  
  .search-icon {
    margin-right: 20rpx;
    font-size: 32rpx;
  }
  
  .search-placeholder {
    color: #999;
    font-size: 28rpx;
  }
}
</style>

Step 4: 完善搜索条件面板组件

<!-- src/components/SearchConditionPanel/index.vue -->
<template>
  <view v-if="visible" class="search-panel-overlay" @click="handleClose">
    <view class="search-panel" @click.stop>
      <view class="search-header">
        <text class="title">搜索条件</text>
        <text class="close-btn" @click="handleClose"></text>
      </view>
      
      <view class="search-content">
        <view class="search-input-wrapper">
          <input
            v-model="searchKeyword"
            class="search-input"
            type="text"
            placeholder="请输入搜索关键词"
            data-testid="search-input"
            @confirm="handleSearch"
          />
        </view>
        
        <view v-if="showHint" class="search-hint">
          <text>请输入搜索关键词</text>
        </view>
        
        <view class="search-history" v-if="searchHistory.length > 0">
          <text class="history-title">搜索历史</text>
          <view class="history-tags">
            <text
              v-for="(item, index) in searchHistory"
              :key="index"
              class="history-tag"
              @click="handleHistoryClick(item)"
            >
              {{ item }}
            </text>
          </view>
        </view>
      </view>
      
      <view class="search-footer">
        <button class="search-button" data-testid="search-button" @click="handleSearch">
          搜索
        </button>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

const props = defineProps<{
  visible: boolean;
}>();

const emit = defineEmits<{
  search: [keyword: string];
  close: [];
}>();

const searchKeyword = ref('');
const showHint = ref(false);
const searchHistory = ref<string[]>([]);

const handleSearch = () => {
  if (!searchKeyword.value.trim()) {
    showHint.value = true;
    return;
  }
  
  showHint.value = false;
  
  // 添加到搜索历史
  if (!searchHistory.value.includes(searchKeyword.value)) {
    searchHistory.value.unshift(searchKeyword.value);
    if (searchHistory.value.length > 10) {
      searchHistory.value.pop();
    }
  }
  
  emit('search', searchKeyword.value);
};

const handleHistoryClick = (keyword: string) => {
  searchKeyword.value = keyword;
  handleSearch();
};

const handleClose = () => {
  emit('close');
};
</script>

Step 5: 运行测试验证通过

Run: cd everything-is-suitable-uniapp && npm run test -- SearchConditionPanel

Expected: PASS

Step 6: Commit

git add src/pages/index/index.vue
git add src/components/SearchConditionPanel/index.vue
git add src/__tests__/components/SearchConditionPanel.test.ts
git commit -m "feat(uniapp): implement search functionality on index page"

Task 2: 集成搜索服务

Files:

  • Modify: src/pages/almanac-search/index.vue
  • Modify: src/services/searchService.ts
  • Test: src/__tests__/services/searchService.test.ts

Step 1: 编写搜索服务集成测试

// src/__tests__/services/searchService.test.ts
import { searchService } from '@/services/searchService';

describe('searchService', () => {
  it('应该返回搜索结果', async () => {
    const result = await searchService.search('黄历');
    expect(result).toBeDefined();
    expect(result.items).toBeInstanceOf(Array);
  });

  it('空关键词应返回空结果', async () => {
    const result = await searchService.search('');
    expect(result.items).toHaveLength(0);
  });

  it('应该支持分页', async () => {
    const result = await searchService.search('黄历', { page: 1, pageSize: 10 });
    expect(result.pagination).toBeDefined();
    expect(result.pagination.page).toBe(1);
  });
});

Step 2: 运行测试验证

Run: cd everything-is-suitable-uniapp && npm run test -- searchService

Expected: PASS (服务已实现)

Step 3: 更新搜索结果页面

<!-- src/pages/almanac-search/index.vue -->
<template>
  <view class="search-result-page">
    <view class="search-header">
      <view class="search-bar">
        <input
          v-model="keyword"
          class="search-input"
          type="text"
          placeholder="搜索老黄历"
          @confirm="handleSearch"
        />
        <text class="search-btn" @click="handleSearch">搜索</text>
      </view>
    </view>
    
    <view class="search-content">
      <view v-if="loading" class="loading">
        <text>搜索中...</text>
      </view>
      
      <view v-else-if="results.length === 0" class="empty">
        <text>暂无搜索结果</text>
      </view>
      
      <view v-else class="result-list">
        <SearchResultCard
          v-for="item in results"
          :key="item.id"
          :data="item"
          @click="handleItemClick(item)"
        />
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { searchService } from '@/services/searchService';
import SearchResultCard from '@/components/SearchResultCard/index.vue';

const keyword = ref('');
const results = ref<any[]>([]);
const loading = ref(false);

onMounted(() => {
  const pages = getCurrentPages();
  const currentPage = pages[pages.length - 1];
  keyword.value = currentPage.options?.keyword || '';
  if (keyword.value) {
    handleSearch();
  }
});

const handleSearch = async () => {
  if (!keyword.value.trim()) return;
  
  loading.value = true;
  try {
    const result = await searchService.search(keyword.value);
    results.value = result.items;
  } catch (error) {
    console.error('搜索失败:', error);
    uni.showToast({ title: '搜索失败', icon: 'none' });
  } finally {
    loading.value = false;
  }
};

const handleItemClick = (item: any) => {
  uni.navigateTo({
    url: `/pages/almanac/index?id=${item.id}`
  });
};
</script>

Step 4: Commit

git add src/pages/almanac-search/index.vue
git add src/__tests__/services/searchService.test.ts
git commit -m "feat(uniapp): integrate search service with search result page"

Task 3: 完善组件测试覆盖率

Files:

  • Create: src/__tests__/components/AlmanacCard.test.ts
  • Create: src/__tests__/components/CalendarCard.test.ts
  • Create: src/__tests__/components/LoadingIndicator.test.ts

Step 1: 编写老黄历卡片组件测试

// src/__tests__/components/AlmanacCard.test.ts
import { mount } from '@vue/test-utils';
import AlmanacCard from '@/components/AlmanacCard/AlmanacCard.vue';

describe('AlmanacCard', () => {
  const mockData = {
    date: '2024-01-01',
    lunarDate: '腊月初一',
    suit: ['嫁娶', '出行'],
    avoid: ['动土', '安葬'],
    zodiac: '龙'
  };

  it('应该正确渲染日期信息', () => {
    const wrapper = mount(AlmanacCard, {
      props: { data: mockData }
    });
    
    expect(wrapper.text()).toContain('2024-01-01');
    expect(wrapper.text()).toContain('腊月初一');
  });

  it('应该显示宜忌信息', () => {
    const wrapper = mount(AlmanacCard, {
      props: { data: mockData }
    });
    
    expect(wrapper.text()).toContain('嫁娶');
    expect(wrapper.text()).toContain('动土');
  });

  it('应该显示生肖信息', () => {
    const wrapper = mount(AlmanacCard, {
      props: { data: mockData }
    });
    
    expect(wrapper.text()).toContain('龙');
  });

  it('点击应触发事件', async () => {
    const wrapper = mount(AlmanacCard, {
      props: { data: mockData }
    });
    
    await wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeTruthy();
  });
});

Step 2: 编写日历卡片组件测试

// src/__tests__/components/CalendarCard.test.ts
import { mount } from '@vue/test-utils';
import CalendarCard from '@/components/CalendarCard/CalendarCard.vue';

describe('CalendarCard', () => {
  const mockDate = new Date('2024-01-15');

  it('应该正确渲染日期', () => {
    const wrapper = mount(CalendarCard, {
      props: { date: mockDate }
    });
    
    expect(wrapper.text()).toContain('15');
  });

  it('应该显示农历信息', () => {
    const wrapper = mount(CalendarCard, {
      props: { date: mockDate, lunarInfo: '腊月初五' }
    });
    
    expect(wrapper.text()).toContain('腊月初五');
  });

  it('今天应该有特殊样式', () => {
    const today = new Date();
    const wrapper = mount(CalendarCard, {
      props: { date: today, isToday: true }
    });
    
    expect(wrapper.classes()).toContain('is-today');
  });

  it('选中状态应有特殊样式', async () => {
    const wrapper = mount(CalendarCard, {
      props: { date: mockDate, selected: false }
    });
    
    expect(wrapper.classes()).not.toContain('is-selected');
    
    await wrapper.setProps({ selected: true });
    expect(wrapper.classes()).toContain('is-selected');
  });
});

Step 3: 编写加载指示器组件测试

// src/__tests__/components/LoadingIndicator.test.ts
import { mount } from '@vue/test-utils';
import LoadingIndicator from '@/components/LoadingIndicator/index.vue';

describe('LoadingIndicator', () => {
  it('默认应该显示加载动画', () => {
    const wrapper = mount(LoadingIndicator);
    expect(wrapper.find('.loading-spinner').exists()).toBe(true);
  });

  it('应该支持不同尺寸', () => {
    const wrapper = mount(LoadingIndicator, {
      props: { size: 'large' }
    });
    
    expect(wrapper.classes()).toContain('size-large');
  });

  it('应该支持自定义提示文字', () => {
    const wrapper = mount(LoadingIndicator, {
      props: { text: '加载中...' }
    });
    
    expect(wrapper.text()).toContain('加载中...');
  });

  it('隐藏时不应渲染', () => {
    const wrapper = mount(LoadingIndicator, {
      props: { visible: false }
    });
    
    expect(wrapper.find('.loading-indicator').exists()).toBe(false);
  });
});

Step 4: 运行所有组件测试

Run: cd everything-is-suitable-uniapp && npm run test

Expected: PASS

Step 5: Commit

git add src/__tests__/components/AlmanacCard.test.ts
git add src/__tests__/components/CalendarCard.test.ts
git add src/__tests__/components/LoadingIndicator.test.ts
git commit -m "test(uniapp): add comprehensive component tests for better coverage"

Task 4: 添加E2E测试

Files:

  • Create: e2e/search.spec.ts
  • Modify: playwright.config.ts

Step 1: 编写搜索功能E2E测试

// e2e/search.spec.ts
import { test, expect } from '@playwright/test';

test.describe('搜索功能', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('首页应显示搜索入口', async ({ page }) => {
    await expect(page.locator('.search-bar')).toBeVisible();
  });

  test('点击搜索入口应打开搜索面板', async ({ page }) => {
    await page.click('.search-bar');
    await expect(page.locator('.search-panel')).toBeVisible();
  });

  test('输入关键词并搜索应跳转到搜索结果页', async ({ page }) => {
    await page.click('.search-bar');
    await page.fill('[data-testid="search-input"]', '黄历');
    await page.click('[data-testid="search-button"]');
    
    await expect(page).toHaveURL(/almanac-search/);
    await expect(page.locator('.result-list')).toBeVisible();
  });

  test('搜索历史应正确显示', async ({ page }) => {
    await page.click('.search-bar');
    await page.fill('[data-testid="search-input"]', '测试搜索');
    await page.click('[data-testid="search-button"]');
    
    // 返回首页再次打开搜索
    await page.goBack();
    await page.click('.search-bar');
    
    await expect(page.locator('.history-tag').first()).toContainText('测试搜索');
  });

  test('空搜索应显示提示', async ({ page }) => {
    await page.click('.search-bar');
    await page.click('[data-testid="search-button"]');
    
    await expect(page.locator('.search-hint')).toBeVisible();
  });
});

Step 2: 运行E2E测试

Run: cd everything-is-suitable-uniapp && npx playwright test search.spec.ts

Expected: PASS

Step 3: Commit

git add e2e/search.spec.ts
git commit -m "test(uniapp): add E2E tests for search functionality"

Task 5: 优化性能和用户体验

Files:

  • Modify: src/components/SearchConditionPanel/index.vue
  • Create: src/composables/useDebounce.ts

Step 1: 添加防抖功能

// src/composables/useDebounce.ts
import { ref, watch } from 'vue';

export function useDebounce<T>(value: Ref<T>, delay: number = 300): Ref<T> {
  const debouncedValue = ref(value.value) as Ref<T>;
  let timeout: ReturnType<typeof setTimeout>;

  watch(value, (newValue) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      debouncedValue.value = newValue;
    }, delay);
  });

  return debouncedValue;
}

Step 2: 在搜索组件中应用防抖

<!-- src/components/SearchConditionPanel/index.vue -->
<script setup lang="ts">
import { useDebounce } from '@/composables/useDebounce';

const searchKeyword = ref('');
const debouncedKeyword = useDebounce(searchKeyword, 300);

watch(debouncedKeyword, (value) => {
  if (value.trim()) {
    // 实时搜索建议
    fetchSuggestions(value);
  }
});

const fetchSuggestions = async (keyword: string) => {
  // 获取搜索建议
};
</script>

Step 3: Commit

git add src/composables/useDebounce.ts
git add src/components/SearchConditionPanel/index.vue
git commit -m "perf(uniapp): add debounce for search input to improve UX"

验收标准

  • 首页搜索功能入口正常工作
  • 搜索结果页面正确显示搜索结果
  • 组件测试覆盖率 >= 80%
  • E2E测试全部通过
  • 搜索响应时间 < 500ms
  • 用户操作流程符合设计规范

风险与依赖

风险 影响 缓解措施
搜索服务依赖后端API 确保API模块端点路径一致
跨平台兼容性问题 充分测试H5、小程序、App
性能问题 添加防抖、缓存优化