Просмотр исходного кода

✨ feat(profile): 添加常见问题弹窗功能

- 新增 mp-html 依赖用于富文本渲染
- 创建 FAQDialog 组件显示常见问题内容
- 在个人中心页面集成常见问题弹窗
- 更新配置文件注册 mp-html 组件
- 完善相关单元测试用例

【test】更新个人中心页面测试用例
- 添加常见问题弹窗的打开关闭测试
- 验证弹窗内容正确显示
- 移除原有的开发中提示测试
yourname 3 месяцев назад
Родитель
Сommit
b1ce85fa23

+ 1 - 0
mini/package.json

@@ -77,6 +77,7 @@
     "clsx": "^2.1.1",
     "clsx": "^2.1.1",
     "date-fns": "^4.1.0",
     "date-fns": "^4.1.0",
     "hono": "4.8.5",
     "hono": "4.8.5",
+    "mp-html": "^2.5.1",
     "react": "^18.0.0",
     "react": "^18.0.0",
     "react-dom": "^18.0.0",
     "react-dom": "^18.0.0",
     "react-hook-form": "^7.62.0",
     "react-hook-form": "^7.62.0",

+ 3 - 1
mini/src/app.config.ts

@@ -42,5 +42,7 @@ export default defineAppConfig({
       }
       }
     ]
     ]
   },
   },
-  usingComponents: {}
+  usingComponents: {
+    'mp-html': './components/mp-html'
+  }
 })
 })

+ 62 - 0
mini/src/components/FAQDialog.tsx

@@ -0,0 +1,62 @@
+import { View } from '@tarojs/components'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+
+interface FAQDialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+}
+
+const FAQ_CONTENT = `
+  <h1>常见问题解答</h1>
+
+  <h2>1. 如何注册账号?</h2>
+  <p>您可以通过手机号注册账号,注册时需要填写基本信息并设置密码。</p>
+
+  <h2>2. 如何下单?</h2>
+  <p>在首页选择活动后,填写出行信息并选择乘车人,确认订单信息后即可下单。</p>
+
+  <h2>3. 如何支付?</h2>
+  <p>我们支持微信支付,下单后您可以在订单页面完成支付。</p>
+
+  <h2>4. 如何取消订单?</h2>
+  <p>在订单详情页面,如果订单状态允许取消,您可以点击取消按钮取消订单。</p>
+
+  <h2>5. 如何联系客服?</h2>
+  <p>您可以通过意见反馈功能联系我们,客服会在24小时内回复您的问题。</p>
+
+  <h2>6. 如何修改个人信息?</h2>
+  <p>在个人中心页面,您可以编辑您的个人信息和乘车人信息。</p>
+
+  <h2>7. 忘记密码怎么办?</h2>
+  <p>您可以通过登录页面的"忘记密码"功能重置密码。</p>
+
+  <h2>8. 如何查看订单状态?</h2>
+  <p>在订单页面,您可以查看所有订单的详细状态和进度。</p>
+`
+
+export function FAQDialog({ open, onOpenChange }: FAQDialogProps) {
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
+        <DialogHeader>
+          <DialogTitle>常见问题</DialogTitle>
+        </DialogHeader>
+
+        <View className="mt-4">
+          <mp-html content={FAQ_CONTENT} />
+        </View>
+
+        <DialogFooter className="mt-6">
+          <Button
+            variant="outline"
+            onClick={() => onOpenChange(false)}
+            className="w-full"
+          >
+            关闭
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  )
+}

Разница между файлами не показана из-за своего большого размера
+ 7 - 0
mini/src/components/mp-html/index.js


+ 1 - 0
mini/src/components/mp-html/index.json

@@ -0,0 +1 @@
+{"component":true,"usingComponents":{"node":"./node/node"}}

+ 1 - 0
mini/src/components/mp-html/index.wxml

@@ -0,0 +1 @@
+<view class="_root {{selectable?'_select':''}}" style="{{containerStyle}}"><slot wx:if="{{!nodes[0]}}"/><node id="_root" childs="{{nodes}}" opts="{{[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]}}" catchadd="_add"/></view>

+ 1 - 0
mini/src/components/mp-html/index.wxss

@@ -0,0 +1 @@
+._root{padding:1px 0;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}._select{-webkit-user-select:text;user-select:text}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
mini/src/components/mp-html/node/node.js


+ 1 - 0
mini/src/components/mp-html/node/node.json

@@ -0,0 +1 @@
+{"component":true,"usingComponents":{"node":"./node"}}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
mini/src/components/mp-html/node/node.wxml


+ 1 - 0
mini/src/components/mp-html/node/node.wxss

@@ -0,0 +1 @@
+._a{padding:1.5px 0 1.5px 0;color:#366092;word-break:break-all}._hover{text-decoration:underline;opacity:.7}._img{max-width:100%;-webkit-touch-callout:none}._b,._strong{font-weight:700}._code{font-family:monospace}._del{text-decoration:line-through}._em,._i{font-style:italic}._h1{font-size:2em}._h2{font-size:1.5em}._h3{font-size:1.17em}._h5{font-size:.83em}._h6{font-size:.67em}._h1,._h2,._h3,._h4,._h5,._h6{display:block;font-weight:700}._ins{text-decoration:underline}._li{display:list-item}._ol{list-style-type:decimal}._ol,._ul{display:block;padding-left:40px;margin:1em 0}._q::before{content:'"'}._q::after{content:'"'}._sub{font-size:smaller;vertical-align:sub}._sup{font-size:smaller;vertical-align:super}._tbody,._tfoot,._thead{display:table-row-group}._tr{display:table-row}._td,._th{display:table-cell;vertical-align:middle}._th{font-weight:700;text-align:center}._ul{list-style-type:disc}._ul ._ul{margin:0;list-style-type:circle}._ul ._ul ._ul{list-style-type:square}._abbr,._b,._code,._del,._em,._i,._ins,._label,._q,._span,._strong,._sub,._sup{display:inline}._blockquote,._div,._p{display:block}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
mini/src/components/mp-html/parser.js


+ 6 - 1
mini/src/pages/profile/index.tsx

@@ -7,12 +7,14 @@ import { cn } from '@/utils/cn'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Navbar } from '@/components/ui/navbar'
 import { Navbar } from '@/components/ui/navbar'
 import { AvatarUpload } from '@/components/ui/avatar-upload'
 import { AvatarUpload } from '@/components/ui/avatar-upload'
+import { FAQDialog } from '@/components/FAQDialog'
 import { type UploadResult } from '@/utils/minio'
 import { type UploadResult } from '@/utils/minio'
 import './index.css'
 import './index.css'
 
 
 const ProfilePage: React.FC = () => {
 const ProfilePage: React.FC = () => {
   const { user: userProfile, logout, isLoading: loading, updateUser } = useAuth()
   const { user: userProfile, logout, isLoading: loading, updateUser } = useAuth()
   const [updatingAvatar, setUpdatingAvatar] = useState(false)
   const [updatingAvatar, setUpdatingAvatar] = useState(false)
+  const [faqDialogOpen, setFaqDialogOpen] = useState(false)
 
 
   const handleLogout = async () => {
   const handleLogout = async () => {
     try {
     try {
@@ -320,7 +322,7 @@ const ProfilePage: React.FC = () => {
           <View className="bg-white rounded-[20rpx] shadow-[0_4rpx_20rpx_rgba(0,0,0,0.08)] border border-[#E5E5EA] overflow-hidden">
           <View className="bg-white rounded-[20rpx] shadow-[0_4rpx_20rpx_rgba(0,0,0,0.08)] border border-[#E5E5EA] overflow-hidden">
             {[
             {[
               { title: '联系客服', desc: '7x24小时在线客服', icon: 'i-heroicons-phone-20-solid', color: 'text-blue-500', onClick: handleCustomerService, testId: 'customer-service-button' },
               { title: '联系客服', desc: '7x24小时在线客服', icon: 'i-heroicons-phone-20-solid', color: 'text-blue-500', onClick: handleCustomerService, testId: 'customer-service-button' },
-              { title: '常见问题', desc: '查看常见问题解答', icon: 'i-heroicons-question-mark-circle-20-solid', color: 'text-green-500', onClick: () => Taro.showToast({ title: '常见问题功能开发中...', icon: 'none' }), testId: 'faq-button' },
+              { title: '常见问题', desc: '查看常见问题解答', icon: 'i-heroicons-question-mark-circle-20-solid', color: 'text-green-500', onClick: () => setFaqDialogOpen(true), testId: 'faq-button' },
               { title: '意见反馈', desc: '提出宝贵意见', icon: 'i-heroicons-chat-bubble-left-ellipsis-20-solid', color: 'text-orange-500', onClick: () => Taro.showToast({ title: '意见反馈功能开发中...', icon: 'none' }), testId: 'feedback-button' }
               { title: '意见反馈', desc: '提出宝贵意见', icon: 'i-heroicons-chat-bubble-left-ellipsis-20-solid', color: 'text-orange-500', onClick: () => Taro.showToast({ title: '意见反馈功能开发中...', icon: 'none' }), testId: 'feedback-button' }
             ].map((item, index) => (
             ].map((item, index) => (
               <View
               <View
@@ -358,6 +360,9 @@ const ProfilePage: React.FC = () => {
           <Text className="text-[22rpx] text-[#999]">去看出行 v1.0.0</Text>
           <Text className="text-[22rpx] text-[#999]">去看出行 v1.0.0</Text>
         </View>
         </View>
       </ScrollView>
       </ScrollView>
+
+      {/* 常见问题弹窗 */}
+      <FAQDialog open={faqDialogOpen} onOpenChange={setFaqDialogOpen} />
     </TabBarLayout>
     </TabBarLayout>
   )
   )
 }
 }

+ 62 - 5
mini/tests/pages/profile.test.tsx

@@ -81,6 +81,21 @@ jest.mock('@/components/ui/button', () => ({
   ))
   ))
 }))
 }))
 
 
+// Mock FAQDialog 组件
+jest.mock('@/components/FAQDialog', () => ({
+  FAQDialog: jest.fn(({ open, onOpenChange }) => (
+    <div data-testid="faq-dialog" data-open={open}>
+      <button
+        data-testid="faq-dialog-close"
+        onClick={() => onOpenChange(false)}
+      >
+        关闭常见问题
+      </button>
+      <div data-testid="faq-content">常见问题内容</div>
+    </div>
+  ))
+}))
+
 // Mock useAuth hook
 // Mock useAuth hook
 const mockUser = {
 const mockUser = {
   id: 1,
   id: 1,
@@ -303,13 +318,12 @@ describe('个人中心页面测试', () => {
       icon: 'none'
       icon: 'none'
     })
     })
 
 
-    // 点击常见问题按钮
+    // 点击常见问题按钮 - 现在应该打开弹窗而不是显示Toast
     const faqButton = screen.getByTestId('faq-button')
     const faqButton = screen.getByTestId('faq-button')
     fireEvent.click(faqButton)
     fireEvent.click(faqButton)
-    expect(taroMock.showToast).toHaveBeenCalledWith({
-      title: '常见问题功能开发中...',
-      icon: 'none'
-    })
+    // 检查弹窗状态变为打开
+    const faqDialog = screen.getByTestId('faq-dialog')
+    expect(faqDialog).toHaveAttribute('data-open', 'true')
 
 
     // 点击意见反馈按钮
     // 点击意见反馈按钮
     const feedbackButton = screen.getByTestId('feedback-button')
     const feedbackButton = screen.getByTestId('feedback-button')
@@ -535,4 +549,47 @@ describe('个人中心页面测试', () => {
       })
       })
     })
     })
   })
   })
+
+  test('应该正确处理常见问题弹窗的打开和关闭', () => {
+    render(
+      <Wrapper>
+        <ProfilePage />
+      </Wrapper>
+    )
+
+    // 初始状态下弹窗应该是关闭的
+    const faqDialog = screen.getByTestId('faq-dialog')
+    expect(faqDialog).toHaveAttribute('data-open', 'false')
+
+    // 点击常见问题按钮打开弹窗
+    const faqButton = screen.getByTestId('faq-button')
+    fireEvent.click(faqButton)
+
+    // 检查弹窗状态变为打开
+    expect(faqDialog).toHaveAttribute('data-open', 'true')
+
+    // 点击关闭按钮关闭弹窗
+    const closeButton = screen.getByTestId('faq-dialog-close')
+    fireEvent.click(closeButton)
+
+    // 检查弹窗状态变为关闭
+    expect(faqDialog).toHaveAttribute('data-open', 'false')
+  })
+
+  test('常见问题弹窗应该显示正确的内容', () => {
+    render(
+      <Wrapper>
+        <ProfilePage />
+      </Wrapper>
+    )
+
+    // 打开常见问题弹窗
+    const faqButton = screen.getByTestId('faq-button')
+    fireEvent.click(faqButton)
+
+    // 检查弹窗内容是否正确显示
+    const faqContent = screen.getByTestId('faq-content')
+    expect(faqContent).toBeInTheDocument()
+    expect(faqContent).toHaveTextContent('常见问题内容')
+  })
 })
 })

+ 8 - 0
pnpm-lock.yaml

@@ -92,6 +92,9 @@ importers:
       hono:
       hono:
         specifier: 4.8.5
         specifier: 4.8.5
         version: 4.8.5
         version: 4.8.5
+      mp-html:
+        specifier: ^2.5.1
+        version: 2.5.1
       react:
       react:
         specifier: ^18.0.0
         specifier: ^18.0.0
         version: 18.3.1
         version: 18.3.1
@@ -8088,6 +8091,9 @@ packages:
   mobile-detect@1.4.5:
   mobile-detect@1.4.5:
     resolution: {integrity: sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==}
     resolution: {integrity: sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==}
 
 
+  mp-html@2.5.1:
+    resolution: {integrity: sha512-7BEH8dnQ89kOIyjdoYni8zcc0QAg+lgEWg0n9or9q2D4l26JNG+KPzHfttDyisC/5S7aPBblpXrFTYQv475w/Q==}
+
   mrmime@2.0.1:
   mrmime@2.0.1:
     resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
     resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
     engines: {node: '>=10'}
     engines: {node: '>=10'}
@@ -19551,6 +19557,8 @@ snapshots:
 
 
   mobile-detect@1.4.5: {}
   mobile-detect@1.4.5: {}
 
 
+  mp-html@2.5.1: {}
+
   mrmime@2.0.1: {}
   mrmime@2.0.1: {}
 
 
   ms@2.0.0: {}
   ms@2.0.0: {}

Некоторые файлы не были показаны из-за большого количества измененных файлов