在这里插入图片描述

引言

知识问答页面是用户交流和获取帮助的重要平台。本文将实现一个功能完善的问答页面,包括:

  • 问答列表展示
  • AI智能问答
  • 提问功能
  • 回答功能
  • 点赞和评论

通过本文,你将掌握如何构建一个互动问答社区页面,并集成AI智能问答功能。


学习目标

完成本文后,你将能够:

  • ✅ 实现问答列表展示
  • ✅ 集成AI智能问答功能
  • ✅ 添加提问功能
  • ✅ 实现回答功能
  • ✅ 添加点赞和评论功能
  • ✅ 处理问答数据

需求分析

功能模块设计

模块 功能描述 技术要点
问答列表 展示问答内容 List布局、卡片组件
AI智能问答 自然语言交互 HTTP请求、AI API调用
提问功能 用户发布问题 表单验证、弹窗
回答功能 用户回答问题 表单验证、提交
点赞功能 点赞问答和回答 状态切换、动画
评论功能 评论问答和回答 输入框、列表

核心实现

步骤1: 页面结构设计

完整代码
// pages/QAPage.ets

import router from '@ohos.router';
import prompt from '@ohos.prompt';
import type { Question, Answer } from '../models/QAModel';

@Entry
@Component
struct QAPage {
  // 问答列表
  @State questions: Question[] = [];
  
  // 当前选中的问题(用于展开回答)
  @State expandedQuestionId: string = '';
  
  // AI聊天状态
  @State chatMessages: ChatMessage[] = [];
  @State isChatMode: boolean = false;
  @State inputMessage: string = '';
  @State isLoading: boolean = false;
  
  // AI服务实例
  private aiService = AIChatService.getInstance();
  
  /**
   * 页面加载时执行
   */
  aboutToAppear() {
    this.loadQuestions();
  }
  
  /**
   * 加载问答数据
   */
  loadQuestions(): void {
    // Mock数据
    this.questions = [
      {
        id: '1',
        userId: '1',
        userName: '用户A',
        avatar: 'avatar1',
        title: '立春和春分有什么区别?',
        content: '立春和春分都是春季的节气,请问它们有什么区别呢?',
        time: '2小时前',
        likes: 24,
        isLiked: false,
        answers: [
          {
            id: 'a1',
            userId: '2',
            userName: '用户B',
            avatar: 'avatar2',
            content: '立春是春季的开始,标志着冬天结束;春分是春季的中点,昼夜平分。',
            time: '1小时前',
            likes: 18,
            isLiked: false
          }
        ]
      },
      {
        id: '2',
        userId: '3',
        userName: '用户C',
        avatar: 'avatar3',
        title: '清明为什么要扫墓?',
        content: '清明节扫墓有什么传统意义?',
        time: '5小时前',
        likes: 45,
        isLiked: true,
        answers: [
          {
            id: 'a2',
            userId: '4',
            userName: '用户D',
            avatar: 'avatar4',
            content: '清明节扫墓是为了缅怀祖先,表达思念之情,是中华民族的传统习俗。',
            time: '3小时前',
            likes: 32,
            isLiked: false
          },
          {
            id: 'a3',
            userId: '5',
            userName: '用户E',
            avatar: 'avatar5',
            content: '扫墓不仅是祭祀祖先,也是传承孝道的一种方式。',
            time: '2小时前',
            likes: 15,
            isLiked: false
          }
        ]
      },
      {
        id: '3',
        userId: '6',
        userName: '用户F',
        avatar: 'avatar6',
        title: '夏至为什么要吃面条?',
        content: '听说夏至有吃面条的习俗,这是为什么呢?',
        time: '1天前',
        likes: 67,
        isLiked: false,
        answers: []
      }
    ];
  }
  
  /**
   * 点赞问题
   */
  likeQuestion(questionId: string): void {
    const question = this.questions.find((q) => q.id === questionId);
    if (question) {
      question.isLiked = !question.isLiked;
      question.likes += question.isLiked ? 1 : -1;
    }
  }
  
  /**
   * 点赞回答
   */
  likeAnswer(questionId: string, answerId: string): void {
    const question = this.questions.find((q) => q.id === questionId);
    if (question) {
      const answer = question.answers.find((a) => a.id === answerId);
      if (answer) {
        answer.isLiked = !answer.isLiked;
        answer.likes += answer.isLiked ? 1 : -1;
      }
    }
  }
  
  /**
   * 展开/收起回答
   */
  toggleAnswers(questionId: string): void {
    this.expandedQuestionId = this.expandedQuestionId === questionId ? '' : questionId;
  }
  
  /**
   * 提交回答
   */
  submitAnswer(questionId: string, content: string): void {
    const question = this.questions.find((q) => q.id === questionId);
    if (question) {
      question.answers.push({
        id: 'a' + Date.now(),
        userId: 'current_user',
        userName: '我',
        avatar: 'current',
        content: content,
        time: '刚刚',
        likes: 0,
        isLiked: false
      });
      prompt.showToast({ message: '回答成功' });
    }
  }
  
  /**
   * 切换聊天模式
   */
  toggleChatMode(): void {
    this.isChatMode = !this.isChatMode;
  }
  
  /**
   * 发送AI消息
   */
  async sendAIMessage(): Promise<void> {
    if (!this.inputMessage.trim() || this.isLoading) return;
    
    const userMessage: ChatMessage = {
      id: 'msg_' + Date.now(),
      role: 'user',
      content: this.inputMessage,
      timestamp: Date.now()
    };
    
    this.chatMessages.push(userMessage);
    this.isLoading = true;
    this.inputMessage = '';
    
    try {
      const response = await this.aiService.sendMessage(userMessage.content);
      
      const aiMessage: ChatMessage = {
        id: 'msg_' + Date.now(),
        role: 'assistant',
        content: response,
        timestamp: Date.now()
      };
      
      this.chatMessages.push(aiMessage);
    } catch (error) {
      console.error('发送消息失败:', error);
      prompt.showToast({ message: '发送失败,请重试' });
    } finally {
      this.isLoading = false;
    }
  }
  
  /**
   * 构建UI
   */
  build() {
    Column({ space: 0 }) {
      // 1. 顶部导航
      this.buildHeader()
      
      // 2. 模式切换标签
      if (!this.isChatMode) {
        this.buildAskButton()
        // 3. 问答列表
        this.buildQuestionList()
      } else {
        // AI聊天界面
        this.buildChatInterface()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F7F2')
  }
}

interface Question {
  id: string;
  userId: string;
  userName: string;
  avatar: string;
  title: string;
  content: string;
  time: string;
  likes: number;
  isLiked: boolean;
  answers: Answer[];
}

interface Answer {
  id: string;
  userId: string;
  userName: string;
  avatar: string;
  content: string;
  time: string;
  likes: number;
  isLiked: boolean;
}

interface ChatMessage {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: number;
}
代码解析

1. 状态管理

  • questions: 问答列表
  • expandedQuestionId: 当前展开的问题ID

2. 功能方法

  • likeQuestion(): 点赞问题
  • likeAnswer(): 点赞回答
  • toggleAnswers(): 展开/收起回答
  • submitAnswer(): 提交回答

步骤1.5: AI智能问答服务封装

// services/AIChatService.ts

import http from '@ohos.net.http';
import prompt from '@ohos.prompt';

export class AIChatService {
  private static instance: AIChatService;
  private mockMode: boolean = true;
  
  private constructor() {}
  
  static getInstance(): AIChatService {
    if (!AIChatService.instance) {
      AIChatService.instance = new AIChatService();
    }
    return AIChatService.instance;
  }
  
  setMockMode(enabled: boolean): void {
    this.mockMode = enabled;
  }
  
  async sendMessage(message: string): Promise<string> {
    if (this.mockMode) {
      return this.getMockResponse(message);
    }
    return this.callAIAPI(message);
  }
  
  private async callAIAPI(message: string): Promise<string> {
    try {
      const httpRequest = http.createHttp();
      const response = await httpRequest.request(
        'https://api.example.com/ai/chat',
        {
          method: http.RequestMethod.POST,
          header: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer YOUR_API_KEY'
          },
          extraData: JSON.stringify({
            message: message,
            model: 'gpt-3.5-turbo',
            temperature: 0.7
          }),
          connectTimeout: 10000,
          readTimeout: 30000
        }
      );
      
      if (response.responseCode === 200) {
        const result = JSON.parse(response.result.toString());
        return result.choices[0].message.content;
      } else {
        throw new Error(`API请求失败: ${response.responseCode}`);
      }
    } catch (error) {
      console.error('AI服务调用失败:', error);
      prompt.showToast({ message: 'AI服务暂时不可用' });
      return '抱歉,暂时无法获取AI回答,请稍后重试。';
    }
  }
  
  private getMockResponse(message: string): string {
    const mockResponses: Record<string, string[]> = {
      '立春': [
        '立春是二十四节气之首,标志着春季的开始。此时气温回升,万物复苏。',
        '立春有咬春、打春等传统习俗,人们会吃春饼、春卷等食物。'
      ],
      '清明': [
        '清明节是祭祖扫墓的传统节日,也是踏青郊游的好时节。',
        '清明前后,气温升高,雨量增多,适合春耕春种。'
      ],
      '夏至': [
        '夏至是北半球一年中白昼最长的一天,标志着炎热季节的开始。',
        '夏至有吃面的习俗,寓意长寿安康。'
      ],
      '冬至': [
        '冬至是北半球一年中白昼最短的一天,之后白天会逐渐变长。',
        '冬至有吃饺子的习俗,据说可以防止耳朵冻掉。'
      ]
    };
    
    for (const [keyword, responses] of Object.entries(mockResponses)) {
      if (message.includes(keyword)) {
        return responses[Math.floor(Math.random() * responses.length)];
      }
    }
    
    return '感谢您的提问!关于节气的问题,我可以为您提供详细解答。';
  }
}
AI服务设计要点

1. 单例模式

  • 确保全局只有一个AI服务实例
  • 便于统一管理配置和状态

2. Mock模式支持

  • 开发阶段使用Mock数据,无需依赖网络
  • 上线后切换为真实API调用

3. API调用封装

  • 统一错误处理
  • 超时设置
  • 请求头配置

4. 常见陷阱

  • API超时: 设置合理的超时时间,避免用户等待过久
  • 消息列表卡顿: 使用LazyForEach优化渲染
  • 状态同步问题: 使用AppStorage管理聊天状态

步骤2: 顶部导航

/**
 * 构建顶部导航
 */
@Builder
buildHeader(): void {
  Row({ space: 16 }) {
    Image($r('app.media.ic_back'))
      .width(24)
      .height(24)
      .fillColor('#333333')
      .onClick(() => {
        try {
          router.back();
        } catch (error) {
          console.error('返回失败: ' + JSON.stringify(error));
        }
      })
    
    Text('知识问答')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
    
    Blank()
  }
  .width('100%')
  .height(56)
  .padding({ left: 16, right: 16 })
  .backgroundColor('#FFFFFF')
}

设计要点:

  • 返回按钮
  • 标题居中
  • AI助手入口按钮

步骤2.5: AI聊天界面

/**
 * 构建AI聊天界面
 */
@Builder
buildChatInterface(): void {
  Column({ space: 0 }) {
    // 聊天消息列表
    List({ space: 16 }) {
      ForEach(this.chatMessages, (message: ChatMessage) => {
        ListItem() {
          this.buildChatMessageItem(message)
        }
      }, (message: ChatMessage) => message.id)
      
      if (this.isLoading) {
        ListItem() {
          Row({ space: 8 }) {
            Image($r('app.media.ic_ai_avatar'))
              .width(40)
              .height(40)
              .borderRadius(20)
            
            Row({ space: 4 }) {
              Row().width(8).height(8).backgroundColor('#CCCCCC').borderRadius(4)
              Row().width(8).height(8).backgroundColor('#CCCCCC').borderRadius(4)
              Row().width(8).height(8).backgroundColor('#CCCCCC').borderRadius(4)
            }
            .padding({ left: 12, right: 12, top: 8, bottom: 8 })
            .backgroundColor('#F0F0F0')
            .borderRadius(16)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
        }
      }
    }
    .width('100%')
    .flexGrow(1)
    .padding({ top: 16, left: 16, right: 16 })
    
    // 输入区域
    this.buildChatInput()
  }
}

/**
 * 构建聊天消息项
 */
@Builder
buildChatMessageItem(message: ChatMessage): void {
  Row({ space: 12 }) {
    if (message.role === 'assistant') {
      Image($r('app.media.ic_ai_avatar'))
        .width(40)
        .height(40)
        .borderRadius(20)
    }
    
    Text(message.content)
      .fontSize(14)
      .fontColor(message.role === 'user' ? '#FFFFFF' : '#333333')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .backgroundColor(message.role === 'user' ? '#4A9B6D' : '#FFFFFF')
      .borderRadius(message.role === 'user' ? { topLeft: 20, topRight: 4, bottomLeft: 20, bottomRight: 20 } : { topLeft: 4, topRight: 20, bottomLeft: 20, bottomRight: 20 })
      .maxWidth('70%')
    
    if (message.role === 'user') {
      Image($r('app.media.ic_default_avatar'))
        .width(40)
        .height(40)
        .borderRadius(20)
    }
  }
  .width('100%')
  .justifyContent(message.role === 'user' ? FlexAlign.End : FlexAlign.Start)
}

/**
 * 构建聊天输入框
 */
@Builder
buildChatInput(): void {
  Row({ space: 12 }) {
    Image($r('app.media.ic_default_avatar'))
      .width(40)
      .height(40)
      .borderRadius(20)
    
    Stack() {
      TextInput({ placeholder: '向AI助手提问...' })
        .width('100%')
        .height(40)
        .backgroundColor('#FFFFFF')
        .borderRadius(20)
        .padding({ left: 16, right: 80 })
        .onChange((value: string) => {
          this.inputMessage = value;
        })
      
      Button('发送')
        .width(60)
        .height(32)
        .backgroundColor(this.inputMessage.trim() ? '#4A9B6D' : '#DDDDDD')
        .fontColor('#FFFFFF')
        .fontSize(13)
        .borderRadius(16)
        .position({ right: 8, top: 4 })
        .onClick(() => {
          this.sendAIMessage();
        })
    }
    .flexGrow(1)
  }
  .width('92%')
  .padding({ bottom: 20 })
}

设计要点:

  • AI助手头像和用户头像区分
  • 气泡样式差异化(用户/助手)
  • 加载状态动画
  • 输入框与发送按钮联动

步骤3: 提问按钮

/**
 * 构建提问按钮
 */
@Builder
buildAskButton(): void {
  Row({ space: 8 }) {
    Image($r('app.media.ic_question'))
      .width(24)
      .height(24)
      .fillColor('#4A9B6D')
    
    Text('我有问题')
      .fontSize(15)
      .fontColor('#666666')
    
    Blank()
    
    Image($r('app.media.ic_arrow_right'))
      .width(16)
      .height(16)
      .fillColor('#CCCCCC')
  }
  .width('92%')
  .height(48)
  .backgroundColor('#FFFFFF')
  .borderRadius(24)
  .padding({ left: 16, right: 16 })
  .margin({ top: 12 })
  .onClick(() => {
    this.showAskDialog();
  })
}

/**
 * 显示提问弹窗
 */
showAskDialog(): void {
  prompt.showToast({ message: '提问功能开发中' });
}

设计要点:

  • 圆角按钮
  • 图标+文字
  • 点击弹出提问表单

步骤4: 问答列表

/**
 * 构建问答列表
 */
@Builder
buildQuestionList(): void {
  List({ space: 12 }) {
    ForEach(this.questions, (question: Question) => {
      ListItem() {
        this.buildQuestionCard(question)
      }
    }, (question: Question) => question.id)
  }
  .width('92%')
  .padding({ top: 12, bottom: 100 })
}

/**
 * 构建问答卡片
 */
@Builder
buildQuestionCard(question: Question): void {
  Card() {
    Column({ space: 12 }) {
      // 用户信息
      Row({ space: 12 }) {
        Image($r('app.media.ic_default_avatar'))
          .width(40)
          .height(40)
          .borderRadius(20)
        
        Column({ space: 4 }) {
          Text(question.userName)
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
          
          Text(question.time)
            .fontSize(12)
            .fontColor('#999999')
        }
        
        Blank()
        
        // 点赞按钮
        Row({ space: 4 }) {
          Image(question.isLiked ? $r('app.media.ic_like_active') : $r('app.media.ic_like'))
            .width(20)
            .height(20)
            .fillColor(question.isLiked ? '#FF5252' : '#999999')
          
          Text(question.likes.toString())
            .fontSize(13)
            .fontColor(question.isLiked ? '#FF5252' : '#999999')
        }
        .onClick(() => {
          this.likeQuestion(question.id);
        })
      }
      
      // 问题内容
      Column({ space: 8 }) {
        Text(question.title)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        
        Text(question.content)
          .fontSize(14)
          .fontColor('#666666')
          .lineHeight(24)
      }
      
      // 回答区域
      Column({ space: 0 }) {
        // 回答数量和展开按钮
        Row({ space: 8 }) {
          Text('回答 (' + question.answers.length + ')')
            .fontSize(14)
            .fontColor('#999999')
          
          if (question.answers.length > 0) {
            Text(this.expandedQuestionId === question.id ? '收起' : '查看')
              .fontSize(13)
              .fontColor('#4A9B6D')
              .onClick(() => {
                this.toggleAnswers(question.id);
              })
          }
        }
        
        // 展开的回答列表
        if (this.expandedQuestionId === question.id && question.answers.length > 0) {
          Column({ space: 12 }) {
            ForEach(question.answers, (answer: Answer) => {
              this.buildAnswerItem(question.id, answer)
            }, (answer: Answer) => answer.id)
          }
          .margin({ top: 12 })
        }
        
        // 回答输入框
        if (this.expandedQuestionId === question.id) {
          this.buildAnswerInput(question.id);
        }
      }
    }
    .padding(16)
  }
}

设计要点:

  • 卡片式布局
  • 用户信息展示
  • 问题标题和内容
  • 回答区域(可展开/收起)

步骤5: 回答项组件

/**
 * 构建回答项
 */
@Builder
buildAnswerItem(questionId: string, answer: Answer): void {
  Row({ space: 12 }) {
    Image($r('app.media.ic_default_avatar'))
      .width(36)
      .height(36)
      .borderRadius(18)
    
    Column({ space: 8 }) {
      Row({ space: 8 }) {
        Text(answer.userName)
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
        
        Text(answer.time)
          .fontSize(12)
          .fontColor('#999999')
      }
      
      Text(answer.content)
        .fontSize(14)
        .fontColor('#555555')
        .lineHeight(22)
      
      Row({ space: 16 }) {
        Row({ space: 4 }) {
          Image(answer.isLiked ? $r('app.media.ic_like_active') : $r('app.media.ic_like'))
            .width(16)
            .height(16)
            .fillColor(answer.isLiked ? '#FF5252' : '#999999')
          
          Text(answer.likes.toString())
            .fontSize(12)
            .fontColor('#999999')
        }
        .onClick(() => {
          this.likeAnswer(questionId, answer.id);
        })
        
        Text('回复')
          .fontSize(12)
          .fontColor('#999999')
      }
    }
    .flexGrow(1)
  }
}

/**
 * 构建回答输入框
 */
@Builder
buildAnswerInput(questionId: string): void {
  @State answerContent: string = '';
  
  Row({ space: 12 }) {
    Image($r('app.media.ic_default_avatar'))
      .width(40)
      .height(40)
      .borderRadius(20)
    
    Stack() {
      TextInput({ placeholder: '写下你的回答...' })
        .width('100%')
        .height(40)
        .backgroundColor('#F5F5F5')
        .borderRadius(20)
        .padding({ left: 16, right: 80 })
        .onChange((value: string) => {
          this.answerContent = value;
        })
      
      Button('发送')
        .width(60)
        .height(32)
        .backgroundColor(this.answerContent.trim() ? '#4A9B6D' : '#DDDDDD')
        .fontColor('#FFFFFF')
        .fontSize(13)
        .borderRadius(16)
        .position({ right: 8, top: 4 })
        .onClick(() => {
          if (this.answerContent.trim()) {
            this.submitAnswer(questionId, this.answerContent);
            this.answerContent = '';
          }
        })
    }
    .flexGrow(1)
  }
  .margin({ top: 12 })
}

设计要点:

  • 回答项布局(头像+内容)
  • 点赞功能
  • 回答输入框

本章小结

核心知识点

本文完成了知识问答页面的实现:

1. 问答列表

  • 卡片式布局展示问答
  • 用户信息展示
  • 问题标题和内容

2. 提问功能

  • 提问按钮
  • 弹窗表单(待实现)

3. 回答功能

  • 展开/收起回答列表
  • 回答输入框
  • 提交回答

4. 点赞功能

  • 问题点赞
  • 回答点赞
  • 状态切换动画

下一步预告

知识问答页面已经完成!在下一篇文章中,我们将学习:

  • 意见反馈页面
  • 反馈表单
  • 问题分类
  • 提交反馈

节气通应用已发布上线,可在应用市场下载体验


相关链接

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐