作为 人才小程序开发者, 我想要 实现设置页和帮助功能, 以便 人才用户能够管理个人信息、查看帮助文档、修改账号设置和退出登录。
现有系统状态:
@d8d/rencai-settings-ui包基础框架已就绪src/api/index.ts)原型设计参考:
docs/小程序原型/rencai.html 提供了设置页的完整原型设计技术集成模式:
yongren-settings-ui的实现模式(如果存在)@d8d/rencai-settings-ui包依赖API (史诗015):
依赖故事完成状态:
@d8d/rencai-settings-ui中实现SettingsPage页面组件 (src/pages/SettingsPage/SettingsPage.tsx)src/components/UserProfileSummary.tsx)
src/components/MenuItem.tsx)
src/components/MenuSection.tsx)
src/components/LogoutButton.tsx)
POST /api/v1/rencai/auth/logoutsrc/pages/AccountSecurityPage/AccountSecurityPage.tsx)src/components/LoginLogsList.tsx)
GET /api/v1/rencai/auth/login-logsmini-talent/src/pages/settings/index.tsx:
@d8d/rencai-settings-ui/pages/SettingsPage/SettingsPage导入SettingsPage组件tests/pages/SettingsPage/SettingsPage.test.tsx)
pnpm typecheck确保类型检查通过故事017.001完成状态:
@d8d/rencai-settings-ui包基础框架已就绪src/api/index.ts)jest.config.cjs)故事017.002完成状态:
故事017.003完成状态:
maskUtils.ts)故事017.004完成状态:
flex flex-col实现垂直布局)故事017.005完成状态:
故事017.012完成状态:
关键实现经验:
@d8d/server@d8d/mini-shared-ui-components中的实现来源: architecture/tech-stack.md
运行时和框架:
测试框架:
重要: mini项目使用Jest测试框架,与web应用使用的Vitest不同。
来源:
关键规范要求:
重要: UI包内部导入必须使用相对路径,不要使用别名。
正确示例:
// ✅ 正确: 使用相对路径导入同一包内的模块
import { UserProfileSummary } from '../../components/UserProfileSummary'
import { MenuItem } from '../components/MenuItem'
错误示例:
// ❌ 错误: 不要使用别名导入UI包内部的模块
import { UserProfileSummary } from '@/components/UserProfileSummary'
import { MenuItem } from '@/components/MenuItem'
{
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"require": "./dist/src/index.js"
},
"./api": {
"types": "./src/api/index.ts",
"import": "./src/api/index.ts",
"require": "./src/api/index.ts"
},
"./pages/SettingsPage/SettingsPage": {
"types": "./src/pages/SettingsPage/SettingsPage.tsx",
"import": "./dist/src/pages/SettingsPage/SettingsPage.js",
"require": "./dist/src/pages/SettingsPage/SettingsPage.js"
}
}
}
重要: Mini UI包必须使用React Query (@tanstack/react-query) 管理服务端状态。
数据查询 (useQuery):
import { useQuery } from '@tanstack/react-query'
import { apiClient } from '../../api'
const MyPage: React.FC = () => {
// ✅ 正确: 使用React Query
const { data, isLoading, error } = useQuery({
queryKey: ['login-logs'],
queryFn: async () => {
const res = await apiClient.auth['login-logs'].$get()
if (!res.ok) {
throw new Error('获取登录日志失败')
}
return await res.json()
}
})
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return <div>{/* 渲染数据 */}</div>
}
数据修改 (useMutation):
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { apiClient } from '../../api'
const SettingsPage: React.FC = () => {
const queryClient = useQueryClient()
const navigate = useNavigate()
// 退出登录mutation
const logoutMutation = useMutation({
mutationFn: async () => {
const res = await apiClient.auth.logout.$post()
if (!res.ok) {
throw new Error('退出登录失败')
}
return await res.json()
},
onSuccess: () => {
// 清除查询缓存
queryClient.clear()
// 清除本地存储
localStorage.removeItem('token')
localStorage.removeItem('user')
// 跳转到登录页
Taro.reLaunch({ url: '/pages/login/index' })
},
onError: (error) => {
Taro.showToast({
title: error.message,
icon: 'none'
})
}
})
const handleLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
logoutMutation.mutate()
}
}
})
}
return (
<View onClick={handleLogout}>
<Text>{logoutMutation.isPending ? '退出中...' : '退出登录'}</Text>
</View>
)
}
每个UI包必须创建jest.config.cjs配置文件,参照rencai-personal-info-ui/jest.config.cjs:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@d8d/mini-testing-utils/setup'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^~/(.*)$': '<rootDir>/tests/$1',
'^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
'\\.(css|less|scss|sass)$': '@d8d/mini-testing-utils/testing/style-mock.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'@d8d/mini-testing-utils/testing/file-mock.js'
},
testMatch: [
'<rootDir>/tests/**/*.spec.{ts,tsx}',
'<rootDir>/tests/**/*.test.{ts,tsx}'
],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.{ts,tsx}',
'!src/**/*.stories.{ts,tsx}'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/coverage/'
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/(?!(swiper|@tarojs)/)'
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
}
设置页 (原型行770-906):
<!-- 个人信息 -->
<div class="p-4 border-b border-gray-200">
<div class="flex items-center mb-4">
<div class="w-16 h-16 rounded-full mr-4 bg-blue-500 flex items-center justify-center text-white text-xl font-bold">
张
</div>
<div>
<h3 class="font-semibold text-gray-800">张明</h3>
<p class="text-sm text-gray-500">肢体残疾 · 三级</p>
</div>
</div>
<div class="grid grid-cols-3 gap-3 text-center">
<div>
<p class="text-xl font-bold text-gray-800">28</p>
<p class="text-xs text-gray-500">本月出勤</p>
</div>
<div>
<p class="text-xl font-bold text-gray-800">156</p>
<p class="text-xs text-gray-500">累计出勤</p>
</div>
<div>
<p class="text-xl font-bold text-gray-800">¥4,800</p>
<p class="text-xs text-gray-500">本月薪资</p>
</div>
</div>
</div>
移动端设计规范:
w-16 h-16),蓝色背景 (bg-blue-500)text-xl font-bold)grid grid-cols-3 gap-3)text-xl font-bold)text-xs text-gray-500)<!-- 功能列表 -->
<div class="space-y-1 mb-6">
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3">
<i class="fas fa-user-edit text-blue-500"></i>
</div>
<span class="text-gray-700">修改个人信息</span>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3">
<i class="fas fa-shield-alt text-green-500"></i>
</div>
<span class="text-gray-700">账号与安全</span>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center mr-3">
<i class="fas fa-bell text-purple-500"></i>
</div>
<span class="text-gray-700">消息通知设置</span>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</div>
移动端设计规范:
space-y-1)p-3)rounded-lg)w-10 h-10)text-gray-400)<!-- 帮助与支持 -->
<div class="space-y-1 mb-6">
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-yellow-100 flex items-center justify-center mr-3">
<i class="fas fa-question-circle text-yellow-500"></i>
</div>
<span class="text-gray-700">帮助中心</span>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center mr-3">
<i class="fas fa-file-alt text-indigo-500"></i>
</div>
<span class="text-gray-700">用户协议</span>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-pink-100 flex items-center justify-center mr-3">
<i class="fas fa-lock text-pink-500"></i>
</div>
<span class="text-gray-700">隐私政策</span>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</div>
图标背景色:
bg-yellow-100 text-yellow-500)bg-indigo-100 text-indigo-500)bg-pink-100 text-pink-500)<!-- 退出登录 -->
<div class="space-y-1">
<div class="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center mr-3">
<i class="fas fa-sign-out-alt text-red-500"></i>
</div>
<span class="text-gray-700">退出登录</span>
</div>
</div>
</div>
退出登录样式:
bg-red-100 text-red-500)来源: docs/stories/017.012.story.md
TabBar页面规范(设置页属于此类):
leftIcon=""和leftText=""隐藏返回按钮@d8d/mini-shared-ui-components/components/navbarNavbar集成示例:
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
import { View, ScrollView } from '@tarojs/components'
export function SettingsPage() {
return (
<View className="h-screen bg-gray-100">
{/* Navbar导航栏 - TabBar页面无返回按钮 */}
<Navbar
title="设置"
leftIcon=""
leftText=""
onClickLeft={() => {}}
placeholder
fixed
/>
{/* 页面内容 */}
<ScrollView scrollY className="h-full">
{/* 页面内容 */}
</ScrollView>
</View>
)
}
关键配置:
leftIcon="": 隐藏返回按钮图标leftText="": 隐藏返回按钮文字placeholder: 添加占位空间,避免内容被Navbar遮挡fixed: 固定在顶部重要: 在Taro小程序中,<View> 组件内的子元素默认是横向布局(flex-row),需要显式添加 flex flex-col 类才能实现垂直布局。
正确示例:
// ✅ 正确: 使用 flex flex-col 实现垂直布局
<View className="flex flex-col">
<Text>姓名: 张三</Text>
<Text>残疾类型: 肢体残疾 · 三级</Text>
</View>
// ❌ 错误: 缺少 flex flex-col,子元素会横向排列
<View>
<Text>姓名: 张三</Text>
<Text>残疾类型: 肢体残疾 · 三级</Text>
</View>
来源: architecture/mini-ui-package-standards.md
重要: 不要使用emoji,必须使用Heroicons图标类。
图标类命名格式: i-heroicons-{图标名称}-{尺寸}-{样式}
本故事需要的图标:
user-20-solid - 修改个人信息(替代fas fa-user-edit)shield-check-20-solid - 账号与安全(替代fas fa-shield-alt)bell-20-solid - 消息通知设置question-mark-circle-20-solid - 帮助中心(替代fas fa-question-circle)document-text-20-solid - 用户协议(替代fas fa-file-alt)lock-closed-20-solid - 隐私政策arrow-right-on-rectangle-20-solid - 退出登录(替代fas fa-sign-out-alt)chevron-right-20-solid - 右箭头正确示例:
// ✅ 正确: 使用Heroicons图标类
<View className="i-heroicons-user-20-solid w-5 h-5 text-blue-500" />
// ❌ 错误: 使用emoji
<Text>👤</Text>
功能入口图标示例:
import { View } from '@tarojs/components'
// 修改个人信息 - 蓝色
<View className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3">
<View className="i-heroicons-user-20-solid text-blue-500 w-5 h-5" />
</View>
// 账号与安全 - 绿色
<View className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3">
<View className="i-heroicons-shield-check-20-solid text-green-500 w-5 h-5" />
</View>
// 消息通知设置 - 紫色
<View className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center mr-3">
<View className="i-heroicons-bell-20-solid text-purple-500 w-5 h-5" />
</View>
来源: architecture/source-tree.md
mini-talent项目结构:
mini-talent/ # 人才小程序项目
├── src/
│ ├── app.tsx # 小程序入口
│ ├── app.config.ts # 小程序配置
│ ├── app.css # 全局样式
│ ├── pages/ # 页面目录
│ │ ├── login/ # 登录页
│ │ ├── index/ # 首页/个人主页
│ │ ├── attendance/ # 考勤记录页
│ │ ├── personal-info/ # 个人信息页
│ │ ├── employment/ # 就业信息页
│ │ └── settings/ # 设置页 (TabBar) - 本故事
│ │ └── index.tsx
├── package.json
├── jest.config.js
└── tsconfig.json
mini-ui-packages目录结构:
mini-ui-packages/
├── rencai-settings-ui/ # 人才设置UI包
│ ├── src/
│ │ ├── api/
│ │ │ ├── settingsClient.ts
│ │ │ └── index.ts
│ │ ├── pages/
│ │ │ └── SettingsPage/
│ │ │ ├── SettingsPage.tsx
│ │ │ └── index.ts (可选)
│ │ ├── components/ # UI组件
│ │ │ ├── UserProfileSummary.tsx # 个人信息摘要
│ │ │ ├── MenuItem.tsx # 菜单项
│ │ │ ├── MenuSection.tsx # 菜单分组
│ │ │ ├── LogoutButton.tsx # 退出登录按钮
│ │ │ └── LoginLogsList.tsx # 登录日志列表(可选)
│ │ ├── types/
│ │ │ └── settings.ts # 类型定义
│ │ └── index.ts
│ ├── tests/ # 测试文件
│ │ ├── pages/
│ │ │ └── SettingsPage/
│ │ │ └── SettingsPage.test.tsx
│ │ └── components/
│ │ ├── UserProfileSummary.test.tsx
│ │ ├── MenuItem.test.tsx
│ │ ├── LogoutButton.test.tsx
│ │ └── LoginLogsList.test.tsx
│ ├── package.json
│ ├── jest.config.cjs
│ └── tsconfig.json
└── mini-shared-ui-components/ # 通用小程序UI组件
├── src/
│ └── components/
│ ├── status-bar.tsx
│ ├── page-container.tsx
│ ├── navbar.tsx
│ └── tab-bar.tsx
└── ...
mini-talent页面导入方式:
// mini-talent/src/pages/settings/index.tsx
import SettingsPage from '@d8d/rencai-settings-ui/pages/SettingsPage/SettingsPage'
import { AuthContextProvider, useAuth } from '@d8d/rencai-auth-ui/utils'
function Settings() {
const { isLoggedIn } = useAuth()
// 未登录跳转到登录页
if (!isLoggedIn) {
Taro.navigateTo({ url: '/pages/login/index' })
return null
}
return <SettingsPage />
}
export default function SettingsIndex() {
return (
<AuthContextProvider>
<Settings />
</AuthContextProvider>
)
}
来源: architecture/testing-strategy.md
测试框架:
测试文件位置:
mini-ui-packages/<package-name>/
└── tests/
├── components/ # 组件测试
│ ├── UserProfileSummary.test.tsx
│ ├── MenuItem.test.tsx
│ ├── LogoutButton.test.tsx
│ └── LoginLogsList.test.tsx
└── pages/ # 页面组件测试
└── SettingsPage/
└── SettingsPage.test.tsx
测试要求:
pnpm typecheck确保类型检查通过页面集成测试示例(使用真实React Query):
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import SettingsPage from '../pages/SettingsPage/SettingsPage'
// Mock API client
jest.mock('../../api', () => ({
apiClient: {
auth: {
logout: {
$post: jest.fn()
},
'login-logs': {
$get: jest.fn()
}
}
}
}))
const { apiClient } = require('../../api')
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false }
}
})
const renderWithQueryClient = (component: React.ReactElement) => {
const queryClient = createTestQueryClient()
return render(
<QueryClientProvider client={queryClient}>
{component}
</QueryClientProvider>
)
}
describe('SettingsPage', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('应该显示个人信息摘要', async () => {
renderWithQueryClient(<SettingsPage />)
await waitFor(() => {
expect(screen.getByText('张明')).toBeInTheDocument()
expect(screen.getByText('肢体残疾 · 三级')).toBeInTheDocument()
})
})
it('应该显示功能入口列表', async () => {
renderWithQueryClient(<SettingsPage />)
expect(screen.getByText('修改个人信息')).toBeInTheDocument()
expect(screen.getByText('账号与安全')).toBeInTheDocument()
expect(screen.getByText('消息通知设置')).toBeInTheDocument()
})
it('应该成功退出登录', async () => {
apiClient.auth.logout.$post.mockResolvedValue({
ok: true,
json: async () => ({ message: '退出成功' })
})
renderWithQueryClient(<SettingsPage />)
const logoutButton = screen.getByText('退出登录')
logoutButton.click()
await waitFor(() => {
expect(apiClient.auth.logout.$post).toHaveBeenCalled()
})
})
})
来源: architecture/coding-standards.md
关键编码规范:
开发Mini UI包时,必须参考并遵循Mini UI包开发规范,该规范基于史诗011和017的经验总结。
flex flex-col实现垂直布局flex flex-col 实现垂直布局w-5 h-5、text-lg等)@/、~/等),必须使用相对路径路径使用示例:
// ✅ 正确: UI包内部使用相对路径
import { UserProfileSummary } from '../../components/UserProfileSummary'
import { MenuItem } from '../components/MenuItem'
// ✅ 正确: 跨包导入使用workspace包名
import { SharedComponent } from '@d8d/mini-shared-ui-components'
// ❌ 错误: UI包内部使用别名
import { UserProfileSummary } from '@/components/UserProfileSummary'
import { MenuItem } from '@/components/MenuItem'
mini-ui-packages/yongren-settings-ui (如果存在)mini-ui-packages/rencai-personal-info-ui
主要风险:
缓解措施:
| 日期 | 版本 | 描述 | 作者 |
|---|---|---|---|
| 2025-12-28 | 1.0 | 创建故事文档 | Bob (Scrum Master) |
| 2025-12-28 | 1.1 | 状态更新为Approved | Bob (Scrum Master) |
| 2025-12-28 | 1.2 | 状态更新为Ready for Review,完成所有开发任务 | James (Claude Code) |
| 2025-12-28 | 1.3 | 修复测试类型错误,所有测试通过(31个测试用例) | James (Claude Code) |
此部分由开发代理在实施过程中填写
claude-sonnet-4-5-20251101 (claude-sonnet)
无重大调试问题。开发过程顺利。
新增源文件:
mini-ui-packages/rencai-settings-ui/src/types/settings.ts - 类型定义mini-ui-packages/rencai-settings-ui/src/components/UserProfileSummary.tsx - 用户信息摘要组件mini-ui-packages/rencai-settings-ui/src/components/MenuItem.tsx - 菜单项组件mini-ui-packages/rencai-settings-ui/src/components/MenuSection.tsx - 菜单分组组件mini-ui-packages/rencai-settings-ui/src/components/LogoutButton.tsx - 退出登录按钮组件mini-ui-packages/rencai-settings-ui/src/pages/AccountSecurityPage/AccountSecurityPage.tsx - 账号与安全页组件修改源文件:
mini-ui-packages/rencai-settings-ui/src/pages/SettingsPage/SettingsPage.tsx - 完整实现设置页功能mini-ui-packages/rencai-settings-ui/src/api/talentSettingsClient.ts - 修正API客户端导入路径mini-ui-packages/rencai-settings-ui/package.json - 添加@d8d/core-module依赖,添加AccountSecurityPage导出新增测试文件:
mini-ui-packages/rencai-settings-ui/tests/components/UserProfileSummary.test.tsx - 6个测试用例mini-ui-packages/rencai-settings-ui/tests/components/MenuItem.test.tsx - 6个测试用例mini-ui-packages/rencai-settings-ui/tests/components/MenuSection.test.tsx - 5个测试用例mini-ui-packages/rencai-settings-ui/tests/components/LogoutButton.test.tsx - 6个测试用例mini-ui-packages/rencai-settings-ui/tests/pages/SettingsPage/SettingsPage.test.tsx - 8个测试用例新增mini-talent页面文件:
mini-talent/src/pages/account-security/index.tsx - 账号与安全页入口mini-talent/src/pages/account-security/index.config.ts - 页面配置修改mini-talent配置文件:
mini-talent/src/app.config.ts - 添加账号与安全页路由修改mini-testing-utils:
mini-ui-packages/mini-testing-utils/testing/taro-api-mock.ts - 添加存储相关API mock新增测试文件:
mini-ui-packages/rencai-settings-ui/tests/components/UserProfileSummary.test.tsxmini-ui-packages/rencai-settings-ui/tests/components/MenuItem.test.tsxmini-ui-packages/rencai-settings-ui/tests/components/MenuSection.test.tsxmini-ui-packages/rencai-settings-ui/tests/components/LogoutButton.test.tsxmini-ui-packages/rencai-settings-ui/tests/pages/SettingsPage/SettingsPage.test.tsx新增mini-talent页面文件:
mini-talent/src/pages/account-security/index.tsx - 账号与安全页入口mini-talent/src/pages/account-security/index.config.ts - 页面配置修改mini-talent配置文件:
mini-talent/src/app.config.ts - 添加账号与安全页路由此部分由QA代理在审查完成后填写