鸿蒙UI开发总是踩坑?ArkUI组件用法记不住?本文用15分钟带你彻底搞懂ArkUI核心组件、布局系统、自定义组件和交互动画,附完整培训班管理系统实战代码和踩坑记录,让你的鸿蒙App界面从此丝滑流畅!


一、培训班管理界面设计

1.1 界面设计原则

  1. 简洁明了:界面布局清晰,信息层次分明

  2. 操作便捷:常用功能一键直达,减少操作步骤

  3. 视觉统一:色彩、字体、间距保持一致

  4. 响应迅速:交互反馈及时,动画流畅自然

1.2 设计规范

元素

规范

说明

主色调

#007DFF

华为蓝,品牌一致性

辅助色

#FF6B6B

警告/删除

成功色

#52C41A

成功状态

字体

HarmonyOS Sans

系统默认字体

圆角

8px

统一圆角

间距

16px

基础间距

1.3 页面结构

┌─────────────────────────────────────┐
│           标题栏 (56px)              │
├─────────────────────────────────────┤
│           搜索栏 (48px)             │
├─────────────────────────────────────┤
│                                     │
│         内容区域 (flex)              │
│                                     │
├─────────────────────────────────────┤
│           底部导航 (56px)            │
└─────────────────────────────────────┘

二、学员列表页面实现

2.1 页面结构

// pages/StudentList.ets
import { Student } from '../model/Student';
import { StudentService } from '../service/StudentService';

@Entry
@Component
struct StudentList {
  @State studentList: Student[] = [];
  @State isLoading: boolean = true;
  @State searchText: string = '';

  private studentService: StudentService = new StudentService();

  async aboutToAppear() {
    await this.loadStudentList();
  }

  async loadStudentList() {
    this.isLoading = true;
    this.studentList = await this.studentService.getAllStudents();
    this.isLoading = false;
  }

  build() {
    Column() {
      // 标题栏
      TitleBar({ title: '学员管理', showBack: false })

      // 搜索栏
      SearchBar({ searchText: $searchText })

      // 学员列表
      if (this.isLoading) {
        LoadingView()
      } else if (this.studentList.length === 0) {
        EmptyView({ message: '暂无学员数据' })
      } else {
        List({ space: 12 }) {
          ForEach(this.studentList, (student: Student) => {
            ListItem() {
              StudentCard({ student: student })
            }
          }, (student: Student) => student.id)
        }
        .padding(16)
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

2.2 学员卡片组件

// components/student/StudentCard.ets
import { Student } from '../../model/Student';

@Component
export struct StudentCard {
  @ObjectLink student: StudentObserved;
  clickCallback: (student: Student) => void = () => {};

  build() {
    Row() {
      // 头像
      Image(this.student.avatar || $r('app.media.ic_default_avatar'))
        .width(48)
        .height(48)
        .borderRadius(24)
        .backgroundColor('#E8E8E8')

      // 信息区域
      Column() {
        // 姓名
        Text(this.student.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')

        // 手机号
        Text(this.student.phone)
          .fontSize(14)
          .fontColor('#666666')
          .margin({ top: 4 })

        // 标签
        Row() {
          Text(this.student.gender === 1 ? '男' : '女')
            .fontSize(12)
            .fontColor('#FFFFFF')
            .backgroundColor(this.student.gender === 1 ? '#007DFF' : '#FF6B6B')
            .borderRadius(4)
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })

          Text(`${this.student.age}岁`)
            .fontSize(12)
            .fontColor('#999999')
            .margin({ left: 8 })
        }
        .margin({ top: 8 })
      }
      .layoutWeight(1)
      .margin({ left: 12 })
      .alignItems(ItemAlign.Start)

      // 箭头
      Image($r('app.media.ic_arrow_right'))
        .width(16)
        .height(16)
        .fillColor('#CCCCCC')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .onClick(() => {
      this.clickCallback(this.student);
    })
  }
}

@Observed
class StudentObserved implements Student {
  id: string = '';
  name: string = '';
  phone: string = '';
  email: string = '';
  gender: number = 0;
  birthday: string = '';
  address: string = '';
  avatar: string = '';
  age: number = 0;
  createTime: string = '';
  updateTime: string = '';
}

2.3 搜索栏组件

// components/common/SearchBar.ets
@Component
export struct SearchBar {
  @Link searchText: string;
  searchCallback: (text: string) => void = () => {};

  build() {
    Row() {
      // 搜索图标
      Image($r('app.media.ic_search'))
        .width(20)
        .height(20)
        .fillColor('#999999')
        .margin({ left: 12 })

      // 搜索输入框
      TextInput({ placeholder: '搜索学员姓名或手机号' })
        .layoutWeight(1)
        .height(40)
        .backgroundColor('transparent')
        .fontSize(14)
        .placeholderColor('#CCCCCC')
        .margin({ left: 8 })
        .onChange((value: string) => {
          this.searchText = value;
          this.searchCallback(value);
        })

      // 清除按钮
      if (this.searchText.length > 0) {
        Image($r('app.media.ic_clear'))
          .width(16)
          .height(16)
          .fillColor('#999999')
          .margin({ right: 12 })
          .onClick(() => {
            this.searchText = '';
            this.searchCallback('');
          })
      }
    }
    .width('100%')
    .height(48)
    .backgroundColor('#FFFFFF')
    .borderRadius(24)
    .margin({ left: 16, right: 16, top: 8, bottom: 8 })
  }
}

三、课程详情页面实现

3.1 页面结构

// pages/CourseDetail.ets
import { Course } from '../model/Course';

@Entry
@Component
struct CourseDetail {
  @State course: Course | null = null;
  @State isLoading: boolean = true;

  async aboutToAppear() {
    // 加载课程详情
    await this.loadCourseDetail();
  }

  async loadCourseDetail() {
    this.isLoading = true;
    // 从路由参数获取课程ID
    const courseId = 'course_001';
    this.course = await CourseService.getCourseById(courseId);
    this.isLoading = false;
  }

  build() {
    Column() {
      // 标题栏
      TitleBar({ title: '课程详情', showBack: true })

      if (this.isLoading) {
        LoadingView()
      } else if (this.course) {
        Scroll() {
          Column() {
            // 课程封面
            Image(this.course.coverImage)
              .width('100%')
              .height(200)
              .objectFit(ImageFit.Cover)

            // 课程信息
            Column() {
              // 课程名称
              Text(this.course.name)
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')

              // 课程描述
              Text(this.course.description)
                .fontSize(14)
                .fontColor('#666666')
                .margin({ top: 8 })
                .lineHeight(22)

              // 课程信息卡片
              Row() {
                this.InfoItem('授课老师', this.course.teacher)
                this.InfoItem('课程时长', `${this.course.duration}课时`)
                this.InfoItem('报名人数', `${this.course.currentStudents}/${this.course.maxStudents}`)
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceAround)
              .margin({ top: 16 })
              .padding(16)
              .backgroundColor('#F8F8F8')
              .borderRadius(8)

              // 课程大纲
              Column() {
                Text('课程大纲')
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')

                // 大纲列表
                ForEach(this.course.outline, (item: string, index: number) => {
                  Row() {
                    Text(`${index + 1}`)
                      .fontSize(14)
                      .fontColor('#FFFFFF')
                      .backgroundColor('#007DFF')
                      .width(24)
                      .height(24)
                      .borderRadius(12)
                      .textAlign(TextAlign.Center)

                    Text(item)
                      .fontSize(14)
                      .fontColor('#666666')
                      .margin({ left: 12 })
                      .layoutWeight(1)
                  }
                  .width('100%')
                  .margin({ top: 12 })
                })
              }
              .width('100%')
              .margin({ top: 16 })
            }
            .padding(16)
          }
        }
        .layoutWeight(1)

        // 底部按钮
        Row() {
          // 价格
          Column() {
            Text('课程价格')
              .fontSize(12)
              .fontColor('#999999')

            Text(`¥${this.course.price}`)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF6B6B')
          }
          .alignItems(ItemAlign.Start)

          // 报名按钮
          Button('立即报名')
            .width(120)
            .height(44)
            .backgroundColor('#007DFF')
            .borderRadius(22)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
              // 跳转报名页面
            })
        }
        .width('100%')
        .height(64)
        .padding({ left: 16, right: 16 })
        .backgroundColor('#FFFFFF')
        .borderWidth({ top: 1 })
        .borderColor('#EEEEEE')
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  InfoItem(label: string, value: string) {
    Column() {
      Text(value)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')

      Text(label)
        .fontSize(12)
        .fontColor('#999999')
        .margin({ top: 4 })
    }
    .alignItems(ItemAlign.Center)
  }
}

四、报名页面实现

4.1 报名表单

// pages/EnrollmentPage.ets
import { Student } from '../model/Student';
import { Course } from '../model/Course';
import { Enrollment } from '../model/Enrollment';

@Entry
@Component
struct EnrollmentPage {
  @State selectedStudent: Student | null = null;
  @State selectedCourse: Course | null = null;
  @State remark: string = '';
  @State isSubmitting: boolean = false;

  build() {
    Column() {
      // 标题栏
      TitleBar({ title: '学员报名', showBack: true })

      Scroll() {
        Column() {
          // 选择学员
          this.SectionTitle('选择学员')
          this.StudentSelector()

          // 选择课程
          this.SectionTitle('选择课程')
          this.CourseSelector()

          // 备注信息
          this.SectionTitle('备注信息')
          TextInput({ placeholder: '请输入备注信息(选填)' })
            .width('100%')
            .height(100)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .padding(12)
            .fontSize(14)
            .margin({ top: 8 })
            .onChange((value: string) => {
              this.remark = value;
            })

          // 报名信息确认
          if (this.selectedStudent && this.selectedCourse) {
            this.SectionTitle('报名信息确认')
            this.EnrollmentSummary()
          }
        }
        .padding(16)
      }
      .layoutWeight(1)

      // 提交按钮
      Button(this.isSubmitting ? '提交中...' : '确认报名')
        .width('90%')
        .height(48)
        .backgroundColor(this.canSubmit() ? '#007DFF' : '#CCCCCC')
        .borderRadius(24)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 24 })
        .enabled(this.canSubmit() && !this.isSubmitting)
        .onClick(() => {
          this.submitEnrollment();
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  SectionTitle(title: string) {
    Text(title)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
      .margin({ top: 16, bottom: 8 })
  }

  @Builder
  StudentSelector() {
    Row() {
      if (this.selectedStudent) {
        // 已选择学员
        Row() {
          Image(this.selectedStudent.avatar || $r('app.media.ic_default_avatar'))
            .width(40)
            .height(40)
            .borderRadius(20)

          Column() {
            Text(this.selectedStudent.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')

            Text(this.selectedStudent.phone)
              .fontSize(12)
              .fontColor('#666666')
          }
          .margin({ left: 12 })
          .alignItems(ItemAlign.Start)
        }
        .layoutWeight(1)

        Text('更换')
          .fontSize(14)
          .fontColor('#007DFF')
          .onClick(() => {
            // 打开学员选择弹窗
          })
      } else {
        // 未选择学员
        Text('请选择学员')
          .fontSize(14)
          .fontColor('#CCCCCC')
          .layoutWeight(1)

        Image($r('app.media.ic_arrow_right'))
          .width(16)
          .height(16)
          .fillColor('#CCCCCC')
      }
    }
    .width('100%')
    .height(64)
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .onClick(() => {
      // 打开学员选择弹窗
    })
  }

  @Builder
  CourseSelector() {
    Row() {
      if (this.selectedCourse) {
        // 已选择课程
        Column() {
          Text(this.selectedCourse.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Text(`${this.course.teacher} | ${this.course.duration}课时 | ¥${this.course.price}`)
            .fontSize(12)
            .fontColor('#666666')
            .margin({ top: 4 })
        }
        .alignItems(ItemAlign.Start)
        .layoutWeight(1)

        Text('更换')
          .fontSize(14)
          .fontColor('#007DFF')
          .onClick(() => {
            // 打开课程选择弹窗
          })
      } else {
        // 未选择课程
        Text('请选择课程')
          .fontSize(14)
          .fontColor('#CCCCCC')
          .layoutWeight(1)

        Image($r('app.media.ic_arrow_right'))
          .width(16)
          .height(16)
          .fillColor('#CCCCCC')
      }
    }
    .width('100%')
    .height(64)
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .onClick(() => {
      // 打开课程选择弹窗
    })
  }

  @Builder
  EnrollmentSummary() {
    Column() {
      this.SummaryItem('学员姓名', this.selectedStudent?.name || '')
      this.SummaryItem('联系电话', this.selectedStudent?.phone || '')
      this.SummaryItem('课程名称', this.selectedCourse?.name || '')
      this.SummaryItem('授课老师', this.selectedCourse?.teacher || '')
      this.SummaryItem('课程费用', `¥${this.selectedCourse?.price || 0}`)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }

  @Builder
  SummaryItem(label: string, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#666666')
        .width(80)

      Text(value)
        .fontSize(14)
        .fontColor('#333333')
        .layoutWeight(1)
    }
    .width('100%')
    .height(40)
    .borderWidth({ bottom: 1 })
    .borderColor('#F0F0F0')
  }

  canSubmit(): boolean {
    return this.selectedStudent !== null && this.selectedCourse !== null;
  }

  async submitEnrollment() {
    if (!this.canSubmit()) return;

    this.isSubmitting = true;

    try {
      const enrollment: Enrollment = {
        id: Utils.generateId(),
        studentId: this.selectedStudent!.id,
        courseId: this.selectedCourse!.id,
        enrollTime: Utils.formatDate(new Date()),
        status: 0,
        remark: this.remark,
        createTime: Utils.formatDate(new Date()),
        updateTime: Utils.formatDate(new Date())
      };

      await EnrollmentService.createEnrollment(enrollment);
      Utils.showToast('报名成功');
      router.back();
    } catch (error) {
      Utils.showToast('报名失败,请重试');
    } finally {
      this.isSubmitting = false;
    }
  }
}

五、响应式布局与多设备适配

5.1 断点系统

// common/BreakpointSystem.ets
export class BreakpointSystem {
  private static instance: BreakpointSystem;
  private currentBreakpoint: string = 'md';

  private constructor() {}

  static getInstance(): BreakpointSystem {
    if (!BreakpointSystem.instance) {
      BreakpointSystem.instance = new BreakpointSystem();
    }
    return BreakpointSystem.instance;
  }

  getCurrentBreakpoint(): string {
    return this.currentBreakpoint;
  }

  updateBreakpoint(windowWidth: number): void {
    if (windowWidth < 600) {
      this.currentBreakpoint = 'sm';
    } else if (windowWidth < 840) {
      this.currentBreakpoint = 'md';
    } else {
      this.currentBreakpoint = 'lg';
    }
  }
}

export const BreakpointConstants = {
  SM: 'sm',    // 手机
  MD: 'md',    // 平板
  LG: 'lg'     // 桌面
};

5.2 响应式布局实现

// components/common/ResponsiveLayout.ets
@Component
export struct ResponsiveLayout {
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md';

  build() {
    Column() {
      if (this.currentBreakpoint === 'sm') {
        // 手机布局:单列
        this.PhoneLayout()
      } else if (this.currentBreakpoint === 'md') {
        // 平板布局:双列
        this.TabletLayout()
      } else {
        // 桌面布局:三列
        this.DesktopLayout()
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  PhoneLayout() {
    Column() {
      // 单列布局内容
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  TabletLayout() {
    Row() {
      // 左侧面板
      Column() {
        // 左侧内容
      }
      .width('40%')
      .height('100%')

      // 右侧内容
      Column() {
        // 右侧内容
      }
      .layoutWeight(1)
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  DesktopLayout() {
    Row() {
      // 左侧面板
      Column() {
        // 左侧内容
      }
      .width('25%')
      .height('100%')

      // 中间内容
      Column() {
        // 中间内容
      }
      .layoutWeight(1)
      .height('100%')

      // 右侧面板
      Column() {
        // 右侧内容
      }
      .width('25%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

5.3 媒体查询

// common/MediaQueryHelper.ets
import mediaQuery from '@ohos.mediaquery';

export class MediaQueryHelper {
  private static listeners: Map<string, mediaQuery.MediaQueryListener> = new Map();

  static registerListener(
    condition: string,
    callback: (matches: boolean) => void
  ): void {
    const listener = mediaQuery.matchMediaSync(condition);
    listener.on('change', (matches: boolean) => {
      callback(matches);
    });
    this.listeners.set(condition, listener);
  }

  static unregisterListener(condition: string): void {
    const listener = this.listeners.get(condition);
    if (listener) {
      listener.off('change');
      this.listeners.delete(condition);
    }
  }

  static isPhone(): boolean {
    return !mediaQuery.matchMediaSync('(min-width: 600px)').matches;
  }

  static isTablet(): boolean {
    return mediaQuery.matchMediaSync('(min-width: 600px)').matches &&
           !mediaQuery.matchMediaSync('(min-width: 840px)').matches;
  }

  static isDesktop(): boolean {
    return mediaQuery.matchMediaSync('(min-width: 840px)').matches;
  }
}

六、交互动画与用户体验优化

6.1 页面转场动画

// common/TransitionAnimation.ets
export class TransitionAnimation {
  static slideInFromRight(): TransitionEffect {
    return TransitionEffect.asymmetric(
      TransitionEffect.move(TransitionEdge.START)
        .animation({ duration: 300, curve: Curve.EaseInOut }),
      TransitionEffect.move(TransitionEdge.END)
        .animation({ duration: 300, curve: Curve.EaseInOut })
    );
  }

  static fadeIn(): TransitionEffect {
    return TransitionEffect.opacity(0)
      .animation({ duration: 200, curve: Curve.EaseInOut });
  }

  static scaleIn(): TransitionEffect {
    return TransitionEffect.scale({ x: 0.8, y: 0.8 })
      .animation({ duration: 200, curve: Curve.EaseInOut });
  }
}

6.2 列表项动画

// components/common/AnimatedListItem.ets
@Component
export struct AnimatedListItem {
  @State isVisible: boolean = false;
  @Prop index: number = 0;

  aboutToAppear() {
    // 延迟显示,实现依次出现效果
    setTimeout(() => {
      this.isVisible = true;
    }, this.index * 50);
  }

  build() {
    Column() {
      // 列表项内容
    }
    .width('100%')
    .opacity(this.isVisible ? 1 : 0)
    .translate({ y: this.isVisible ? 0 : 20 })
    .animation({
      duration: 300,
      curve: Curve.EaseInOut,
      delay: this.index * 50
    })
  }
}

6.3 按钮点击反馈

// components/common/ActionButton.ets
@Component
export struct ActionButton {
  @Prop label: string = '';
  @Prop icon: Resource | null = null;
  @State isPressed: boolean = false;
  clickCallback: () => void = () => {};

  build() {
    Column() {
      if (this.icon) {
        Image(this.icon)
          .width(24)
          .height(24)
          .fillColor(this.isPressed ? '#007DFF' : '#666666')
      }

      Text(this.label)
        .fontSize(12)
        .fontColor(this.isPressed ? '#007DFF' : '#666666')
        .margin({ top: 4 })
    }
    .width(64)
    .height(64)
    .justifyContent(FlexAlign.Center)
    .scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 })
    .animation({ duration: 100, curve: Curve.EaseInOut })
    .onTouch((event: TouchEvent) => {
      if (event.type === TouchType.Down) {
        this.isPressed = true;
      } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
        this.isPressed = false;
        if (event.type === TouchType.Up) {
          this.clickCallback();
        }
      }
    })
  }
}

6.4 下拉刷新

// components/common/PullToRefresh.ets
@Component
export struct PullToRefresh {
  @Prop isRefreshing: boolean = false;
  refreshCallback: () => void = () => {};

  build() {
    Refresh({ refreshing: $$this.isRefreshing }) {
      // 列表内容
    }
    .onRefreshing(() => {
      this.refreshCallback();
    })
  }
}

6.5 加载状态动画

// components/common/LoadingView.ets
@Component
export struct LoadingView {
  @State rotationAngle: number = 0;

  aboutToAppear() {
    this.startRotation();
  }

  startRotation() {
    setInterval(() => {
      this.rotationAngle += 10;
    }, 50);
  }

  build() {
    Column() {
      // 加载图标
      Image($r('app.media.ic_loading'))
        .width(40)
        .height(40)
        .rotate({ angle: this.rotationAngle })

      // 加载文字
      Text('加载中...')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 12 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

七、踩坑记录

7.1 组件复用问题

问题:ForEach渲染的列表项状态混乱。

原因:ForEach的keyGenerator函数返回值不唯一。

解决方案

ForEach(this.list, (item: Item) => {
  // 列表项内容
}, (item: Item) => item.id)  // 确保id唯一

7.2 动画性能问题

问题:复杂动画导致界面卡顿。

原因:动画属性过多或动画时长过长。

解决方案

// 使用硬件加速
.animation({
  duration: 300,
  curve: Curve.EaseInOut,
  tempo: 1.0,  // 使用默认节奏
  iterations: 1
})

7.3 响应式布局问题

问题:不同设备上布局错乱。

原因:使用固定尺寸而非相对尺寸。

解决方案

// 使用layoutWeight而非固定宽度
.layoutWeight(1)

// 使用百分比而非固定像素
.width('80%')

八、总结与预告

本文要点回顾

  1. 界面设计:遵循设计规范,保持视觉统一

  2. 组件开发:封装可复用组件,提高开发效率

  3. 列表实现:实现学员列表、课程详情、报名页面

  4. 响应式布局:适配手机、平板、桌面多设备

  5. 交互动画:提升用户体验,增加界面流畅感

下期预告

下期我们将深入讲解状态管理篇,包括:

  • 学员信息状态管理

  • 课程数据状态流转

  • 报名流程状态控制

  • @State、@Prop、@Link实战应用

互动引导

如果本文对你有帮助,请点赞、收藏、关注!有任何问题欢迎在评论区留言,我会及时回复。


系列文章导航

  • 第1篇:项目架构篇

  • 第2篇:UI界面篇(本文)

  • 第3篇:状态管理篇

  • 第4篇:数据持久化篇

  • 第5篇:性能优化篇

Logo

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

更多推荐