第5篇:列表渲染——展示大量数据 鸿蒙中文编程
本文介绍了如何使用ForEach循环和List组件高效展示大量数据。主要内容包括:1) ForEach循环的基本语法和示例,展示如何遍历数组渲染内容;2) List和ListItem组件的使用方法,优化长列表性能;3) 定义数据类型的重要性;4) 列表交互功能实现,如点击和滑动操作。文章通过班级通讯录等实例,演示了如何在实际项目中应用这些技术,帮助开发者掌握高效渲染大量数据的技巧。
第5篇:列表渲染——展示大量数据
本课目标:掌握ForEach循环渲染和List组件,能展示大量数据
**作者:**中文编程倡导者—— 李金雨
预计课时:2课时(90分钟)
难度等级:⭐⭐⭐(进阶)
一、开篇引入
1.1 生活中的列表
生活中到处都有"列表":
- 📋 班级花名册(50个同学的名字)
- 🛒 购物清单(要买的东西)
- 📱 微信通讯录(几百个好友)
- 📰 新闻列表(几十条新闻)
如果让你一个个写出来,那太麻烦了!
1.2 程序中的列表
假设你要显示一个班级50个学生的信息:
笨方法(不要这样做):
Text("张三")
Text("李四")
Text("王五")
// ... 重复50次!
聪明方法:
ForEach(学生列表, (学生) => {
Text(学生.姓名)
})
// 一行代码搞定!
1.3 本课目标
今天我们要学习:
- 用
ForEach循环显示列表 - 用
List和ListItem优化长列表 - 列表的点击和交互
- 实战:通讯录、商品列表、待办事项
1.4 预期成果
完成本课后,你能做出这样的应用:
通讯录: 商品列表: 待办事项:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 📇 通讯录 │ │ 🛍️ 商品列表 │ │ 📋 待办事项 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ 👤 张三 │ │ 📱 手机 ¥2999│ │ ☑️ 完成作业 │
│ 👤 李四 │ │ 💻 电脑 ¥5999│ │ ☐ 买牛奶 │
│ 👤 王五 │ │ 🎧 耳机 ¥199 │ │ ☑️ 锻炼身体 │
│ 👤 赵六 │ │ ⌚ 手表 ¥899 │ │ ☐ 洗衣服 │
│ ... │ │ ... │ │ ... │
│ │ │ │ │ │
│ [添加] │ │ [购物车] │ │ [+ 新建] │
└─────────────┘ └─────────────┘ └─────────────┘
二、概念讲解
2.1 ForEach——循环渲染
什么是ForEach?
ForEach 就像一个"复制机":
- 你给它一个列表
- 告诉它每个项目怎么显示
- 它会自动为每个项目生成对应的界面
基本语法
ForEach(
数据列表, // 要遍历的数组
(项目: 类型, 索引: number) => { // 对每个项目做什么
// 界面代码
},
(项目: 类型) => 唯一标识 // 可选:帮助ArkTS识别每个项目
)
简单例子
@State 水果列表: string[] = ["苹果", "香蕉", "橙子", "葡萄"]
build() {
Column() {
ForEach(this.水果列表, (水果: string) => {
Text(水果)
.fontSize(20)
.margin(10)
})
}
}
效果:
苹果
香蕉
橙子
葡萄
带索引的例子
ForEach(this.水果列表, (水果: string, 序号: number) => {
Text(`${序号 + 1}. ${水果}`)
.fontSize(20)
})
效果:
1. 苹果
2. 香蕉
3. 橙子
4. 葡萄
2.2 List和ListItem——优化长列表
为什么要用List?
当列表很长时(比如1000条数据),如果全部显示出来:
- 占用大量内存
- 界面卡顿
- 加载慢
List组件会"智能加载":
- 只加载屏幕上能看到的部分
- 滑出屏幕的会自动回收
- 就像传送带,循环利用
List的基本用法
List({ space: 10 }) { // space: 列表项之间的间距
ForEach(this.数据列表, (项目) => {
ListItem() { // 每个列表项用ListItem包裹
// 列表项的内容
Text(项目.名称)
}
})
}
.width('100%')
.height('100%')
.padding(20)
List的重要属性
List() {
// ...
}
.width('100%')
.height('100%')
.listDirection(Axis.Vertical) // 垂直排列(默认)
.divider({ // 分隔线
strokeWidth: 1, // 线宽
color: '#EEEEEE' // 颜色
})
.edgeEffect(EdgeEffect.Spring) // 边缘效果(弹簧效果)
.scrollBar(BarState.Auto) // 滚动条(自动显示)
2.3 数据类型定义
为什么要定义类型?
当列表项比较复杂时(有多个字段),我们最好定义一个"模板":
// 定义学生信息的结构
interface 学生信息 {
学号: string
姓名: string
年龄: number
成绩: number
}
// 使用这个类型
@State 学生列表: 学生信息[] = [
{ 学号: "001", 姓名: "张三", 年龄: 15, 成绩: 95 },
{ 学号: "002", 姓名: "李四", 年龄: 16, 成绩: 88 },
{ 学号: "003", 姓名: "王五", 年龄: 15, 成绩: 92 }
]
好处:
- 代码更清晰
- 有自动提示
- 不容易出错
2.4 列表的交互
点击列表项
ListItem() {
Text(学生.姓名)
}
.onClick(() => {
console.log(`点击了${学生.姓名}`)
})
滑动操作(左滑删除)
ListItem() {
Text(学生.姓名)
}
.swipeAction({
end: { // 从右向左滑显示的操作
Button("删除")
.onClick(() => {
this.删除学生(学生.学号)
})
}
})
三、动手实践
3.1 基础练习:班级通讯录
做一个简单的班级通讯录:
// 完整可运行代码,复制到 Index.ets 即可运行
// 定义学生信息类型
interface 学生信息 {
学号: string
姓名: string
性别: string
年龄: number
电话: string
}
@Entry
@Component
struct Index {
@State 学生列表: 学生信息[] = [
{ 学号: "202401", 姓名: "张三", 性别: "男", 年龄: 15, 电话: "138****1111" },
{ 学号: "202402", 姓名: "李四", 性别: "女", 年龄: 15, 电话: "139****2222" },
{ 学号: "202403", 姓名: "王五", 性别: "男", 年龄: 16, 电话: "137****3333" },
{ 学号: "202404", 姓名: "赵六", 性别: "女", 年龄: 15, 电话: "136****4444" },
{ 学号: "202405", 姓名: "孙七", 性别: "男", 年龄: 16, 电话: "135****5555" },
{ 学号: "202406", 姓名: "周八", 性别: "女", 年龄: 15, 电话: "134****6666" },
{ 学号: "202407", 姓名: "吴九", 性别: "男", 年龄: 15, 电话: "133****7777" },
{ 学号: "202408", 姓名: "郑十", 性别: "女", 年龄: 16, 电话: "132****8888" }
]
@State 选中项: string = ""
build() {
Column() {
// 标题栏
Row() {
Text("📇 班级通讯录")
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(`共${this.学生列表.length}人`)
.fontSize(14)
.fontColor("#999999")
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(20)
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
// 学生列表
List({ space: 1 }) {
ForEach(this.学生列表, (学生: 学生信息) => {
ListItem() {
Row({ space: 15 }) {
// 头像
Text(学生.性别 == "男" ? "👦" : "👧")
.fontSize(40)
// 信息
Column({ space: 5 }) {
Text(学生.姓名)
.fontSize(18)
.fontWeight(FontWeight.Medium)
Row({ space: 10 }) {
Text(`学号: ${学生.学号}`)
.fontSize(12)
.fontColor("#999999")
Text(`${学生.年龄}岁`)
.fontSize(12)
.fontColor("#999999")
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 电话
Text(学生.电话)
.fontSize(14)
.fontColor("#2196F3")
}
.width('100%')
.padding(15)
.backgroundColor(this.选中项 == 学生.学号 ? '#E3F2FD' : '#FFFFFF')
}
.onClick(() => {
this.选中项 = 学生.学号
console.log(`选中了:${学生.姓名}`)
})
})
}
.width('100%')
.layoutWeight(1)
.divider({ strokeWidth: 1, color: '#EEEEEE' })
// 底部按钮
Row({ space: 20 }) {
Button("添加学生", { type: ButtonType.Capsule })
.backgroundColor('#4CAF50')
.onClick(() => {
this.添加学生()
})
Button("清空列表", { type: ButtonType.Capsule })
.backgroundColor('#F44336')
.onClick(() => {
this.学生列表 = []
})
}
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
添加学生() {
let 新学生: 学生信息 = {
学号: `2024${(this.学生列表.length + 9).toString().padStart(2, '0')}`,
姓名: `新同学${this.学生列表.length + 1}`,
性别: this.学生列表.length % 2 == 0 ? "男" : "女",
年龄: 15,
电话: "138****0000"
}
this.学生列表.push(新学生)
// 重新赋值触发更新
this.学生列表 = [...this.学生列表]
}
}
3.2 进阶练习:商品列表
做一个带分类和购物车的商品列表:
// 完整可运行代码,复制到 Index.ets 即可运行
// 定义商品类型
interface 商品信息 {
编号: string
名称: string
价格: number
原价: number
销量: number
分类: string
标签: string[]
}
@Entry
@Component
struct Index {
@State 商品列表: 商品信息[] = [
{ 编号: "P001", 名称: "无线蓝牙耳机", 价格: 299, 原价: 399, 销量: 12580, 分类: "数码", 标签: ["热销", "包邮"] },
{ 编号: "P002", 名称: "智能手环 Pro", 价格: 199, 原价: 259, 销量: 8560, 分类: "数码", 标签: ["新品"] },
{ 编号: "P003", 名称: "便携充电宝", 价格: 89, 原价: 129, 销量: 23150, 分类: "数码", 标签: ["爆款"] },
{ 编号: "P004", 名称: "纯棉T恤", 价格: 59, 原价: 99, 销量: 5680, 分类: "服装", 标签: ["特价"] },
{ 编号: "P005", 名称: "运动鞋", 价格: 299, 原价: 499, 销量: 3420, 分类: "服装", 标签: ["限时"] },
{ 编号: "P006", 名称: "零食大礼包", 价格: 49, 原价: 79, 销量: 18900, 分类: "食品", 标签: ["热销", "包邮"] },
{ 编号: "P007", 名称: "坚果礼盒", 价格: 128, 原价: 168, 销量: 5670, 分类: "食品", 标签: ["新品"] },
{ 编号: "P008", 名称: "保温杯", 价格: 69, 原价: 99, 销量: 9870, 分类: "家居", 标签: [] }
]
@State 当前分类: string = "全部"
@State 购物车数量: number = 0
// 获取分类列表
get 分类列表(): string[] {
let 分类集: Set<string> = new Set()
分类集.add("全部")
this.商品列表.forEach(商品 => 分类集.add(商品.分类))
return Array.from(分类集)
}
// 获取过滤后的商品
get 过滤后商品(): 商品信息[] {
if (this.当前分类 == "全部") {
return this.商品列表
}
return this.商品列表.filter(商品 => 商品.分类 == this.当前分类)
}
build() {
Column() {
// 标题栏
Row() {
Text("🛍️ 商品列表")
.fontSize(22)
.fontWeight(FontWeight.Bold)
Row({ space: 5 }) {
Text("🛒")
.fontSize(24)
Text(`${this.购物车数量}`)
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('#F44336')
.borderRadius(10)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(15)
.backgroundColor('#FFFFFF')
// 分类标签
Scroll() {
Row({ space: 10 }) {
ForEach(this.分类列表, (分类: string) => {
Text(分类)
.fontSize(14)
.fontColor(this.当前分类 == 分类 ? '#FFFFFF' : '#666666')
.padding({ left: 15, right: 15, top: 6, bottom: 6 })
.backgroundColor(this.当前分类 == 分类 ? '#2196F3' : '#F5F5F5')
.borderRadius(15)
.onClick(() => {
this.当前分类 = 分类
})
})
}
.padding(15)
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
// 商品列表
List({ space: 10 }) {
ForEach(this.过滤后商品, (商品: 商品信息) => {
ListItem() {
this.商品卡片(商品)
}
})
}
.padding(15)
.layoutWeight(1)
.backgroundColor('#F5F5F5')
}
.width('100%')
.height('100%')
}
// 商品卡片
@Builder
商品卡片(商品: 商品信息) {
Row({ space: 12 }) {
// 图片区域
Column() {
Text("📦")
.fontSize(50)
}
.width(100)
.height(100)
.backgroundColor('#E3F2FD')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
// 信息区域
Column({ space: 8 }) {
// 名称和标签
Row({ space: 5 }) {
Text(商品.名称)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
ForEach(商品.标签, (标签: string) => {
Text(标签)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
.backgroundColor('#FF5722')
.borderRadius(3)
})
}
// 价格
Row({ space: 8 }) {
Text(`¥${商品.价格}`)
.fontSize(20)
.fontColor('#F44336')
.fontWeight(FontWeight.Bold)
Text(`¥${商品.原价}`)
.fontSize(12)
.fontColor('#999999')
.decoration({ type: TextDecorationType.LineThrough })
Text(`${((1 - 商品.价格 / 商品.原价) * 100).toFixed(0)}折`)
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 4, right: 4 })
.backgroundColor('#FF5722')
.borderRadius(3)
}
// 销量和按钮
Row() {
Text(`已售${商品.销量}件`)
.fontSize(12)
.fontColor('#999999')
Blank()
Button("加入购物车", { type: ButtonType.Capsule })
.fontSize(12)
.height(28)
.backgroundColor('#2196F3')
.onClick(() => {
this.购物车数量++
})
}
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
}
3.3 挑战练习:待办事项列表
做一个功能完整的待办事项应用:
// 完整可运行代码,复制到 Index.ets 即可运行
// 定义待办事项类型
interface 待办事项 {
编号: string
内容: string
是否完成: boolean
创建时间: string
}
@Entry
@Component
struct Index {
@State 待办列表: 待办事项[] = [
{ 编号: "1", 内容: "完成数学作业", 是否完成: false, 创建时间: "2024-01-15" },
{ 编号: "2", 内容: "去超市买牛奶", 是否完成: true, 创建时间: "2024-01-15" },
{ 编号: "3", 内容: "锻炼身体30分钟", 是否完成: false, 创建时间: "2024-01-14" },
{ 编号: "4", 内容: "阅读课外书", 是否完成: false, 创建时间: "2024-01-14" }
]
@State 新事项内容: string = ""
@State 过滤条件: string = "全部" // 全部、未完成、已完成
// 计算属性
get 未完成数量(): number {
return this.待办列表.filter(事项 => !事项.是否完成).length
}
get 已完成数量(): number {
return this.待办列表.filter(事项 => 事项.是否完成).length
}
get 过滤后列表(): 待办事项[] {
switch (this.过滤条件) {
case "未完成":
return this.待办列表.filter(事项 => !事项.是否完成)
case "已完成":
return this.待办列表.filter(事项 => 事项.是否完成)
default:
return this.待办列表
}
}
build() {
Column() {
// 标题栏
Row() {
Column({ space: 5 }) {
Text("📋 待办事项")
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(`未完成 ${this.未完成数量} 项 · 已完成 ${this.已完成数量} 项`)
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Button("清空已完成")
.fontSize(12)
.height(32)
.backgroundColor('#F44336')
.onClick(() => {
this.待办列表 = this.待办列表.filter(事项 => !事项.是否完成)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(20)
.backgroundColor('#FFFFFF')
// 输入区域
Row({ space: 10 }) {
TextInput({ placeholder: "添加新事项...", text: this.新事项内容 })
.placeholderColor('#999999')
.height(45)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.layoutWeight(1)
.onChange((值: string) => {
this.新事项内容 = 值
})
Button("+", { type: ButtonType.Circle })
.width(45)
.height(45)
.fontSize(24)
.backgroundColor('#2196F3')
.enabled(this.新事项内容.length > 0)
.onClick(() => {
this.添加事项()
})
}
.padding(15)
.backgroundColor('#FFFFFF')
// 过滤标签
Row({ space: 15 }) {
ForEach(["全部", "未完成", "已完成"], (条件: string) => {
Text(条件)
.fontSize(14)
.fontColor(this.过滤条件 == 条件 ? '#2196F3' : '#666666')
.fontWeight(this.过滤条件 == 条件 ? FontWeight.Bold : FontWeight.Normal)
.padding({ bottom: 8 })
.border({
width: { bottom: this.过滤条件 == 条件 ? 2 : 0 },
color: '#2196F3'
})
.onClick(() => {
this.过滤条件 = 条件
})
})
}
.width('100%')
.padding({ left: 20, right: 20 })
.backgroundColor('#FFFFFF')
// 待办列表
List({ space: 1 }) {
ForEach(this.过滤后列表, (事项: 待办事项) => {
ListItem() {
this.待办项(事项)
}
.swipeAction({
end: {
Button("删除")
.width(80)
.height('100%')
.backgroundColor('#F44336')
.fontColor('#FFFFFF')
.onClick(() => {
this.删除事项(事项.编号)
})
}
})
})
}
.width('100%')
.layoutWeight(1)
.divider({ strokeWidth: 1, color: '#EEEEEE' })
.backgroundColor('#FFFFFF')
// 空状态提示
if (this.过滤后列表.length == 0) {
Column({ space: 10 }) {
Text("📝")
.fontSize(60)
Text("暂无事项")
.fontSize(16)
.fontColor('#999999')
}
.position({ x: '50%', y: '60%' })
.markAnchor({ x: '50%', y: '50%' })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 待办项组件
@Builder
待办项(事项: 待办事项) {
Row({ space: 12 }) {
// 复选框
Text(事项.是否完成 ? "☑️" : "⬜")
.fontSize(24)
.onClick(() => {
this.切换完成状态(事项.编号)
})
// 内容
Column({ space: 4 }) {
Text(事项.内容)
.fontSize(16)
.fontColor(事项.是否完成 ? '#999999' : '#333333')
.decoration({
type: 事项.是否完成 ? TextDecorationType.LineThrough : TextDecorationType.None
})
Text(事项.创建时间)
.fontSize(11)
.fontColor('#CCCCCC')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
}
// 添加事项
添加事项() {
if (this.新事项内容.trim() == "") return
let 新事项: 待办事项 = {
编号: Date.now().toString(),
内容: this.新事项内容.trim(),
是否完成: false,
创建时间: new Date().toLocaleDateString()
}
this.待办列表.unshift(新事项) // 添加到开头
this.待办列表 = [...this.待办列表]
this.新事项内容 = ""
}
// 切换完成状态
切换完成状态(编号: string) {
let 索引 = this.待办列表.findIndex(事项 => 事项.编号 == 编号)
if (索引 != -1) {
this.待办列表[索引].是否完成 = !this.待办列表[索引].是否完成
this.待办列表 = [...this.待办列表]
}
}
// 删除事项
删除事项(编号: string) {
this.待办列表 = this.待办列表.filter(事项 => 事项.编号 != 编号)
}
}
四、知识总结
4.1 核心概念回顾
- ForEach:循环渲染列表数据
- List/ListItem:优化长列表显示
- Interface:定义数据结构
- 计算属性:动态计算列表数据
4.2 关键代码速查
// 定义数据类型
interface 数据类型 {
字段1: string
字段2: number
}
// 定义状态数组
@State 列表: 数据类型[] = []
// ForEach循环渲染
ForEach(this.列表, (项目: 数据类型, 索引: number) => {
// 渲染每个项目
})
// List优化长列表
List({ space: 10 }) {
ForEach(this.列表, (项目) => {
ListItem() {
// 列表项内容
}
})
}
// 列表项点击
ListItem() {
// ...
}
.onClick(() => {
// 点击处理
})
// 左滑删除
ListItem() {
// ...
}
.swipeAction({
end: {
Button("删除")
}
})
4.3 数组操作速查
// 添加元素
this.列表 = [...this.列表, 新元素] // 添加到末尾
this.列表 = [新元素, ...this.列表] // 添加到开头
// 删除元素
this.列表 = this.列表.filter(item => item.id != 要删的id)
// 修改元素
this.列表 = this.列表.map(item =>
item.id == 要改的id ? { ...item, 字段: 新值 } : item
)
// 查找元素
let 找到了 = this.列表.find(item => item.id == 要找的id)
let 索引 = this.列表.findIndex(item => item.id == 要找的id)
// 过滤数组
let 过滤后 = this.列表.filter(item => item.条件 == true)
4.4 常见错误提醒
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
| 列表不更新 | 直接修改数组元素 | 重新赋值整个数组 |
| ForEach报错 | 数据类型不匹配 | 检查interface定义 |
| 列表项不显示 | 没加ListItem | 用ListItem包裹 |
| 滑动卡顿 | 没用List组件 | 改用List代替Column |
| 删除错乱 | 没有唯一标识 | 给ForEach加第三个参数 |
五、课后作业
5.1 巩固练习(必做)
练习1:音乐播放列表
做一个音乐列表:
- 显示歌曲名、歌手、时长
- 点击播放(改变样式)
- 可以删除歌曲
练习2:新闻列表
做一个新闻列表:
- 显示标题、摘要、发布时间
- 分类筛选(国内、国际、体育、娱乐)
- 下拉刷新(模拟)
练习3:聊天记录
做一个聊天界面:
- 左右消息气泡
- 显示发送时间
- 可以删除消息
5.2 创意编程(选做)
创意1:相册应用
- 显示照片网格
- 点击查看大图
- 可以删除照片
创意2:课程表
- 显示周一到周日的课程
- 点击课程显示详情
- 可以添加新课程
创意3:记账本
- 记录收入和支出
- 按月份筛选
- 显示收支统计
5.3 下篇预习
下一篇,我们将学习条件渲染,根据条件显示不同内容。预习问题:
- 怎么实现登录/未登录显示不同界面?
- 怎么显示/隐藏某些内容?
- 怎么根据状态显示不同颜色?
附录:更多列表技巧
技巧1:分组列表
List() {
// 第一组
ListItemGroup({ header: this.分组标题("水果") }) {
ForEach(this.水果列表, (水果) => {
ListItem() { /* ... */ }
})
}
// 第二组
ListItemGroup({ header: this.分组标题("蔬菜") }) {
ForEach(this.蔬菜列表, (蔬菜) => {
ListItem() { /* ... */ }
})
}
}
技巧2:索引列表(字母索引)
List({ space: 0, initialIndex: 0 }) {
// ...
}
.sticky(StickyStyle.Header) // 分组标题吸顶
技巧3:网格列表
Grid() {
ForEach(this.图片列表, (图片) => {
GridItem() {
Image(图片)
}
})
}
.columnsTemplate('1fr 1fr 1fr') // 3列
.rowsGap(10)
.columnsGap(10)
恭喜你完成了第5篇的学习! 🎉
现在你已经掌握了列表渲染的核心技能,可以处理大量数据的展示了。记住:用List优化长列表,用ForEach循环渲染,用interface规范数据!
下节课,我们将学习如何根据条件显示不同内容!
更多推荐

所有评论(0)