mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
refactor(ohos): DevicePage 560→307 行(入口列表/四个面板/模型拆分)
- common/DeviceModel.ets(73):device_id 兜底(桥 id 优先 → 持久化 → 生成并落盘, 顺序与原 aboutToAppear 一致)、/device/online 响应解析、四个二级页路由 id - components/DeviceRootEntries.ets(92):一级入口(本机/通道两组 NavRow 列表); 宽屏高亮自己读 AppStorage 的 isWideScreen,页面只给 activeSub 与 onOpen - components/DevicePanes.ets(316):DeviceLocalPane(基本信息 + 授权开关)、 DeviceCapsPane(能力清单)、DeviceGatewayPane(网关信息 + 刷新)、 DeviceListPane(在线设备列表)、DeviceKvRow;每个面板自带 SubPageLayer 页面保留导航栈、openSub/closeSub、授权开关、toast、refreshDevices 与 SubDestination 分发。所有文案、图标、颜色、过渡与失败分支逐字保留; 面板内容多了一层无 padding 的 Column 根节点(@Component 的 build() 只允许 一个根),宽度 100% + Start 对齐,与 SubPageLayer 内容槽的布局一致。 验证:hvigorw assembleHap BUILD SUCCESSFUL。
This commit is contained in:
73
cmd/ohos/HomeAgent/entry/src/main/ets/common/DeviceModel.ets
Normal file
73
cmd/ohos/HomeAgent/entry/src/main/ets/common/DeviceModel.ets
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 设备页的纯逻辑:本机 device_id 兜底与在线设备列表解析。
|
||||||
|
*
|
||||||
|
* 从 pages/DevicePage.ets 抽出(非 UI,可被其它页面/桥复用)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DeviceInfo } from '../model/Model';
|
||||||
|
import { connStore } from './ConnStore';
|
||||||
|
|
||||||
|
// ===== 二级页面路由 id(页面与一级入口列表共用)=====
|
||||||
|
export const SUB_NONE: string = '';
|
||||||
|
export const SUB_LOCAL: string = 'local';
|
||||||
|
export const SUB_CAPS: string = 'caps';
|
||||||
|
export const SUB_GATEWAY: string = 'gateway';
|
||||||
|
export const SUB_LIST: string = 'list';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本机 device_id:桥里已有就用桥的,其次读持久化,都没有则生成一个并落盘。
|
||||||
|
* 生成后必须持久化,否则每次冷启动换 id,网关侧会累积成一堆幽灵设备。
|
||||||
|
*
|
||||||
|
* 判定顺序与原 DevicePage.aboutToAppear 一致:桥的 id 优先于持久化的 id。
|
||||||
|
*/
|
||||||
|
export function resolveDeviceId(bridgeId: string): string {
|
||||||
|
let id: string = bridgeId;
|
||||||
|
if (id.length === 0) {
|
||||||
|
id = connStore.getDeviceId();
|
||||||
|
}
|
||||||
|
if (id.length === 0) {
|
||||||
|
id = 'ohos-' + Date.now().toString(36);
|
||||||
|
try {
|
||||||
|
connStore.saveDeviceId(id);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore persist failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 /device/online 响应体。
|
||||||
|
*
|
||||||
|
* apiClient 已自动前置 /api/v1,调用方只写其后的部分
|
||||||
|
* (否则会拼成 /api/v1/api/v1/device/online 并 404)。
|
||||||
|
*/
|
||||||
|
export function parseOnlineDevices(parsed: Record<string, Object>): DeviceInfo[] {
|
||||||
|
const devs: Object = parsed['devices'];
|
||||||
|
const list: DeviceInfo[] = [];
|
||||||
|
if (devs === undefined || devs === null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
const arr: Object[] = devs as Object[];
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const d: Record<string, Object> = arr[i] as Record<string, Object>;
|
||||||
|
const capsArr: Object = d['caps'];
|
||||||
|
const caps: string[] = [];
|
||||||
|
if (capsArr !== undefined && capsArr !== null) {
|
||||||
|
const cArr: Object[] = capsArr as Object[];
|
||||||
|
for (let j = 0; j < cArr.length; j++) {
|
||||||
|
caps.push(cArr[j] as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const info: DeviceInfo = {
|
||||||
|
deviceId: d['device_id'] as string ?? '',
|
||||||
|
name: d['name'] as string ?? '',
|
||||||
|
kind: d['kind'] as string ?? '',
|
||||||
|
online: true,
|
||||||
|
authorized: d['authorized'] as boolean ?? false,
|
||||||
|
caps: caps,
|
||||||
|
};
|
||||||
|
list.push(info);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
316
cmd/ohos/HomeAgent/entry/src/main/ets/components/DevicePanes.ets
Normal file
316
cmd/ohos/HomeAgent/entry/src/main/ets/components/DevicePanes.ets
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
/**
|
||||||
|
* 设备页的四个二级页面内容:本机设备 / 设备能力 / 设备通道 / 接入的设备。
|
||||||
|
*
|
||||||
|
* 从 pages/DevicePage.ets 抽出(原来是 LocalDeviceContent / CapsContent /
|
||||||
|
* GatewayContent / OnlineDevicesContent 四个 @Builder + KvRow)。
|
||||||
|
* 每个面板自带 SubPageLayer 外壳(标题、所属 Tab、返回、刷新),
|
||||||
|
* 页面只保留路由分发 —— 与 SettingsPage 拆出的三个 Pane 同一套做法。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DeviceInfo } from '../model/Model';
|
||||||
|
import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, ANIM_FAST, ANIM_NORMAL,
|
||||||
|
ANIM_ENTER } from '../common/Constants';
|
||||||
|
import { LOCAL_DEVICE_CAPS } from '../common/DeviceBridgeSession';
|
||||||
|
import { MotionBase } from './MotionBase';
|
||||||
|
import { PlainCard, SubPageLayer } from './SubPage';
|
||||||
|
|
||||||
|
/** 通用 KV 行(面板之间共用) */
|
||||||
|
@Component
|
||||||
|
export struct DeviceKvRow {
|
||||||
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
|
@Prop label: string = '';
|
||||||
|
@Prop value: string = '';
|
||||||
|
|
||||||
|
private palette(): ThemePalette {
|
||||||
|
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Row() {
|
||||||
|
Text(this.label)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor(this.palette().textSecondary)
|
||||||
|
Blank()
|
||||||
|
Text(this.value)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor(this.palette().textPrimary)
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ top: 8, bottom: 8 })
|
||||||
|
.border({ width: { bottom: 1 }, color: this.palette().kvBorder })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 二级:本机设备(基本信息 + 远程控制授权) */
|
||||||
|
@Component
|
||||||
|
export struct DeviceLocalPane {
|
||||||
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
|
@Prop deviceId: string = '';
|
||||||
|
@Prop authorized: boolean = false;
|
||||||
|
onBack?: () => void;
|
||||||
|
onToggleAuth?: (on: boolean) => void;
|
||||||
|
|
||||||
|
private palette(): ThemePalette {
|
||||||
|
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
SubPageLayer({
|
||||||
|
title: '本机设备',
|
||||||
|
tab: 2,
|
||||||
|
onBack: () => {
|
||||||
|
const cb: (() => void) | undefined = this.onBack;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
PlainCard({ caption: '基本信息' }) {
|
||||||
|
DeviceKvRow({ label: '设备 ID', value: this.deviceId.length > 0 ? this.deviceId : '未注册' })
|
||||||
|
DeviceKvRow({ label: '名称', value: 'HomeAgent OHOS' })
|
||||||
|
DeviceKvRow({ label: '类型', value: 'phone' })
|
||||||
|
}
|
||||||
|
|
||||||
|
PlainCard({ caption: '权限控制' }) {
|
||||||
|
Row() {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('允许 agent 控制本机')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor(this.palette().textPrimary)
|
||||||
|
Text('授权后 agent 可调用下方能力;截屏仅捕获本应用画面,剪贴板读取需系统弹窗确认。')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(this.palette().textMuted)
|
||||||
|
.margin({ top: 4 })
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
|
||||||
|
Toggle({ type: ToggleType.Switch, isOn: this.authorized })
|
||||||
|
.selectedColor(this.palette().accent)
|
||||||
|
.onChange((on: boolean) => {
|
||||||
|
const cb: ((on: boolean) => void) | undefined = this.onToggleAuth;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb(on);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
|
||||||
|
Row() {
|
||||||
|
Circle({ width: 8, height: 8 })
|
||||||
|
.fill(this.authorized ? '#17A964' : '#E84026')
|
||||||
|
.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })
|
||||||
|
.margin({ right: 8 })
|
||||||
|
Text(this.authorized ? '已授权 — agent 可远程调用能力' : '未授权 — agent 将拒绝远程命令')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(this.authorized ? '#17A964' : this.palette().textMuted)
|
||||||
|
.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.margin({ top: 12 })
|
||||||
|
.padding({ left: 4 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 二级:设备能力(本机能被 agent 调用的能力清单) */
|
||||||
|
@Component
|
||||||
|
export struct DeviceCapsPane {
|
||||||
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
|
onBack?: () => void;
|
||||||
|
|
||||||
|
private palette(): ThemePalette {
|
||||||
|
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
SubPageLayer({
|
||||||
|
title: '设备能力',
|
||||||
|
tab: 2,
|
||||||
|
onBack: () => {
|
||||||
|
const cb: (() => void) | undefined = this.onBack;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
PlainCard({ caption: '能力清单' }) {
|
||||||
|
Text('agent 通过设备桥可调用的本机能力:')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(this.palette().textMuted)
|
||||||
|
.margin({ bottom: 10 })
|
||||||
|
|
||||||
|
Flex({ wrap: FlexWrap.Wrap }) {
|
||||||
|
ForEach(LOCAL_DEVICE_CAPS, (cap: string) => {
|
||||||
|
Text(cap)
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(this.palette().accent)
|
||||||
|
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||||
|
.borderRadius(999)
|
||||||
|
.backgroundColor(this.palette().bgHover)
|
||||||
|
.border({ width: 1, color: this.palette().glassBorder })
|
||||||
|
.margin({ right: 6, bottom: 6 })
|
||||||
|
}, (cap: string) => cap)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 二级:设备通道(网关地址 / Token / 连接状态) */
|
||||||
|
@Component
|
||||||
|
export struct DeviceGatewayPane {
|
||||||
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
|
@Prop bridgeUrl: string = '';
|
||||||
|
@Prop bridgeToken: string = '';
|
||||||
|
@Prop bridgeConnected: boolean = false;
|
||||||
|
onBack?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
|
||||||
|
private palette(): ThemePalette {
|
||||||
|
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
SubPageLayer({
|
||||||
|
title: '设备通道',
|
||||||
|
tab: 2,
|
||||||
|
onBack: () => {
|
||||||
|
const cb: (() => void) | undefined = this.onBack;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
PlainCard({ caption: '连接信息' }) {
|
||||||
|
DeviceKvRow({ label: '网关地址', value: this.bridgeUrl.length > 0 ? this.bridgeUrl : '-' })
|
||||||
|
DeviceKvRow({ label: 'Token', value: this.bridgeToken.length > 0 ? '已从连接继承' : '未配置' })
|
||||||
|
DeviceKvRow({ label: '状态', value: this.bridgeConnected ? '已连接' : '未连接' })
|
||||||
|
}
|
||||||
|
|
||||||
|
PlainCard({ caption: '操作' }) {
|
||||||
|
Row() {
|
||||||
|
Blank()
|
||||||
|
MotionBase({ pressEnabled: true, fillWidth: false }) {
|
||||||
|
Button('刷新设备')
|
||||||
|
.height(34)
|
||||||
|
.fontSize(12)
|
||||||
|
.backgroundColor(Color.Transparent)
|
||||||
|
.border({ width: 1, color: this.palette().btnGhostBorder })
|
||||||
|
.fontColor(this.palette().textSecondary)
|
||||||
|
.onClick(() => {
|
||||||
|
const cb: (() => void) | undefined = this.onRefresh;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Text('设备通道由应用前台生命周期统一管理;切换连接配置后会自动使用新地址和 Token。')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(this.palette().textMuted)
|
||||||
|
.margin({ top: 10 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 二级:接入的设备(网关侧在线设备列表) */
|
||||||
|
@Component
|
||||||
|
export struct DeviceListPane {
|
||||||
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
|
@Prop devices: DeviceInfo[] = [];
|
||||||
|
onBack?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
|
||||||
|
private palette(): ThemePalette {
|
||||||
|
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
SubPageLayer({
|
||||||
|
title: '接入的设备',
|
||||||
|
tab: 2,
|
||||||
|
showRefresh: true,
|
||||||
|
onBack: () => {
|
||||||
|
const cb: (() => void) | undefined = this.onBack;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRefresh: () => {
|
||||||
|
const cb: (() => void) | undefined = this.onRefresh;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
DeviceOnlineList({ devices: this.devices })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在线设备列表本体(抽出来只是为了让 DeviceListPane 的 build 更短) */
|
||||||
|
@Component
|
||||||
|
struct DeviceOnlineList {
|
||||||
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
|
@Prop devices: DeviceInfo[] = [];
|
||||||
|
|
||||||
|
private palette(): ThemePalette {
|
||||||
|
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
if (this.devices.length === 0) {
|
||||||
|
Text('暂无其他设备。电脑 GUI 或 CLI 连接同一网关后会出现在这里。')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(this.palette().textMuted)
|
||||||
|
.padding({ left: 4 })
|
||||||
|
.transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(this.devices, (dev: DeviceInfo) => {
|
||||||
|
// PlainCard 是自定义组件,transition 不能直接挂在它上面(会生成 __Common__ 包装),
|
||||||
|
// 所以用一个无 padding、满宽的 Column 承载入场动画,布局不受影响。
|
||||||
|
Column() {
|
||||||
|
PlainCard({ caption: '' }) {
|
||||||
|
Row() {
|
||||||
|
Circle({ width: 8, height: 8 })
|
||||||
|
.fill(dev.online ? '#17A964' : '#77809A')
|
||||||
|
.margin({ right: 10 })
|
||||||
|
Column() {
|
||||||
|
Text(dev.name.length > 0 ? dev.name : dev.deviceId)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor(this.palette().textPrimary)
|
||||||
|
Text(dev.kind + (dev.authorized ? ' · 已授权' : ' · 未授权'))
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(dev.authorized ? '#17A964' : this.palette().textMuted)
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
|
||||||
|
Text(dev.caps.length.toString() + ' 能力')
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(this.palette().textMuted)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.transition(TransitionEffect.OPACITY
|
||||||
|
.combine(TransitionEffect.translate({ y: 12 }))
|
||||||
|
.animation({ duration: ANIM_ENTER, curve: Curve.EaseOut }))
|
||||||
|
}, (dev: DeviceInfo) => dev.deviceId + dev.online.toString())
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 设备页一级入口列表(本机 / 通道两组)。
|
||||||
|
*
|
||||||
|
* 从 pages/DevicePage.ets 抽出(原来是 RootEntries 一个 @Builder)。
|
||||||
|
* 宽屏高亮由组件自己读 AppStorage 的 isWideScreen 决定,
|
||||||
|
* 页面只负责给 activeSub 与四个打开动作。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LOCAL_DEVICE_CAPS } from '../common/DeviceBridgeSession';
|
||||||
|
import { SUB_CAPS, SUB_GATEWAY, SUB_LIST, SUB_LOCAL } from '../common/DeviceModel';
|
||||||
|
import { NavGroup, NavRow } from './SubPage';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct DeviceRootEntries {
|
||||||
|
/** 宽屏:左边一级界面(含底部导航栏),右边二级界面 */
|
||||||
|
@StorageProp('isWideScreen') private isWide: boolean = false;
|
||||||
|
/** 当前右栏展示的二级页面 id,用于宽屏下高亮左侧入口行 */
|
||||||
|
@Prop activeSub: string = '';
|
||||||
|
@Prop deviceId: string = '';
|
||||||
|
@Prop bridgeConnected: boolean = false;
|
||||||
|
@Prop bridgeUrl: string = '';
|
||||||
|
@Prop loadingDevices: boolean = false;
|
||||||
|
@Prop deviceCount: number = 0;
|
||||||
|
onOpen?: (id: string) => void;
|
||||||
|
|
||||||
|
private capsCount(): number {
|
||||||
|
return LOCAL_DEVICE_CAPS.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private open(id: string): void {
|
||||||
|
const cb: ((id: string) => void) | undefined = this.onOpen;
|
||||||
|
if (cb !== undefined) {
|
||||||
|
cb(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
// 一级入口列表整体作为一个容器根节点:@Component 的 build() 只允许一个根,
|
||||||
|
// 页面侧仍是 `.padding(...)` 的 Column,逐项布局与拆分前一致。
|
||||||
|
Column() {
|
||||||
|
NavGroup({ caption: '本机' }) {
|
||||||
|
NavRow({
|
||||||
|
icon: $r('app.media.ic_phone'),
|
||||||
|
title: '本机设备',
|
||||||
|
subtitle: this.deviceId.length > 0 ? this.deviceId : '未注册',
|
||||||
|
value: this.bridgeConnected ? '在线' : '离线',
|
||||||
|
selected: this.isWide && this.activeSub === SUB_LOCAL,
|
||||||
|
onTap: () => {
|
||||||
|
this.open(SUB_LOCAL);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
NavRow({
|
||||||
|
icon: $r('app.media.ic_bolt'),
|
||||||
|
title: '设备能力',
|
||||||
|
subtitle: this.capsCount().toString() + ' 项能力',
|
||||||
|
value: '',
|
||||||
|
showDivider: false,
|
||||||
|
selected: this.isWide && this.activeSub === SUB_CAPS,
|
||||||
|
onTap: () => {
|
||||||
|
this.open(SUB_CAPS);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
NavGroup({ caption: '通道' }) {
|
||||||
|
NavRow({
|
||||||
|
icon: $r('app.media.ic_gateway'),
|
||||||
|
title: '设备通道',
|
||||||
|
subtitle: this.bridgeUrl.length > 0 ? '网关已配置' : '未配置',
|
||||||
|
value: this.bridgeConnected ? '已连接' : '未连接',
|
||||||
|
selected: this.isWide && this.activeSub === SUB_GATEWAY,
|
||||||
|
onTap: () => {
|
||||||
|
this.open(SUB_GATEWAY);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
NavRow({
|
||||||
|
icon: $r('app.media.ic_devices_multi'),
|
||||||
|
title: '接入的设备',
|
||||||
|
subtitle: this.loadingDevices ? '加载中...' : '当前在线',
|
||||||
|
value: this.deviceCount.toString() + ' 台',
|
||||||
|
showDivider: false,
|
||||||
|
selected: this.isWide && this.activeSub === SUB_LIST,
|
||||||
|
onTap: () => {
|
||||||
|
this.open(SUB_LIST);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,19 +4,25 @@ import { apiClient } from '../common/ApiClient';
|
|||||||
import { handleNavOnScroll } from '../common/NavBarController';
|
import { handleNavOnScroll } from '../common/NavBarController';
|
||||||
import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry';
|
import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry';
|
||||||
import { DeviceInfo } from '../model/Model';
|
import { DeviceInfo } from '../model/Model';
|
||||||
import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT, ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants';
|
import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH,
|
||||||
import { MotionBase } from '../components/MotionBase';
|
WIDE_MIN_CONTENT, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants';
|
||||||
|
import { deviceGatewayUrl } from '../common/DeviceBridgeSession';
|
||||||
|
import { DeviceRootEntries } from '../components/DeviceRootEntries';
|
||||||
|
import { DeviceLocalPane, DeviceCapsPane, DeviceGatewayPane, DeviceListPane } from '../components/DevicePanes';
|
||||||
import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from '../components/PageTopBar';
|
import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from '../components/PageTopBar';
|
||||||
import { SubPageLayer, NavGroup, NavRow, PlainCard, markSubPageOpen, subPageParam } from '../components/SubPage';
|
import { markSubPageOpen, subPageParam } from '../components/SubPage';
|
||||||
import { LOCAL_DEVICE_CAPS, deviceGatewayUrl } from '../common/DeviceBridgeSession';
|
import { parseOnlineDevices, resolveDeviceId, SUB_CAPS, SUB_GATEWAY, SUB_LIST,
|
||||||
|
SUB_LOCAL, SUB_NONE } from '../common/DeviceModel';
|
||||||
/** 二级页面标识 */
|
|
||||||
const SUB_NONE: string = '';
|
|
||||||
const SUB_LOCAL: string = 'local';
|
|
||||||
const SUB_CAPS: string = 'caps';
|
|
||||||
const SUB_GATEWAY: string = 'gateway';
|
|
||||||
const SUB_LIST: string = 'list';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备页:只做"页面壳"。
|
||||||
|
*
|
||||||
|
* 拆分后的分工(拆分前这里是 560 行的单文件):
|
||||||
|
* - 一级入口列表(本机/通道两组) → components/DeviceRootEntries.ets
|
||||||
|
* - 四个二级页面内容(本机/能力/通道/列表)→ components/DevicePanes.ets
|
||||||
|
* - device_id 兜底与在线设备解析 → common/DeviceModel.ets(路由 id 也在那)
|
||||||
|
* 本文件保留:导航栈与二级页分发、授权开关、toast、设备列表拉取与生命周期。
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
export struct DevicePage {
|
export struct DevicePage {
|
||||||
@StorageProp('themeIsDark') private isDark: boolean = true;
|
@StorageProp('themeIsDark') private isDark: boolean = true;
|
||||||
@ -39,18 +45,8 @@ export struct DevicePage {
|
|||||||
private navStack: NavPathStack = new NavPathStack();
|
private navStack: NavPathStack = new NavPathStack();
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
this.deviceId = deviceBridge.getDeviceId();
|
// id 判定顺序:桥里的 id 优先于持久化的 id,都没有才生成并落盘
|
||||||
if (this.deviceId.length === 0) {
|
this.deviceId = resolveDeviceId(deviceBridge.getDeviceId());
|
||||||
this.deviceId = connStore.getDeviceId();
|
|
||||||
}
|
|
||||||
if (this.deviceId.length === 0) {
|
|
||||||
this.deviceId = 'ohos-' + Date.now().toString(36);
|
|
||||||
try {
|
|
||||||
connStore.saveDeviceId(this.deviceId);
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.authorized = connStore.getDeviceAuth();
|
this.authorized = connStore.getDeviceAuth();
|
||||||
// Gateway URL derives from current connection
|
// Gateway URL derives from current connection
|
||||||
const cur = connStore.getCurrentConnection();
|
const cur = connStore.getCurrentConnection();
|
||||||
@ -133,31 +129,7 @@ export struct DevicePage {
|
|||||||
const resp = await apiClient.get('/device/online');
|
const resp = await apiClient.get('/device/online');
|
||||||
if (resp.status >= 200 && resp.status < 300) {
|
if (resp.status >= 200 && resp.status < 300) {
|
||||||
const parsed: Record<string, Object> = JSON.parse(resp.body) as Record<string, Object>;
|
const parsed: Record<string, Object> = JSON.parse(resp.body) as Record<string, Object>;
|
||||||
const devs: Object = parsed['devices'];
|
const list: DeviceInfo[] = parseOnlineDevices(parsed);
|
||||||
const list: DeviceInfo[] = [];
|
|
||||||
if (devs !== undefined && devs !== null) {
|
|
||||||
const arr: Object[] = devs as Object[];
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
const d: Record<string, Object> = arr[i] as Record<string, Object>;
|
|
||||||
const capsArr: Object = d['caps'];
|
|
||||||
const caps: string[] = [];
|
|
||||||
if (capsArr !== undefined && capsArr !== null) {
|
|
||||||
const cArr: Object[] = capsArr as Object[];
|
|
||||||
for (let j = 0; j < cArr.length; j++) {
|
|
||||||
caps.push(cArr[j] as string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const info: DeviceInfo = {
|
|
||||||
deviceId: d['device_id'] as string ?? '',
|
|
||||||
name: d['name'] as string ?? '',
|
|
||||||
kind: d['kind'] as string ?? '',
|
|
||||||
online: true,
|
|
||||||
authorized: d['authorized'] as boolean ?? false,
|
|
||||||
caps: caps,
|
|
||||||
};
|
|
||||||
list.push(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => {
|
this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => {
|
||||||
this.devices = list;
|
this.devices = list;
|
||||||
});
|
});
|
||||||
@ -177,7 +149,17 @@ export struct DevicePage {
|
|||||||
Column() {
|
Column() {
|
||||||
Scroll() {
|
Scroll() {
|
||||||
Column() {
|
Column() {
|
||||||
this.RootEntries()
|
DeviceRootEntries({
|
||||||
|
activeSub: this.activeSub,
|
||||||
|
deviceId: this.deviceId,
|
||||||
|
bridgeConnected: this.bridgeConnected,
|
||||||
|
bridgeUrl: this.bridgeUrl,
|
||||||
|
loadingDevices: this.loadingDevices,
|
||||||
|
deviceCount: this.devices.length,
|
||||||
|
onOpen: (id: string) => {
|
||||||
|
this.openSub(id);
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ left: 16, right: 16, top: 76, bottom: 174 })
|
.padding({ left: 16, right: 16, top: 76, bottom: 174 })
|
||||||
@ -248,61 +230,58 @@ export struct DevicePage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 二级页面路由表 */
|
/**
|
||||||
|
* 二级页面路由表。
|
||||||
|
* 每个面板自带 SubPageLayer 外壳(标题/所属 Tab/返回/刷新),这里只做分发。
|
||||||
|
*/
|
||||||
@Builder
|
@Builder
|
||||||
SubDestination(name: string, param: object) {
|
SubDestination(name: string, param: object) {
|
||||||
NavDestination() {
|
NavDestination() {
|
||||||
if (name === SUB_LOCAL) {
|
if (name === SUB_LOCAL) {
|
||||||
SubPageLayer({
|
DeviceLocalPane({
|
||||||
title: '本机设备',
|
deviceId: this.deviceId,
|
||||||
tab: 2,
|
authorized: this.authorized,
|
||||||
onBack: () => {
|
onBack: () => {
|
||||||
this.closeSub();
|
this.closeSub();
|
||||||
},
|
},
|
||||||
}) {
|
onToggleAuth: (on: boolean) => {
|
||||||
this.LocalDeviceContent()
|
this.toggleAuthorized(on);
|
||||||
}
|
},
|
||||||
|
})
|
||||||
} else if (name === SUB_CAPS) {
|
} else if (name === SUB_CAPS) {
|
||||||
SubPageLayer({
|
DeviceCapsPane({
|
||||||
title: '设备能力',
|
|
||||||
tab: 2,
|
|
||||||
onBack: () => {
|
onBack: () => {
|
||||||
this.closeSub();
|
this.closeSub();
|
||||||
},
|
},
|
||||||
}) {
|
})
|
||||||
this.CapsContent()
|
|
||||||
}
|
|
||||||
} else if (name === SUB_GATEWAY) {
|
} else if (name === SUB_GATEWAY) {
|
||||||
SubPageLayer({
|
DeviceGatewayPane({
|
||||||
title: '设备通道',
|
bridgeUrl: this.bridgeUrl,
|
||||||
tab: 2,
|
bridgeToken: this.bridgeToken,
|
||||||
|
bridgeConnected: this.bridgeConnected,
|
||||||
onBack: () => {
|
onBack: () => {
|
||||||
this.closeSub();
|
this.closeSub();
|
||||||
},
|
},
|
||||||
}) {
|
|
||||||
this.GatewayContent()
|
|
||||||
}
|
|
||||||
} else if (name === SUB_LIST) {
|
|
||||||
SubPageLayer({
|
|
||||||
title: '接入的设备',
|
|
||||||
tab: 2,
|
|
||||||
onBack: () => {
|
|
||||||
this.closeSub();
|
|
||||||
},
|
|
||||||
showRefresh: true,
|
|
||||||
onRefresh: () => {
|
onRefresh: () => {
|
||||||
this.refreshDevices();
|
this.refreshDevices();
|
||||||
},
|
},
|
||||||
}) {
|
})
|
||||||
this.OnlineDevicesContent()
|
} else if (name === SUB_LIST) {
|
||||||
}
|
DeviceListPane({
|
||||||
|
devices: this.devices,
|
||||||
|
onBack: () => {
|
||||||
|
this.closeSub();
|
||||||
|
},
|
||||||
|
onRefresh: () => {
|
||||||
|
this.refreshDevices();
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hideTitleBar(true)
|
.hideTitleBar(true)
|
||||||
.backgroundColor(Color.Transparent)
|
.backgroundColor(Color.Transparent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
Toast() {
|
Toast() {
|
||||||
if (this.toastMsg.length > 0) {
|
if (this.toastMsg.length > 0) {
|
||||||
@ -323,238 +302,6 @@ export struct DevicePage {
|
|||||||
.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }))
|
.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== 一级入口列表 =====================
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
RootEntries() {
|
|
||||||
NavGroup({ caption: '本机' }) {
|
|
||||||
NavRow({
|
|
||||||
icon: $r('app.media.ic_phone'),
|
|
||||||
title: '本机设备',
|
|
||||||
subtitle: this.deviceId.length > 0 ? this.deviceId : '未注册',
|
|
||||||
value: this.bridgeConnected ? '在线' : '离线',
|
|
||||||
selected: this.isWide && this.activeSub === SUB_LOCAL,
|
|
||||||
onTap: () => {
|
|
||||||
this.openSub(SUB_LOCAL);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
NavRow({
|
|
||||||
icon: $r('app.media.ic_bolt'),
|
|
||||||
title: '设备能力',
|
|
||||||
subtitle: this.capsCount().toString() + ' 项能力',
|
|
||||||
value: '',
|
|
||||||
showDivider: false,
|
|
||||||
selected: this.isWide && this.activeSub === SUB_CAPS,
|
|
||||||
onTap: () => {
|
|
||||||
this.openSub(SUB_CAPS);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
NavGroup({ caption: '通道' }) {
|
|
||||||
NavRow({
|
|
||||||
icon: $r('app.media.ic_gateway'),
|
|
||||||
title: '设备通道',
|
|
||||||
subtitle: this.bridgeUrl.length > 0 ? '网关已配置' : '未配置',
|
|
||||||
value: this.bridgeConnected ? '已连接' : '未连接',
|
|
||||||
selected: this.isWide && this.activeSub === SUB_GATEWAY,
|
|
||||||
onTap: () => {
|
|
||||||
this.openSub(SUB_GATEWAY);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
NavRow({
|
|
||||||
icon: $r('app.media.ic_devices_multi'),
|
|
||||||
title: '接入的设备',
|
|
||||||
subtitle: this.loadingDevices ? '加载中...' : '当前在线',
|
|
||||||
value: this.devices.length.toString() + ' 台',
|
|
||||||
showDivider: false,
|
|
||||||
selected: this.isWide && this.activeSub === SUB_LIST,
|
|
||||||
onTap: () => {
|
|
||||||
this.openSub(SUB_LIST);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private capsCount(): number {
|
|
||||||
return LOCAL_DEVICE_CAPS.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 二级:本机设备 =====================
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
LocalDeviceContent() {
|
|
||||||
PlainCard({ caption: '基本信息' }) {
|
|
||||||
this.KvRow('设备 ID', this.deviceId.length > 0 ? this.deviceId : '未注册')
|
|
||||||
this.KvRow('名称', 'HomeAgent OHOS')
|
|
||||||
this.KvRow('类型', 'phone')
|
|
||||||
}
|
|
||||||
|
|
||||||
PlainCard({ caption: '权限控制' }) {
|
|
||||||
Row() {
|
|
||||||
Column({ space: 2 }) {
|
|
||||||
Text('允许 agent 控制本机')
|
|
||||||
.fontSize(14)
|
|
||||||
.fontColor(this.palette().textPrimary)
|
|
||||||
Text('授权后 agent 可调用下方能力;截屏仅捕获本应用画面,剪贴板读取需系统弹窗确认。')
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor(this.palette().textMuted)
|
|
||||||
.margin({ top: 4 })
|
|
||||||
}
|
|
||||||
.layoutWeight(1)
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
|
|
||||||
Toggle({ type: ToggleType.Switch, isOn: this.authorized })
|
|
||||||
.selectedColor(this.palette().accent)
|
|
||||||
.onChange((on: boolean) => {
|
|
||||||
this.toggleAuthorized(on);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.alignItems(VerticalAlign.Center)
|
|
||||||
|
|
||||||
Row() {
|
|
||||||
Circle({ width: 8, height: 8 })
|
|
||||||
.fill(this.authorized ? '#17A964' : '#E84026')
|
|
||||||
.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })
|
|
||||||
.margin({ right: 8 })
|
|
||||||
Text(this.authorized ? '已授权 — agent 可远程调用能力' : '未授权 — agent 将拒绝远程命令')
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor(this.authorized ? '#17A964' : this.palette().textMuted)
|
|
||||||
.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.margin({ top: 12 })
|
|
||||||
.padding({ left: 4 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 二级:设备能力 =====================
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
CapsContent() {
|
|
||||||
PlainCard({ caption: '能力清单' }) {
|
|
||||||
Text('agent 通过设备桥可调用的本机能力:')
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor(this.palette().textMuted)
|
|
||||||
.margin({ bottom: 10 })
|
|
||||||
|
|
||||||
Flex({ wrap: FlexWrap.Wrap }) {
|
|
||||||
ForEach(LOCAL_DEVICE_CAPS, (cap: string) => {
|
|
||||||
Text(cap)
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor(this.palette().accent)
|
|
||||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
|
||||||
.borderRadius(999)
|
|
||||||
.backgroundColor(this.palette().bgHover)
|
|
||||||
.border({ width: 1, color: this.palette().glassBorder })
|
|
||||||
.margin({ right: 6, bottom: 6 })
|
|
||||||
}, (cap: string) => cap)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 二级:设备通道 =====================
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
GatewayContent() {
|
|
||||||
PlainCard({ caption: '连接信息' }) {
|
|
||||||
this.KvRow('网关地址', this.bridgeUrl.length > 0 ? this.bridgeUrl : '-')
|
|
||||||
this.KvRow('Token', this.bridgeToken.length > 0 ? '已从连接继承' : '未配置')
|
|
||||||
this.KvRow('状态', this.bridgeConnected ? '已连接' : '未连接')
|
|
||||||
}
|
|
||||||
|
|
||||||
PlainCard({ caption: '操作' }) {
|
|
||||||
Row() {
|
|
||||||
Blank()
|
|
||||||
MotionBase({ pressEnabled: true, fillWidth: false }) {
|
|
||||||
Button('刷新设备')
|
|
||||||
.height(34)
|
|
||||||
.fontSize(12)
|
|
||||||
.backgroundColor(Color.Transparent)
|
|
||||||
.border({ width: 1, color: this.palette().btnGhostBorder })
|
|
||||||
.fontColor(this.palette().textSecondary)
|
|
||||||
.onClick(() => {
|
|
||||||
this.refreshDevices();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
|
|
||||||
Text('设备通道由应用前台生命周期统一管理;切换连接配置后会自动使用新地址和 Token。')
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor(this.palette().textMuted)
|
|
||||||
.margin({ top: 10 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 二级:接入的设备 =====================
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
OnlineDevicesContent() {
|
|
||||||
if (this.devices.length === 0) {
|
|
||||||
Text('暂无其他设备。电脑 GUI 或 CLI 连接同一网关后会出现在这里。')
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor(this.palette().textMuted)
|
|
||||||
.padding({ left: 4 })
|
|
||||||
.transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(this.devices, (dev: DeviceInfo) => {
|
|
||||||
// PlainCard 是自定义组件,transition 不能直接挂在它上面(会生成 __Common__ 包装),
|
|
||||||
// 所以用一个无 padding、满宽的 Column 承载入场动画,布局不受影响。
|
|
||||||
Column() {
|
|
||||||
PlainCard({ caption: '' }) {
|
|
||||||
Row() {
|
|
||||||
Circle({ width: 8, height: 8 })
|
|
||||||
.fill(dev.online ? '#17A964' : '#77809A')
|
|
||||||
.margin({ right: 10 })
|
|
||||||
Column() {
|
|
||||||
Text(dev.name.length > 0 ? dev.name : dev.deviceId)
|
|
||||||
.fontSize(14)
|
|
||||||
.fontColor(this.palette().textPrimary)
|
|
||||||
Text(dev.kind + (dev.authorized ? ' · 已授权' : ' · 未授权'))
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor(dev.authorized ? '#17A964' : this.palette().textMuted)
|
|
||||||
}
|
|
||||||
.layoutWeight(1)
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
|
|
||||||
Text(dev.caps.length.toString() + ' 能力')
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor(this.palette().textMuted)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.transition(TransitionEffect.OPACITY
|
|
||||||
.combine(TransitionEffect.translate({ y: 12 }))
|
|
||||||
.animation({ duration: ANIM_ENTER, curve: Curve.EaseOut }))
|
|
||||||
}, (dev: DeviceInfo) => dev.deviceId + dev.online.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 通用 KV 行 =====================
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
KvRow(k: string, v: string) {
|
|
||||||
Row() {
|
|
||||||
Text(k)
|
|
||||||
.fontSize(13)
|
|
||||||
.fontColor(this.palette().textSecondary)
|
|
||||||
Blank()
|
|
||||||
Text(v)
|
|
||||||
.fontSize(13)
|
|
||||||
.fontColor(this.palette().textPrimary)
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding({ top: 8, bottom: 8 })
|
|
||||||
.border({ width: { bottom: 1 }, color: this.palette().kvBorder })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RADIUS_MD: number = 10;
|
const RADIUS_MD: number = 10;
|
||||||
|
|||||||
Reference in New Issue
Block a user