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

694 lines
17 KiB
Markdown

# 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: 编写搜索功能测试**
```typescript
// 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: 更新首页添加搜索入口**
```vue
<!-- 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: 完善搜索条件面板组件**
```vue
<!-- 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**
```bash
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: 编写搜索服务集成测试**
```typescript
// 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: 更新搜索结果页面**
```vue
<!-- 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**
```bash
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: 编写老黄历卡片组件测试**
```typescript
// 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: 编写日历卡片组件测试**
```typescript
// 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: 编写加载指示器组件测试**
```typescript
// 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**
```bash
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测试**
```typescript
// 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**
```bash
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: 添加防抖功能**
```typescript
// 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: 在搜索组件中应用防抖**
```vue
<!-- 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**
```bash
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 |
| 性能问题 | 低 | 添加防抖、缓存优化 |