HarmonyOS开发:设备分组——设备分类管理
HarmonyOS开发:设备分组——设备分类管理
📌 核心要点:设备分组让几十个设备井井有条,按房间分、按类型分、按自定义标签分,批量控制一个分组搞定,分组数据用关系型数据库持久化保平安。
背景与动机
你家有30个智能设备。客厅5个灯、2个空调、1个窗帘;卧室3个灯、1个空调;厨房2个灯、1个油烟机……
打开App,30个设备平铺在一个列表里。你要关客厅的灯,得在30个设备里找5个客厅灯,一个一个关。找着找着,你关了卧室的灯——因为列表太长,滚着滚着就搞混了。
这不是用户的问题,是你的设计有问题。
设备分组就是解决"找设备难、批量操作难"的问题。 按房间分组,客厅的设备在一起,卧室的设备在一起;按类型分组,所有灯在一起,所有空调在一起。关客厅灯?选中"客厅"分组,一键全关。
分组看起来简单——不就是给设备打个标签嘛?但做起来有几个关键问题:一个设备能不能属于多个分组?分组层级要不要支持(客厅→灯→主灯)?批量控制失败了怎么办?分组数据怎么持久化?
核心原理
分组模型
设备分组有两种基本模型:房间分组和类型分组。
flowchart TD
A[全部设备] --> B{分组维度}
B -->|按房间| C[客厅]
B -->|按房间| D[卧室]
B -->|按房间| E[厨房]
B -->|按房间| F[卫生间]
B -->|按类型| G[灯光]
B -->|按类型| H[空调]
B -->|按类型| I[窗帘]
B -->|按类型| J[传感器]
C --> C1[客厅主灯]
C --> C2[客厅射灯]
C --> C3[客厅空调]
C --> C4[客厅窗帘]
G --> G1[客厅主灯]
G --> G2[客厅射灯]
G --> G3[卧室灯]
G --> G4[厨房灯]
C1 -.->|同一设备可属于多个分组| G1
classDef root fill:#1565C0,color:#fff,stroke:#0D47A1
classDef room fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef type fill:#E65100,color:#fff,stroke:#BF360C
classDef device fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,root
class C,D,E,F,room
class G,H,I,J,type
class C1,C2,C3,C4,G1,G2,G3,G4,device
分组与设备的关系
一个设备可以属于多个分组。 客厅主灯既属于"客厅"分组,也属于"灯光"分组。这是多对多关系,不是树形结构。
| 关系 | 说明 | 示例 |
|---|---|---|
| 设备→房间 | 一个设备属于一个房间 | 客厅主灯→客厅 |
| 设备→类型 | 一个设备属于一个类型 | 客厅主灯→灯光 |
| 设备→自定义分组 | 一个设备可属于多个自定义分组 | 客厅主灯→“回家模式设备组” |
批量控制
分组的核心价值是批量控制。选中一个分组,对所有设备执行同一操作。
批量控制的关键问题:部分失败怎么办? 30个灯关了28个,2个因为离线关不了——告诉用户"成功"还是"失败"?
答案:部分成功。 返回结果包含成功数和失败数,失败的设备标红提示用户。不要因为2个失败就回滚28个成功的——那样用户更崩溃。
分组数据持久化
分组数据必须持久化,否则App重启后分组就没了。用鸿蒙的关系型数据库(RDB)存储分组信息,表结构如下:
device_group表:分组ID、分组名称、分组类型(房间/类型/自定义)、图标、排序device_group_member表:分组ID、设备ID(多对多关联表)
代码实战
基础用法:分组数据模型与数据库
用关系型数据库存储分组信息,实现分组的增删改查。
// services/DeviceGroupService.ets
// 设备分组服务 - 分组管理、批量操作、数据持久化
import { relationalStore } from '@kit.ArkData'
// 分组信息
interface DeviceGroup {
groupId: string
name: string
type: 'room' | 'deviceType' | 'custom' // 分组类型
icon?: Resource
sortOrder: number // 排序序号
deviceCount: number // 设备数量(查询时计算)
}
// 分组成员
interface GroupMember {
groupId: string
deviceId: string
}
// 批量控制结果
interface BatchControlResult {
totalCount: number
successCount: number
failedDevices: string[] // 控制失败的设备ID
}
class DeviceGroupService {
private rdbStore?: relationalStore.RdbStore
private readonly DB_NAME = 'smart_home.db'
private readonly GROUP_TABLE = 'device_group'
private readonly MEMBER_TABLE = 'device_group_member'
// 初始化数据库
async init(context: Context): Promise<void> {
const config: relationalStore.StoreConfig = {
name: this.DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1
}
this.rdbStore = await relationalStore.getRdbStore(context, config)
// 创建分组表
const createGroupTable = `
CREATE TABLE IF NOT EXISTS ${this.GROUP_TABLE} (
group_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at INTEGER,
updated_at INTEGER
)
`
await this.rdbStore.executeSql(createGroupTable)
// 创建分组成员表(多对多关联)
const createMemberTable = `
CREATE TABLE IF NOT EXISTS ${this.MEMBER_TABLE} (
group_id TEXT NOT NULL,
device_id TEXT NOT NULL,
PRIMARY KEY (group_id, device_id)
)
`
await this.rdbStore.executeSql(createMemberTable)
}
// 创建分组
async createGroup(group: DeviceGroup): Promise<boolean> {
if (!this.rdbStore) return false
try {
const valueBucket: relationalStore.ValuesBucket = {
'group_id': group.groupId,
'name': group.name,
'type': group.type,
'sort_order': group.sortOrder,
'created_at': Date.now(),
'updated_at': Date.now()
}
await this.rdbStore.insert(this.GROUP_TABLE, valueBucket)
return true
} catch (err) {
console.error(`创建分组失败: ${JSON.stringify(err)}`)
return false
}
}
// 添加设备到分组
async addDeviceToGroup(groupId: string, deviceId: string): Promise<boolean> {
if (!this.rdbStore) return false
try {
const valueBucket: relationalStore.ValuesBucket = {
'group_id': groupId,
'device_id': deviceId
}
await this.rdbStore.insert(this.MEMBER_TABLE, valueBucket)
return true
} catch (err) {
console.error(`添加设备到分组失败: ${JSON.stringify(err)}`)
return false
}
}
// 从分组移除设备
async removeDeviceFromGroup(groupId: string, deviceId: string): Promise<boolean> {
if (!this.rdbStore) return false
try {
const predicates = new relationalStore.RdbPredicates(this.MEMBER_TABLE)
predicates.equalTo('group_id', groupId).and().equalTo('device_id', deviceId)
await this.rdbStore.delete(predicates)
return true
} catch (err) {
console.error(`移除设备失败: ${JSON.stringify(err)}`)
return false
}
}
// 获取所有分组
async getAllGroups(): Promise<DeviceGroup[]> {
if (!this.rdbStore) return []
try {
const predicates = new relationalStore.RdbPredicates(this.GROUP_TABLE)
predicates.orderByAsc('sort_order')
const resultSet = await this.rdbStore.query(predicates, ['group_id', 'name', 'type', 'sort_order'])
const groups: DeviceGroup[] = []
while (resultSet.goToNextRow()) {
const groupId = resultSet.getString(resultSet.getColumnIndex('group_id'))
const name = resultSet.getString(resultSet.getColumnIndex('name'))
const type = resultSet.getString(resultSet.getColumnIndex('type')) as 'room' | 'deviceType' | 'custom'
const sortOrder = resultSet.getLong(resultSet.getColumnIndex('sort_order'))
// 查询分组内的设备数量
const countPredicates = new relationalStore.RdbPredicates(this.MEMBER_TABLE)
countPredicates.equalTo('group_id', groupId)
const count = await this.rdbStore.query(countPredicates, ['COUNT(*)'])
let deviceCount = 0
if (count.goToNextRow()) {
deviceCount = count.getLong(0)
}
count.close()
groups.push({ groupId, name, type, sortOrder, deviceCount })
}
resultSet.close()
return groups
} catch (err) {
console.error(`查询分组失败: ${JSON.stringify(err)}`)
return []
}
}
// 获取分组内的设备ID列表
async getGroupDeviceIds(groupId: string): Promise<string[]> {
if (!this.rdbStore) return []
try {
const predicates = new relationalStore.RdbPredicates(this.MEMBER_TABLE)
predicates.equalTo('group_id', groupId)
const resultSet = await this.rdbStore.query(predicates, ['device_id'])
const deviceIds: string[] = []
while (resultSet.goToNextRow()) {
deviceIds.push(resultSet.getString(resultSet.getColumnIndex('device_id')))
}
resultSet.close()
return deviceIds
} catch (err) {
console.error(`查询分组设备失败: ${JSON.stringify(err)}`)
return []
}
}
// 删除分组
async deleteGroup(groupId: string): Promise<boolean> {
if (!this.rdbStore) return false
try {
// 先删除分组成员
const memberPredicates = new relationalStore.RdbPredicates(this.MEMBER_TABLE)
memberPredicates.equalTo('group_id', groupId)
await this.rdbStore.delete(memberPredicates)
// 再删除分组
const groupPredicates = new relationalStore.RdbPredicates(this.GROUP_TABLE)
groupPredicates.equalTo('group_id', groupId)
await this.rdbStore.delete(groupPredicates)
return true
} catch (err) {
console.error(`删除分组失败: ${JSON.stringify(err)}`)
return false
}
}
// 批量控制分组内所有设备
async batchControlGroup(
groupId: string,
property: string,
value: Object,
executor: (deviceId: string, property: string, value: Object) => Promise<boolean>
): Promise<BatchControlResult> {
const deviceIds = await this.getGroupDeviceIds(groupId)
let successCount = 0
const failedDevices: string[] = []
// 并发控制:最多同时5个请求
const batchSize = 5
for (let i = 0; i < deviceIds.length; i += batchSize) {
const batch = deviceIds.slice(i, i + batchSize)
const results = await Promise.allSettled(
batch.map(deviceId => executor(deviceId, property, value))
)
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value) {
successCount++
} else {
failedDevices.push(batch[index])
}
})
}
return {
totalCount: deviceIds.length,
successCount,
failedDevices
}
}
}
export default new DeviceGroupService()
export type { DeviceGroup, BatchControlResult }
进阶用法:分组管理UI
分组列表展示、设备分配、拖拽排序。
// components/GroupManager.ets
// 分组管理组件 - 分组列表、设备分配、批量操作
import DeviceGroupService, { DeviceGroup, BatchControlResult } from '../services/DeviceGroupService'
@Component
export struct GroupManager {
@State groups: DeviceGroup[] = []
@State selectedGroupId: string = ''
@State batchResult: BatchControlResult | null = null
@State isBatching: boolean = false
// 设备控制回调
onControlDevice?: (deviceId: string, property: string, value: Object) => Promise<boolean>
aboutToAppear() {
this.loadGroups()
}
async loadGroups() {
this.groups = await DeviceGroupService.getAllGroups()
}
// 批量关灯
async batchTurnOff(groupId: string) {
this.isBatching = true
this.selectedGroupId = groupId
this.batchResult = await DeviceGroupService.batchControlGroup(
groupId,
'power',
false,
this.onControlDevice || (async () => true)
)
this.isBatching = false
}
// 批量开灯
async batchTurnOn(groupId: string) {
this.isBatching = true
this.selectedGroupId = groupId
this.batchResult = await DeviceGroupService.batchControlGroup(
groupId,
'power',
true,
this.onControlDevice || (async () => true)
)
this.isBatching = false
}
build() {
Column() {
// 分组类型Tab
Tabs({ index: 0 }) {
TabContent() {
this.GroupList('room')
}.tabBar('按房间')
TabContent() {
this.GroupList('deviceType')
}.tabBar('按类型')
TabContent() {
this.GroupList('custom')
}.tabBar('自定义')
}
.width('100%')
.layoutWeight(1)
.barMode(BarMode.Fixed)
// 批量操作结果
if (this.batchResult) {
this.BatchResultBar()
}
}
.width('100%')
.height('100%')
}
// 分组列表
@Builder GroupList(type: string) {
List({ space: 8 }) {
ForEach(
this.groups.filter(g => g.type === type),
(group: DeviceGroup) => {
ListItem() {
Row() {
// 分组图标
Column() {
Text(group.name.charAt(0))
.fontSize(20)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(44)
.height(44)
.borderRadius(12)
.backgroundColor(this.getGroupColor(group.type))
.justifyContent(FlexAlign.Center)
// 分组名称和设备数
Column() {
Text(group.name)
.fontSize(15)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
Text(`${group.deviceCount}个设备`)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
// 批量操作按钮
Row() {
// 全开
Text('全开')
.fontSize(12)
.fontColor('#007DFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#F0F7FF')
.borderRadius(12)
.onClick(() => this.batchTurnOn(group.groupId))
// 全关
Text('全关')
.fontSize(12)
.fontColor('#FF5722')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#FFF3E0')
.borderRadius(12)
.margin({ left: 8 })
.onClick(() => this.batchTurnOff(group.groupId))
}
// 箭头
Image($r('sys.media.ohos_ic_public_arrow_right'))
.width(16).height(16).fillColor('#CCCCCC')
.margin({ left: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.onClick(() => {
this.selectedGroupId = group.groupId
// 跳转到分组详情页
})
}
}
}
// 添加分组按钮
ListItem() {
Row() {
Text('+').fontSize(20).fontColor('#007DFF')
Text('添加分组').fontSize(14).fontColor('#007DFF').margin({ left: 8 })
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Dashed })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 8 })
}
// 批量操作结果
@Builder BatchResultBar() {
Row() {
if (this.batchResult!.failedDevices.length === 0) {
Image($r('sys.media.ohos_ic_public_ok'))
.width(16).height(16).fillColor('#4CAF50')
Text(`全部成功(${this.batchResult!.successCount}个设备)`)
.fontSize(13).fontColor('#4CAF50').margin({ left: 8 })
} else {
Image($r('sys.media.ohos_ic_public_fail'))
.width(16).height(16).fillColor('#FF9800')
Text(`成功${this.batchResult!.successCount}个,失败${this.batchResult!.failedDevices.length}个`)
.fontSize(13).fontColor('#FF9800').margin({ left: 8 })
}
}
.width('100%')
.padding(12)
.backgroundColor(this.batchResult!.failedDevices.length === 0 ? '#E8F5E9' : '#FFF3E0')
.padding({ left: 16, right: 16 })
}
// 分组颜色
private getGroupColor(type: string): string {
const colors: Record<string, string> = {
'room': '#007DFF',
'deviceType': '#4CAF50',
'custom': '#FF9800'
}
return colors[type] || '#999999'
}
}
完整示例:设备分组主页面
整合分组服务、分组管理组件,实现完整的设备分组管理体验。
// pages/DeviceGroupPage.ets
// 设备分组主页面
import DeviceGroupService, { DeviceGroup } from '../services/DeviceGroupService'
import DeviceControlManager from '../services/DeviceControlManager'
import { GroupManager } from '../components/GroupManager'
import { promptAction } from '@kit.ArkUI'
@Entry
@Component
struct DeviceGroupPage {
@State groups: DeviceGroup[] = []
@State showCreateDialog: boolean = false
@State newGroupName: string = ''
@State newGroupType: string = 'room'
aboutToAppear() {
this.initGroups()
}
// 初始化默认分组
async initGroups() {
await DeviceGroupService.init(getContext(this))
const existingGroups = await DeviceGroupService.getAllGroups()
if (existingGroups.length === 0) {
// 首次使用,创建默认房间分组
const defaultRooms: DeviceGroup[] = [
{ groupId: 'room-living', name: '客厅', type: 'room', sortOrder: 0, deviceCount: 0 },
{ groupId: 'room-bedroom', name: '卧室', type: 'room', sortOrder: 1, deviceCount: 0 },
{ groupId: 'room-kitchen', name: '厨房', type: 'room', sortOrder: 2, deviceCount: 0 },
{ groupId: 'room-bathroom', name: '卫生间', type: 'room', sortOrder: 3, deviceCount: 0 }
]
// 创建默认类型分组
const defaultTypes: DeviceGroup[] = [
{ groupId: 'type-light', name: '灯光', type: 'deviceType', sortOrder: 0, deviceCount: 0 },
{ groupId: 'type-ac', name: '空调', type: 'deviceType', sortOrder: 1, deviceCount: 0 },
{ groupId: 'type-curtain', name: '窗帘', type: 'deviceType', sortOrder: 2, deviceCount: 0 },
{ groupId: 'type-sensor', name: '传感器', type: 'deviceType', sortOrder: 3, deviceCount: 0 }
]
for (const group of [...defaultRooms, ...defaultTypes]) {
await DeviceGroupService.createGroup(group)
}
// 为默认分组添加设备(示例)
await DeviceGroupService.addDeviceToGroup('room-living', 'light-001')
await DeviceGroupService.addDeviceToGroup('room-living', 'ac-001')
await DeviceGroupService.addDeviceToGroup('room-living', 'curtain-001')
await DeviceGroupService.addDeviceToGroup('type-light', 'light-001')
await DeviceGroupService.addDeviceToGroup('type-ac', 'ac-001')
}
this.groups = await DeviceGroupService.getAllGroups()
}
// 创建自定义分组
async createCustomGroup() {
if (!this.newGroupName.trim()) {
promptAction.showToast({ message: '请输入分组名称' })
return
}
const group: DeviceGroup = {
groupId: `custom_${Date.now()}`,
name: this.newGroupName,
type: 'custom',
sortOrder: this.groups.filter(g => g.type === 'custom').length,
deviceCount: 0
}
const success = await DeviceGroupService.createGroup(group)
if (success) {
this.showCreateDialog = false
this.newGroupName = ''
this.groups = await DeviceGroupService.getAllGroups()
promptAction.showToast({ message: '分组创建成功' })
}
}
build() {
Navigation() {
Column() {
// 分组管理组件
GroupManager({
onControlDevice: async (deviceId: string, property: string, value: Object) => {
DeviceControlManager.controlDevice(deviceId, property, value)
return true
}
})
}
.width('100%')
.height('100%')
.backgroundColor('#F8F8F8')
}
.title('设备分组')
.titleMode(NavigationTitleMode.Mini)
// 创建分组弹窗
.bindContentCover(this.showCreateDialog, this.CreateGroupDialog(), {
modalTransition: ModalTransition.DEFAULT
})
}
// 创建分组对话框
@Builder CreateGroupDialog() {
Column() {
Text('创建自定义分组')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 24 })
// 分组名称输入
TextInput({ placeholder: '输入分组名称', text: this.newGroupName })
.fontSize(16)
.onChange((value: string) => { this.newGroupName = value })
.margin({ bottom: 16 })
// 按钮
Row() {
Button('取消')
.width('45%')
.height(44)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.borderRadius(22)
.onClick(() => { this.showCreateDialog = false })
Button('创建')
.width('45%')
.height(44)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.borderRadius(22)
.onClick(() => this.createCustomGroup())
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 24, topRight: 24 })
}
}
踩坑与注意事项
1. 分组与设备的多对多关系
一个设备属于"客厅"分组,也属于"灯光"分组。删除"客厅"分组时,设备还在"灯光"分组里。删除设备时,要从所有分组中移除。
解法: 用关联表(device_group_member)管理多对多关系。删除分组时只删关联记录,不删设备。删除设备时,先从所有分组中移除关联,再删设备。
2. 批量控制的并发问题
30个设备同时发控制指令,MQTT可能扛不住(尤其是Broker性能有限的时候)。设备端也可能处理不过来——网关同时收到30条指令,CPU飙到100%,然后卡死。
解法: 批量控制做并发限制,最多同时5个请求。用Promise.allSettled配合分批处理,一批5个完成后再发下一批。虽然慢一点,但不会把设备搞崩。
3. 分组排序的持久化
用户拖拽调整了分组顺序,刷新后又回到原来的顺序——因为排序没持久化。
解法: 分组表加sort_order字段,拖拽排序后更新sort_order值。查询时按sort_order排序。拖拽排序的UI实现可以用List组件的onDragStart和onDrop回调。
4. 分组名称的国际化
“客厅”"卧室"这些分组名称,海外用户看不懂。但分组名称是用户创建的,不是你预置的——你不能替用户翻译。
解法: 预置分组用国际化资源($r('app.string.room_living')),用户自定义分组用用户输入的文本。预置分组的名称显示用资源ID,自定义分组的名称直接显示文本。
5. 空分组的处理
用户创建了一个分组但没添加设备,分组列表里显示"0个设备"。这个分组要不要显示?显示吧,占地方;不显示吧,用户以为分组丢了。
解法: 空分组正常显示,但样式上做区分——设备数为0时,分组名称用灰色,右侧显示"点击添加设备"的提示。不要自动删除空分组,那是用户的数据。
HarmonyOS 6适配说明
HarmonyOS 6对设备分组做了几项改进:
-
关系型数据库性能优化:
relationalStore的批量插入性能提升3倍,初始化大量分组数据更快。新增batchInsertAPI,一次插入多条记录。 -
拖拽排序API增强:
List组件新增onMove回调,支持列表项拖拽重排序,不需要自己计算拖拽位置和交换逻辑。 -
分组智能推荐:HarmonyOS 6的智能家居Kit新增分组推荐API,根据设备类型和位置自动推荐分组方案。新用户首次使用时,系统自动按设备类型创建分组,不用手动创建。
-
批量控制结果通知:新增
@ohos.notification的批量操作结果通知,批量控制完成后,即使App在后台,用户也能看到结果通知。
适配代码示例:
// HarmonyOS 6 拖拽排序
List() {
ForEach(this.groups, (group: DeviceGroup) => {
ListItem() {
// 列表项内容...
}
})
}
.onMove((from: number, to: number) => {
// 拖拽排序回调
const item = this.groups.splice(from, 1)[0]
this.groups.splice(to, 0, item)
// 更新排序字段
this.groups.forEach((g, index) => {
g.sortOrder = index
// 持久化排序
DeviceGroupService.updateSortOrder(g.groupId, index)
})
})
// HarmonyOS 6 批量插入
const valueBuckets: relationalStore.ValuesBucket[] = groups.map(group => ({
'group_id': group.groupId,
'name': group.name,
'type': group.type,
'sort_order': group.sortOrder,
'created_at': Date.now(),
'updated_at': Date.now()
}))
await this.rdbStore.batchInsert(this.GROUP_TABLE, valueBuckets)
总结
设备分组是智能家居App的"收纳术"。没有分组,30个设备平铺在列表里,找设备靠眼力;有了分组,按房间、按类型、按自定义标签,一目了然,批量操作一键搞定。
分组的核心是多对多关系和批量控制。多对多关系用关联表管理,批量控制做并发限制和部分失败处理。数据持久化用关系型数据库,分组排序用sort_order字段。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ 分组本身不难,但多对多关系、批量控制、拖拽排序组合起来有一定复杂度 |
| 使用频率 | ⭐⭐⭐⭐ 设备多了必须分组,否则用户体验极差 |
| 重要程度 | ⭐⭐⭐⭐ 分组是智能家居App的基础设施,没有分组就没有批量操作 |
一句话:设备不多的时候分组是锦上添花,设备多了之后分组是雪中送炭。 别等用户骂了才加。
- 点赞
- 收藏
- 关注作者
评论(0)