From 131c1ff0e940dc1af56682cc307478e82f69f8db Mon Sep 17 00:00:00 2001 From: root Date: Sun, 13 Sep 2026 22:18:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ohos):=20PluginsPage=20995=E2=86=92374?= =?UTF-8?q?=20=E8=A1=8C=EF=BC=88=E6=8E=A5=E5=8F=A3/=E7=8A=B6=E6=80=81/?= =?UTF-8?q?=E5=88=97=E8=A1=A8/=E8=AF=A6=E6=83=85/=E6=B5=AE=E5=B1=82?= =?UTF-8?q?=E6=8B=86=E5=88=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拆分方向按职责切,页面只留导航与数据编排: - common/PluginApi.ets(174):fetchPluginRows / fetchPluginDetail - common/PluginStatus.ets(78):状态判定、文案、颜色、副标题 - components/PluginListView.ets(166):列表卡 + 列表 - components/PluginDetailPane.ets(291):详情面板 - components/PluginsOverlays.ets(65):安装表单 - components/ToastBar.ets(48):提示条,PluginsToast 与 SettingsPage 的 toast 合并成这一个组件(此前两处各写一份) UI 结构与文案逐字保留(含 92%/layoutWeight 等既有布局修法)。 验证:hvigorw assembleHap BUILD SUCCESSFUL。 --- .../entry/src/main/ets/common/PluginApi.ets | 174 +++++ .../src/main/ets/common/PluginStatus.ets | 78 ++ .../main/ets/components/PluginDetailPane.ets | 291 +++++++ .../main/ets/components/PluginListView.ets | 166 ++++ .../main/ets/components/PluginsOverlays.ets | 65 ++ .../src/main/ets/components/ToastBar.ets | 48 ++ .../entry/src/main/ets/pages/PluginsPage.ets | 731 ++---------------- 7 files changed, 877 insertions(+), 676 deletions(-) create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginApi.ets create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginStatus.ets create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginDetailPane.ets create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginListView.ets create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginsOverlays.ets create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/components/ToastBar.ets diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginApi.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginApi.ets new file mode 100644 index 0000000..23a37e2 --- /dev/null +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginApi.ets @@ -0,0 +1,174 @@ +/** + * 插件数据获取(无 UI 依赖)。 + * + * 从 pages/PluginsPage.ets 抽出:三处接口的取数、合并口径与字段解析 + * 都放在这里,页面只负责把结果落到 @State。 + * + * 数据源对齐 WebGUI renderPlugins: + * - GET /kernel → plugins[{name,loaded}](含全部内置插件)+ tools(按 plugin 归属) + * - GET /plugins → 已安装外部插件元数据(version/description 等) + * - GET /plugins/disabled → {disabled:[{name,...}]} + * 三方按名称合并去重排序。 + */ + +import { apiClient } from './ApiClient'; +import { PluginRow, PluginDetail, emptyPluginDetail } from '../model/Model'; + +/** 拉取并合并插件列表;失败时抛错,由调用方转成人话提示。 */ +export async function fetchPluginRows(): Promise { + // ---- kernel: loaded plugins + tool ownership ---- + const loadedMap: Map = new Map(); + const toolsByPlugin: Map = new Map(); + const kResp = await apiClient.getWithTimeout('/kernel', 12000); + const kernelObj: Record = JSON.parse(kResp.body) as Record; + const kpRaw: Object | undefined = kernelObj['plugins']; + if (kpRaw !== undefined && kpRaw !== null) { + const kpArr: Object[] = kpRaw as Object[]; + for (let i = 0; i < kpArr.length; i++) { + const item: Record = kpArr[i] as Record; + const n: string = item['name'] as string ?? ''; + if (n.length === 0) { + continue; + } + loadedMap.set(n, item['loaded'] as boolean ?? true); + } + } + const tRaw: Object | undefined = kernelObj['tools']; + if (tRaw !== undefined && tRaw !== null) { + const tArr: Object[] = tRaw as Object[]; + for (let i = 0; i < tArr.length; i++) { + const item: Record = tArr[i] as Record; + const tn: string = item['name'] as string ?? ''; + const owner: string = item['plugin'] as string ?? ''; + if (tn.length === 0 || owner.length === 0) { + continue; + } + let list: string[] | undefined = toolsByPlugin.get(owner); + if (list === undefined) { + list = []; + toolsByPlugin.set(owner, list); + } + // 每插件最多展示 8 个工具名,避免卡片过长 + if (list.length < 8) { + list.push(tn); + } + } + } + + // ---- installed external plugins metadata ---- + const externalMeta: Map> = new Map>(); + try { + const pResp = await apiClient.getWithTimeout('/plugins', 10000); + const bodyTrim = pResp.body.trim(); + let arr: Object[] = []; + if (bodyTrim.length > 0 && bodyTrim.charAt(0) === '[') { + arr = JSON.parse(pResp.body) as Object[]; + } else { + const obj: Record = JSON.parse(pResp.body) as Record; + const rawList: Object = obj['plugins'] ?? obj['data']; + if (rawList !== undefined && rawList !== null) { + arr = rawList as Object[]; + } + } + for (let i = 0; i < arr.length; i++) { + const item: Record = arr[i] as Record; + const n: string = item['name'] as string ?? ''; + if (n.length > 0) { + externalMeta.set(n, item); + } + } + } catch (e) { + // 外部列表失败不阻塞内置展示 + } + + // ---- disabled list ---- + const disabledNames: Set = new Set(); + try { + const dResp = await apiClient.getWithTimeout('/plugins/disabled', 8000); + const dObj: Record = JSON.parse(dResp.body) as Record; + const dArr: Object | undefined = dObj['disabled']; + if (dArr !== undefined && dArr !== null) { + const items: Object[] = dArr as Object[]; + for (let di = 0; di < items.length; di++) { + const dItem: Record = items[di] as Record; + const dn: string = dItem['name'] as string ?? ''; + if (dn.length > 0) { + disabledNames.add(dn); + } + } + } + } catch (e) { + // disabled endpoint may not exist; ignore + } + + // ---- merge: allNames sorted(与 GUI 一致)---- + const allNames: Set = new Set(); + loadedMap.forEach((v: boolean, k: string) => { + allNames.add(k); + }); + externalMeta.forEach((v: Record, k: string) => { + allNames.add(k); + }); + disabledNames.forEach((n: string) => { + allNames.add(n); + }); + const names: string[] = Array.from(allNames); + names.sort(); + + const rows: PluginRow[] = []; + for (let i = 0; i < names.length; i++) { + const name: string = names[i]; + const meta: Record | undefined = externalMeta.get(name); + const tools: string[] | undefined = toolsByPlugin.get(name); + const row: PluginRow = { + name: name, + loaded: loadedMap.get(name) ?? false, + disabled: disabledNames.has(name), + external: externalMeta.has(name), + version: meta !== undefined ? meta['version'] as string ?? '' : '', + description: meta !== undefined ? meta['description'] as string ?? '' : '', + tools: tools, + }; + rows.push(row); + } + return rows; +} + +function strArray(raw: Object | undefined): string[] { + const out: string[] = []; + if (raw === undefined || raw === null) { + return out; + } + const arr: Object[] = raw as Object[]; + for (let i = 0; i < arr.length; i++) { + const s: string = arr[i] as string ?? ''; + if (s.length > 0) { + out.push(s); + } + } + return out; +} + +/** + * GET /plugins/{name} —— 后端返回插件清单字段。 + * WebGUI 只是把它 JSON.stringify 进
,这里逐字段结构化展示。
+ * 内置插件不在 /plugins 里,取不到详情时由调用方退回列表已有信息。
+ */
+export async function fetchPluginDetail(name: string): Promise {
+  const resp = await apiClient.getWithTimeout('/plugins/' + name, 10000);
+  const o: Record = JSON.parse(resp.body) as Record;
+  const d: PluginDetail = emptyPluginDetail();
+  d.name = o['name'] as string ?? name;
+  d.version = o['version'] as string ?? '';
+  d.description = o['description'] as string ?? '';
+  d.author = o['author'] as string ?? '';
+  d.license = o['license'] as string ?? '';
+  d.homepage = o['homepage'] as string ?? '';
+  d.repository = o['repository'] as string ?? '';
+  d.entry = o['entry'] as string ?? '';
+  d.minVersion = o['min_version'] as string ?? '';
+  d.deprecated = o['deprecated'] as boolean ?? false;
+  d.tags = strArray(o['tags']);
+  d.files = strArray(o['files']);
+  return d;
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginStatus.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginStatus.ets
new file mode 100644
index 0000000..d1b202a
--- /dev/null
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/PluginStatus.ets
@@ -0,0 +1,78 @@
+/**
+ * 插件状态展示口径(纯函数,无 UI 依赖)。
+ *
+ * 从 pages/PluginsPage.ets 抽出:列表行与详情页共用同一套状态判定/文案/配色,
+ * 抽成模块后两处不会再各写一份。
+ *
+ * 状态口径与 WebGUI 一致:已加载绿 / 禁用待生效黄 / 已禁用红 / 未加载灰。
+ */
+
+import { PluginRow } from '../model/Model';
+
+/** 'loaded' | 'pending' | 'disabled' | 'notloaded' */
+export function pluginStatusOf(plugin: PluginRow): string {
+  if (plugin.loaded && !plugin.disabled) {
+    return 'loaded'; // 已加载
+  }
+  if (plugin.loaded && plugin.disabled) {
+    return 'pending'; // 运行中(禁用待生效)
+  }
+  if (plugin.disabled) {
+    return 'disabled'; // 已禁用
+  }
+  return 'notloaded'; // 未加载
+}
+
+export function pluginStatusText(plugin: PluginRow): string {
+  const s: string = pluginStatusOf(plugin);
+  if (s === 'loaded') {
+    return '已加载';
+  }
+  if (s === 'pending') {
+    return '待生效';
+  }
+  if (s === 'disabled') {
+    return '已禁用';
+  }
+  return '未加载';
+}
+
+/** 状态点颜色;未加载态用主题里的弱化色(mutedColor 由调用方从 palette 取)。 */
+export function pluginStatusColor(plugin: PluginRow, mutedColor: string): string {
+  const s: string = pluginStatusOf(plugin);
+  if (s === 'loaded') {
+    return '#17A964';
+  }
+  if (s === 'pending') {
+    return '#D99A2B';
+  }
+  if (s === 'disabled') {
+    return '#E84026';
+  }
+  return mutedColor;
+}
+
+/** 列表行副标题:状态 + 内置/外部 + 工具数,一行灰字,不用徽标 */
+export function pluginRowSubtitle(plugin: PluginRow): string {
+  const parts: string[] = [];
+  parts.push(pluginStatusText(plugin));
+  parts.push(plugin.external ? '外部' : '内置');
+  if (plugin.tools !== undefined && plugin.tools.length > 0) {
+    parts.push(plugin.tools.length.toString() + ' 工具');
+  }
+  return parts.join(' · ');
+}
+
+/** 详情页状态行:状态 + 内置/外部 + 已废弃 + 工具数(顺序与原实现一致) */
+export function pluginDetailStatusLine(row: PluginRow, deprecated: boolean): string {
+  const parts: string[] = [];
+  parts.push(pluginStatusText(row));
+  parts.push(row.external ? '外部' : '内置');
+  if (deprecated) {
+    parts.push('已废弃');
+  }
+  if (row.tools !== undefined && row.tools.length > 0) {
+    parts.push(row.tools.length.toString() + ' 个工具');
+  }
+  return parts.join(' · ');
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginDetailPane.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginDetailPane.ets
new file mode 100644
index 0000000..9e376d3
--- /dev/null
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginDetailPane.ets
@@ -0,0 +1,291 @@
+/**
+ * 插件详情(二级页面内容)。
+ *
+ * 从 pages/PluginsPage.ets 抽出。
+ * WebGUI 这里只有一个 JSON.stringify 的 
,
+ * 移植时改成结构化卡片:状态 / 清单字段 / 工具 / 配置 / 操作。
+ */
+
+import { PluginRow, PluginDetail } from '../model/Model';
+import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_SM,
+  ANIM_FAST, COLOR_ERROR } from '../common/Constants';
+import { pluginDetailStatusLine, pluginStatusColor } from '../common/PluginStatus';
+import { PlainCard } from './SubPage';
+import { SettingsEditor } from './SettingsEditor';
+
+@Component
+export struct PluginDetailPane {
+  @StorageProp('themeIsDark') private isDark: boolean = true;
+  @Prop detail: PluginDetail;
+  /** 当前查看详情的插件名(可能还没拿到 detail.name) */
+  @Prop activeName: string = '';
+  @Prop busy: boolean = false;
+  @Prop errorText: string = '';
+  /** 一级列表里对应的那一行;取不到详情时用它兜底,也为操作按钮提供状态 */
+  @Prop row: PluginRow | undefined = undefined;
+  onToggle?: () => void;
+  onRemove?: () => void;
+
+  private palette(): ThemePalette {
+    return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
+  }
+
+  // ---- 取值助手:ArkTS 禁止非空断言,统一在这里做 null 收敛 ----
+
+  private hasManifest(): boolean {
+    return this.detail.author.length > 0 || this.detail.license.length > 0 ||
+      this.detail.homepage.length > 0 || this.detail.repository.length > 0 ||
+      this.detail.entry.length > 0 || this.detail.minVersion.length > 0;
+  }
+
+  private isBuiltin(): boolean {
+    const r: PluginRow | undefined = this.row;
+    return r !== undefined ? !r.external : false;
+  }
+
+  private isDisabled(): boolean {
+    const r: PluginRow | undefined = this.row;
+    return r !== undefined ? r.disabled : false;
+  }
+
+  private tools(): string[] {
+    const r: PluginRow | undefined = this.row;
+    if (r === undefined) {
+      return [];
+    }
+    return r.tools ?? [];
+  }
+
+  private statusLine(): string {
+    const r: PluginRow | undefined = this.row;
+    return r !== undefined ? pluginDetailStatusLine(r, this.detail.deprecated) : '未加载';
+  }
+
+  private statusColor(): string {
+    const r: PluginRow | undefined = this.row;
+    return r !== undefined ? pluginStatusColor(r, this.palette().textMuted) : this.palette().textMuted;
+  }
+
+  /** 明细行:值为空时整行不渲染,避免详情页出现一排 "-" */
+  @Builder
+  KvRow(label: string, value: string) {
+    if (value.length > 0) {
+      Row() {
+        Text(label)
+          .fontSize(13)
+          .fontColor(this.palette().textSecondary)
+          .layoutWeight(1)
+        Text(value)
+          .fontSize(13)
+          .fontColor(this.palette().textPrimary)
+          .textAlign(TextAlign.End)
+          .maxLines(2)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+          .constraintSize({ maxWidth: 220 })
+          .margin({ left: 16 })
+      }
+      .width('100%')
+      .padding({ top: 8, bottom: 8 })
+      .alignItems(VerticalAlign.Top)
+    }
+  }
+
+  /** 详情页状态行:同样去掉徽标,一个状态点 + 一行纯文字 */
+  @Builder
+  Badges() {
+    Row({ space: 6 }) {
+      Circle({ width: 7, height: 7 })
+        .fill(this.statusColor())
+      Text(this.statusLine())
+        .fontSize(12)
+        .fontColor(this.palette().textSecondary)
+        .layoutWeight(1)
+        .maxLines(1)
+        .textOverflow({ overflow: TextOverflow.Ellipsis })
+    }
+    .width('100%')
+    .alignItems(VerticalAlign.Center)
+  }
+
+  /** 工具清单来自一级列表已合并的 kernel.tools(按 plugin 归属) */
+  @Builder
+  ToolsCard() {
+    if (this.tools().length > 0) {
+      PlainCard({ caption: '注册的工具 (' + this.tools().length.toString() + ')' }) {
+        Flex({ wrap: FlexWrap.Wrap }) {
+          ForEach(this.tools(), (tool: string) => {
+            Text(tool)
+              .fontSize(11)
+              .fontColor('#4A90D9')
+              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
+              .borderRadius(RADIUS_SM)
+              .backgroundColor(this.palette().frostSoftBg)
+              .margin({ right: 5, bottom: 5 })
+          }, (tool: string) => tool)
+        }
+      }
+    }
+  }
+
+  @Builder
+  ActionsCard() {
+    if (this.activeName.length > 0) {
+      PlainCard({ caption: '操作' }) {
+        Row() {
+          Button(this.isDisabled() ? '启用' : '禁用')
+            .height(34)
+            .fontSize(12)
+            .backgroundColor(Color.Transparent)
+            .border({
+              width: 1,
+              color: this.isDisabled()
+                ? this.palette().btnGhostBorder : 'rgba(217, 154, 43, 0.5)',
+            })
+            .fontColor(this.isDisabled()
+              ? this.palette().textSecondary : '#D99A2B')
+            .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
+            .onClick(() => {
+              const cb: (() => void) | undefined = this.onToggle;
+              if (cb !== undefined) {
+                cb();
+              }
+            })
+          Blank()
+          if (!this.isBuiltin()) {
+            Button('卸载')
+              .height(34)
+              .fontSize(12)
+              .backgroundColor(Color.Transparent)
+              .border({ width: 1, color: 'rgba(232, 64, 38, 0.45)' })
+              .fontColor(COLOR_ERROR)
+              .onClick(() => {
+                const cb: (() => void) | undefined = this.onRemove;
+                if (cb !== undefined) {
+                  cb();
+                }
+              })
+              .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
+          }
+        }
+        .width('100%')
+      }
+    }
+  }
+
+  build() {
+    // 单一根节点:与原来作为 @Builder 直接铺在 SubPageLayer 的 Column 里同构,
+    // 保持 alignItems Start,避免文本被默认居中对齐。
+    Column() {
+    if (this.busy) {
+      Row() {
+        LoadingProgress()
+          .width(26)
+          .height(26)
+          .color(this.palette().accent)
+      }
+      .width('100%')
+      .justifyContent(FlexAlign.Center)
+      .padding({ top: 30, bottom: 30 })
+      .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
+    }
+
+    if (this.errorText.length > 0) {
+      Text(this.errorText)
+        .fontSize(12)
+        .fontColor(COLOR_ERROR)
+        .padding({ left: 4, bottom: 12 })
+        .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
+    }
+
+    // 概览卡:名称、版本、状态徽标、描述
+    PlainCard({ caption: '概览' }) {
+      Row({ space: 8 }) {
+        Text(this.detail.name.length > 0 ? this.detail.name : this.activeName)
+          .fontSize(17)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(this.palette().textPrimary)
+          .layoutWeight(1)
+          .maxLines(1)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+        if (this.detail.version.length > 0) {
+          Text('v' + this.detail.version)
+            .fontSize(12)
+            .fontColor(this.palette().textSecondary)
+        }
+      }
+      .width('100%')
+      .margin({ bottom: 10 })
+
+      this.Badges()
+
+      if (this.detail.description.length > 0) {
+        Text(this.detail.description)
+          .fontSize(13)
+          .fontColor(this.palette().textSecondary)
+          .width('100%')
+          .margin({ top: 10 })
+      }
+    }
+
+    // 清单卡:只有真拿到字段才出卡,否则会留一张空壳(内置插件没有清单文件)
+    if (this.hasManifest()) {
+      PlainCard({ caption: '清单' }) {
+        this.KvRow('作者', this.detail.author)
+        this.KvRow('许可证', this.detail.license)
+        this.KvRow('主页', this.detail.homepage)
+        this.KvRow('仓库', this.detail.repository)
+        this.KvRow('入口', this.detail.entry)
+        this.KvRow('最低内核版本', this.detail.minVersion)
+      }
+    }
+
+    if (this.detail.tags.length > 0) {
+      PlainCard({ caption: '标签' }) {
+        Flex({ wrap: FlexWrap.Wrap }) {
+          ForEach(this.detail.tags, (t: string) => {
+            Text(t)
+              .fontSize(10)
+              .fontColor('#4A90D9')
+              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
+              .borderRadius(RADIUS_SM)
+              .backgroundColor(this.palette().frostSoftBg)
+              .margin({ right: 5, bottom: 5 })
+          }, (t: string) => t)
+        }
+      }
+    }
+
+    this.ToolsCard()
+
+    // 插件配置:plugin..* 从后端 /settings?prefix= 取,就地编辑。
+    // 这些 key 属于插件本身,之前被平铺在「设置 → 后端配置」里,
+    // 现在归位到插件详情页 —— 「插件的设计页面就是插件的详情页」。
+    if (this.activeName.length > 0) {
+      PlainCard({ caption: '插件配置' }) {
+        SettingsEditor({
+          prefix: 'plugin.' + this.activeName + '.',
+          emptyHint: '该插件没有暴露可配置项',
+        })
+      }
+    }
+
+    if (this.detail.files.length > 0) {
+      PlainCard({ caption: '文件 (' + this.detail.files.length.toString() + ')' }) {
+        ForEach(this.detail.files, (f: string) => {
+          Text(f)
+            .fontSize(12)
+            .fontColor(this.palette().textSecondary)
+            .width('100%')
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+            .margin({ bottom: 4 })
+        }, (f: string) => f)
+      }
+    }
+
+    this.ActionsCard()
+    }
+    .width('100%')
+    .alignItems(HorizontalAlign.Start)
+  }
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginListView.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginListView.ets
new file mode 100644
index 0000000..d0ca63a
--- /dev/null
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginListView.ets
@@ -0,0 +1,166 @@
+/**
+ * 插件一级列表(列表卡 + 三种占位态)。
+ *
+ * 从 pages/PluginsPage.ets 抽出:列表只负责"选谁",
+ * 描述/工具/启停全部下沉到详情页。
+ */
+
+import { PluginRow } from '../model/Model';
+import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG, RADIUS_MD,
+  ANIM_FAST, ANIM_ENTER } from '../common/Constants';
+import { pluginRowSubtitle, pluginStatusColor } from '../common/PluginStatus';
+import { noConnectionMessage } from '../common/UserError';
+import { MotionBase } from './MotionBase';
+
+/**
+ * 单个插件行卡。
+ *
+ * 外层 Column 只为承载 transition:.transition() 不能直接挂在自定义组件
+ * 调用点上(会生成 __Common__ 包装节点)。按压缩放由 MotionBase 统一提供,
+ * 每行自带独立按压态,不再需要 pressedName 这种"哪一行被按"的手工记账。
+ */
+@Component
+export struct PluginListCard {
+  @StorageProp('themeIsDark') private isDark: boolean = true;
+  @Prop plugin: PluginRow;
+  /** 是否当前选中(宽屏高亮左侧列表项)。单独用基本类型传,选中态变更才能触发更新 */
+  @Prop active: boolean = false;
+  onTap?: () => void;
+
+  private palette(): ThemePalette {
+    return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
+  }
+
+  build() {
+    Column() {
+      MotionBase({ pressEnabled: true }) {
+        Row() {
+          Column({ space: 3 }) {
+            Row({ space: 6 }) {
+              Text(this.plugin.name)
+                .fontSize(15)
+                .fontWeight(this.active ? FontWeight.Medium : FontWeight.Normal)
+                .fontColor(this.active ? this.palette().accent : this.palette().textPrimary)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
+              if (this.plugin.version !== undefined && this.plugin.version.length > 0) {
+                Text('v' + this.plugin.version)
+                  .fontSize(10)
+                  .fontColor(this.palette().textMuted)
+              }
+            }
+            // 徽标全部去掉(用户要求):状态用一个 3vp 圆点表达,
+            // 其余信息退化为一行灰字副标题 —— 列表只负责"选谁",细节看详情页。
+            Row({ space: 6 }) {
+              Circle({ width: 6, height: 6 })
+                .fill(pluginStatusColor(this.plugin, this.palette().textMuted))
+              Text(pluginRowSubtitle(this.plugin))
+                .fontSize(11)
+                .fontColor(this.palette().textMuted)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .layoutWeight(1)
+            }
+            .width('100%')
+            .alignItems(VerticalAlign.Center)
+          }
+          .layoutWeight(1)
+          .alignItems(HorizontalAlign.Start)
+
+          Image($r('app.media.ic_chevron_right'))
+            .width(15)
+            .height(15)
+            .fillColor(this.active ? this.palette().accent : this.palette().textMuted)
+            .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
+            .draggable(false)
+        }
+        .width('100%')
+        .padding(14)
+        .borderRadius(RADIUS_LG)
+        .backgroundColor(this.active ? this.palette().accentBg : this.palette().bgCard)
+        .border({
+          width: 1,
+          color: this.active ? this.palette().accent : this.palette().glassBorder,
+        })
+        // 选中态的底色/描边渐变:MotionBase 的 .animation() 到不了这里
+        .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
+        .alignItems(VerticalAlign.Center)
+        .onClick(() => {
+          const cb: (() => void) | undefined = this.onTap;
+          if (cb !== undefined) {
+            cb();
+          }
+        })
+      }
+    }
+    .width('100%')
+    .margin({ bottom: 10 })
+    // ForEach key 含 loaded/disabled:启停会整行重挂载,靠 transition 变成交叉淡入
+    .transition(TransitionEffect.OPACITY
+      .combine(TransitionEffect.translate({ y: 12 }))
+      .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut }))
+  }
+}
+
+/**
+ * 一级列表整体:占位态 + 行卡 ForEach。
+ * 取数留在页面里,这里只吃数据。
+ */
+@Component
+export struct PluginListView {
+  @StorageProp('themeIsDark') private isDark: boolean = true;
+  @Prop plugins: PluginRow[] = [];
+  @Prop busy: boolean = false;
+  @Prop activeName: string = '';
+  @Prop hasConn: boolean = false;
+  onSelect?: (name: string) => void;
+
+  private palette(): ThemePalette {
+    return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
+  }
+
+  build() {
+    Column() {
+      // ── 加载中 / 未配置 / 空列表三种占位态 ──
+      if (this.busy && this.plugins.length === 0) {
+        LoadingProgress()
+          .width(32)
+          .height(32)
+          .color(this.palette().accent)
+          .margin({ top: 40 })
+          .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
+      }
+
+      if (!this.hasConn) {
+        Text(noConnectionMessage())
+          .fontSize(13)
+          .fontColor(this.palette().textMuted)
+          .padding(20)
+          .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
+      }
+
+      if (!this.busy && this.plugins.length === 0 && this.hasConn) {
+        Text('暂无已加载插件')
+          .fontSize(13)
+          .fontColor(this.palette().textMuted)
+          .padding(20)
+          .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
+      }
+
+      ForEach(this.plugins, (plugin: PluginRow) => {
+        PluginListCard({
+          plugin: plugin,
+          active: this.activeName === plugin.name,
+          onTap: () => {
+            const cb: ((name: string) => void) | undefined = this.onSelect;
+            if (cb !== undefined) {
+              cb(plugin.name);
+            }
+          },
+        })
+      }, (plugin: PluginRow) => plugin.name + (plugin.loaded ? 'L' : '') + (plugin.disabled ? 'D' : ''))
+    }
+    .width('100%')
+  }
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginsOverlays.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginsOverlays.ets
new file mode 100644
index 0000000..3da2b27
--- /dev/null
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PluginsOverlays.ets
@@ -0,0 +1,65 @@
+/**
+ * 安装表单悬浮卡。
+ *
+ * 从 pages/PluginsPage.ets 抽出(浮在内容之上的独立图层,与列表/详情无关)。
+ * 轻提示条与设置页共用,见 components/ToastBar.ets。
+ */
+
+import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_MD, RADIUS_SM,
+  ANIM_ENTER } from '../common/Constants';
+
+/** 安装表单:悬浮在安装按钮上方的一张玻璃卡(点悬浮按钮开合) */
+@Component
+export struct PluginInstallForm {
+  @StorageProp('themeIsDark') private isDark: boolean = true;
+  @Prop url: string = '';
+  onUrlChange?: (v: string) => void;
+  onInstall?: () => void;
+
+  private palette(): ThemePalette {
+    return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
+  }
+
+  build() {
+    Row() {
+      TextInput({ placeholder: '.hmap 包下载 URL', text: this.url })
+        .layoutWeight(1)
+        .height(36)
+        .fontSize(14)
+        .fontColor(this.palette().textPrimary)
+        .placeholderColor(this.palette().textMuted)
+        .backgroundColor(this.palette().bgInput)
+        .borderRadius(RADIUS_SM)
+        .border({ width: 1, color: this.palette().border })
+        .onChange((v: string) => {
+          const cb: ((v: string) => void) | undefined = this.onUrlChange;
+          if (cb !== undefined) {
+            cb(v);
+          }
+        })
+      Button('安装')
+        .height(36)
+        .fontSize(12)
+        .backgroundColor(this.palette().accent)
+        .fontColor('#FFFFFF')
+        .margin({ left: 6 })
+        .onClick(() => {
+          const cb: (() => void) | undefined = this.onInstall;
+          if (cb !== undefined) {
+            cb();
+          }
+        })
+    }
+    .width('100%')
+    .padding(10)
+    .margin({ bottom: 10 })
+    .backgroundColor(this.palette().navBarBg)
+    .borderRadius(RADIUS_MD)
+    .border({ width: 1, color: this.palette().navBarBorder })
+    .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 })
+    .alignItems(VerticalAlign.Center)
+    .transition(TransitionEffect.OPACITY
+      .combine(TransitionEffect.translate({ y: 12 }))
+      .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut }))
+  }
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ToastBar.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ToastBar.ets
new file mode 100644
index 0000000..5d87ee6
--- /dev/null
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ToastBar.ets
@@ -0,0 +1,48 @@
+/**
+ * 右下角轻提示条(设置页与插件页共用)。
+ *
+ * 两处原本各写一份,只有"描边"这一处不同:设置页无描边,插件页有。
+ * 用 bordered 表达这个差异(width 0 的边框不占位、不可见)。
+ *
+ * 配色由调用方决定:颜色标志必须在调用方落 animateTo 之前先写好,
+ * 否则第一帧会用上一条 toast 的配色。
+ */
+
+import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_MD, ANIM_NORMAL } from '../common/Constants';
+
+@Component
+export struct ToastBar {
+  @StorageProp('themeIsDark') private isDark: boolean = true;
+  @Prop msg: string = '';
+  @Prop isError: boolean = false;
+  /** 是否带一圈语义色描边(插件页用,设置页不用) */
+  @Prop bordered: boolean = false;
+
+  private palette(): ThemePalette {
+    return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
+  }
+
+  build() {
+    if (this.msg.length > 0) {
+      Row() {
+        Text(this.msg)
+          .fontSize(13)
+          .fontColor(this.isError ? this.palette().toastErrorText : this.palette().toastText)
+          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
+          .borderRadius(RADIUS_MD)
+          .backgroundColor(this.isError ? this.palette().toastErrorBg : this.palette().toastBg)
+          .border({
+            width: this.bordered ? 1 : 0,
+            color: this.isError ? 'rgba(232, 64, 38, 0.3)' : 'rgba(23, 169, 100, 0.3)',
+          })
+      }
+      .width('100%')
+      .justifyContent(FlexAlign.End)
+      .padding({ right: 20 })
+      .margin({ bottom: 166 })
+      .transition(TransitionEffect.OPACITY
+        .combine(TransitionEffect.translate({ y: 12 }))
+        .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }))
+    }
+  }
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets
index 854331c..39b776c 100644
--- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets
@@ -1,24 +1,26 @@
 import { apiClient } from '../common/ApiClient';
-import { userMessage, noConnectionMessage } from '../common/UserError';
+import { userMessage } from '../common/UserError';
 import { handleNavOnScroll } from '../common/NavBarController';
 import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry';
 import { PluginRow, PluginDetail, emptyPluginDetail } from '../model/Model';
 import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT } from '../common/Constants';
-import { RADIUS_LG, RADIUS_MD, RADIUS_SM, RADIUS_PILL, COLOR_ERROR } from '../common/Constants';
-import { ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants';
-import { MotionBase } from '../components/MotionBase';
-import { PageTopBar, NavFloatOverlay, NavFloatRow, GlassShell, FloatIconButton } from '../components/PageTopBar';
-import { SubPageLayer, PlainCard, markSubPageOpen, subPageParam } from '../components/SubPage';
-import { SettingsEditor } from '../components/SettingsEditor';
-
-interface InstallBody {
-  url: string;
-}
+import { ANIM_NORMAL, ANIM_ENTER } from '../common/Constants';
+import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from '../components/PageTopBar';
+import { SubPageLayer, markSubPageOpen, subPageParam } from '../components/SubPage';
+import { PluginDetailPane } from '../components/PluginDetailPane';
+import { PluginListView } from '../components/PluginListView';
+import { PluginInstallForm } from '../components/PluginsOverlays';
+import { ToastBar } from '../components/ToastBar';
+import { fetchPluginRows, fetchPluginDetail } from '../common/PluginApi';
 
 /** 二级页面标识:插件详情 */
 const SUB_NONE: string = '';
 const SUB_DETAIL: string = 'detail';
 
+interface InstallBody {
+  url: string;
+}
+
 @Component
 export struct PluginsPage {
   @StorageProp('themeIsDark') private isDark: boolean = true;
@@ -58,134 +60,14 @@ export struct PluginsPage {
     return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
   }
 
-  /**
-   * 数据源对齐 WebGUI renderPlugins:
-   *  - GET /kernel          → plugins[{name,loaded}](含全部内置插件)+ tools(按 plugin 归属)
-   *  - GET /plugins         → 已安装外部插件元数据(version/description 等)
-   *  - GET /plugins/disabled → {disabled:[{name,...}]}
-   * 三方按名称合并去重排序。
-   */
+  /** 取数口径见 common/PluginApi.ets;这里只负责把结果落到 @State。 */
   private async loadPlugins(): Promise {
     if (!apiClient.hasConnection()) {
       return;
     }
     this.loading = true;
     try {
-      // ---- kernel: loaded plugins + tool ownership ----
-      const kResp = await apiClient.getWithTimeout('/kernel', 12000);
-      const kernelObj: Record = JSON.parse(kResp.body) as Record;
-      const loadedMap: Map = new Map();
-      const kpRaw: Object | undefined = kernelObj['plugins'];
-      if (kpRaw !== undefined && kpRaw !== null) {
-        const kpArr: Object[] = kpRaw as Object[];
-        for (let i = 0; i < kpArr.length; i++) {
-          const item: Record = kpArr[i] as Record;
-          const n: string = item['name'] as string ?? '';
-          if (n.length === 0) {
-            continue;
-          }
-          loadedMap.set(n, item['loaded'] as boolean ?? true);
-        }
-      }
-      const toolsByPlugin: Map = new Map();
-      const tRaw: Object | undefined = kernelObj['tools'];
-      if (tRaw !== undefined && tRaw !== null) {
-        const tArr: Object[] = tRaw as Object[];
-        for (let i = 0; i < tArr.length; i++) {
-          const item: Record = tArr[i] as Record;
-          const tn: string = item['name'] as string ?? '';
-          const owner: string = item['plugin'] as string ?? '';
-          if (tn.length === 0 || owner.length === 0) {
-            continue;
-          }
-          let list: string[] | undefined = toolsByPlugin.get(owner);
-          if (list === undefined) {
-            list = [];
-            toolsByPlugin.set(owner, list);
-          }
-          // 每插件最多展示 8 个工具名,避免卡片过长
-          if (list.length < 8) {
-            list.push(tn);
-          }
-        }
-      }
-
-      // ---- installed external plugins metadata ----
-      const externalMeta: Map> = new Map>();
-      try {
-        const pResp = await apiClient.getWithTimeout('/plugins', 10000);
-        const bodyTrim = pResp.body.trim();
-        let arr: Object[] = [];
-        if (bodyTrim.length > 0 && bodyTrim.charAt(0) === '[') {
-          arr = JSON.parse(pResp.body) as Object[];
-        } else {
-          const obj: Record = JSON.parse(pResp.body) as Record;
-          const rawList: Object = obj['plugins'] ?? obj['data'];
-          if (rawList !== undefined && rawList !== null) {
-            arr = rawList as Object[];
-          }
-        }
-        for (let i = 0; i < arr.length; i++) {
-          const item: Record = arr[i] as Record;
-          const n: string = item['name'] as string ?? '';
-          if (n.length > 0) {
-            externalMeta.set(n, item);
-          }
-        }
-      } catch (e) {
-        // 外部列表失败不阻塞内置展示
-      }
-
-      // ---- disabled list ----
-      const disabledNames: Set = new Set();
-      try {
-        const dResp = await apiClient.getWithTimeout('/plugins/disabled', 8000);
-        const dObj: Record = JSON.parse(dResp.body) as Record;
-        const dArr: Object | undefined = dObj['disabled'];
-        if (dArr !== undefined && dArr !== null) {
-          const items: Object[] = dArr as Object[];
-          for (let di = 0; di < items.length; di++) {
-            const dItem: Record = items[di] as Record;
-            const dn: string = dItem['name'] as string ?? '';
-            if (dn.length > 0) {
-              disabledNames.add(dn);
-            }
-          }
-        }
-      } catch (e) {
-        // disabled endpoint may not exist; ignore
-      }
-
-      // ---- merge: allNames sorted(与 GUI 一致)----
-      const allNames: Set = new Set();
-      loadedMap.forEach((v: boolean, k: string) => {
-        allNames.add(k);
-      });
-      externalMeta.forEach((v: Record, k: string) => {
-        allNames.add(k);
-      });
-      disabledNames.forEach((n: string) => {
-        allNames.add(n);
-      });
-      const names: string[] = Array.from(allNames);
-      names.sort();
-
-      const rows: PluginRow[] = [];
-      for (let i = 0; i < names.length; i++) {
-        const name: string = names[i];
-        const meta: Record | undefined = externalMeta.get(name);
-        const tools: string[] | undefined = toolsByPlugin.get(name);
-        const row: PluginRow = {
-          name: name,
-          loaded: loadedMap.get(name) ?? false,
-          disabled: disabledNames.has(name),
-          external: externalMeta.has(name),
-          version: meta !== undefined ? meta['version'] as string ?? '' : '',
-          description: meta !== undefined ? meta['description'] as string ?? '' : '',
-          tools: tools,
-        };
-        rows.push(row);
-      }
+      const rows: PluginRow[] = await fetchPluginRows();
       // 列表整体重建(ForEach key 含 loaded/disabled,启停会整行重挂载):
       // 放进 animateTo 让新旧行走 transition 交叉淡入,而不是硬切一帧。
       this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => {
@@ -199,20 +81,6 @@ export struct PluginsPage {
     });
   }
 
-  /** 徽标状态:与 GUI 一致 —— 已加载绿 / 禁用待生效黄 / 已禁用红 / 未加载灰。 */
-  private statusOf(plugin: PluginRow): string {
-    if (plugin.loaded && !plugin.disabled) {
-      return 'loaded'; // 已加载
-    }
-    if (plugin.loaded && plugin.disabled) {
-      return 'pending'; // 运行中(禁用待生效)
-    }
-    if (plugin.disabled) {
-      return 'disabled'; // 已禁用
-    }
-    return 'notloaded'; // 未加载
-  }
-
   private async togglePlugin(plugin: PluginRow): Promise {
     const name: string = plugin.name;
     const action: string = plugin.disabled ? 'enable' : 'disable';
@@ -280,29 +148,14 @@ export struct PluginsPage {
   }
 
   /**
-   * GET /plugins/{name} —— 后端返回插件清单字段。
-   * WebGUI 只是把它 JSON.stringify 进 
,这里逐字段结构化展示。
+   * 详情数据落地。
    * 内置插件不在 /plugins 里,取不到详情时退回用列表已有的信息。
    */
   private async loadDetail(name: string): Promise {
     this.detailLoading = true;
     this.detailError = '';
     try {
-      const resp = await apiClient.getWithTimeout('/plugins/' + name, 10000);
-      const o: Record = JSON.parse(resp.body) as Record;
-      const d: PluginDetail = emptyPluginDetail();
-      d.name = o['name'] as string ?? name;
-      d.version = o['version'] as string ?? '';
-      d.description = o['description'] as string ?? '';
-      d.author = o['author'] as string ?? '';
-      d.license = o['license'] as string ?? '';
-      d.homepage = o['homepage'] as string ?? '';
-      d.repository = o['repository'] as string ?? '';
-      d.entry = o['entry'] as string ?? '';
-      d.minVersion = o['min_version'] as string ?? '';
-      d.deprecated = o['deprecated'] as boolean ?? false;
-      d.tags = this.strArray(o['tags']);
-      d.files = this.strArray(o['files']);
+      const d: PluginDetail = await fetchPluginDetail(name);
       // 详情字段一次性落地:条件卡片在 animateTo 帧内挂载,V1 给它们默认透明度过渡
       this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => {
         this.detail = d;
@@ -327,21 +180,6 @@ export struct PluginsPage {
     });
   }
 
-  private strArray(raw: Object | undefined): string[] {
-    const out: string[] = [];
-    if (raw === undefined || raw === null) {
-      return out;
-    }
-    const arr: Object[] = raw as Object[];
-    for (let i = 0; i < arr.length; i++) {
-      const s: string = arr[i] as string ?? '';
-      if (s.length > 0) {
-        out.push(s);
-      }
-    }
-    return out;
-  }
-
   private findRow(name: string): PluginRow | null {
     for (let i = 0; i < this.plugins.length; i++) {
       if (this.plugins[i].name === name) {
@@ -387,8 +225,15 @@ export struct PluginsPage {
         Column() {
           Scroll() {
             Column() {
-              this.ListStates()
-              this.PluginList()
+              PluginListView({
+                plugins: this.plugins,
+                busy: this.loading,
+                activeName: this.activeName,
+                hasConn: apiClient.hasConnection(),
+                onSelect: (name: string) => {
+                  this.openDetail(name);
+                },
+              })
             }
             .width('100%')
             .padding({ left: 16, right: 16, top: 76, bottom: 174 })
@@ -411,7 +256,15 @@ export struct PluginsPage {
         // 安装表单以悬浮卡形式浮在按钮上方,不再占用列表顶部一行。
         NavFloatOverlay({ tab: 1 }) {
           if (this.showInstallForm) {
-            this.InstallForm()
+            PluginInstallForm({
+              url: this.installUrl,
+              onUrlChange: (v: string) => {
+                this.installUrl = v;
+              },
+              onInstall: () => {
+                this.installPlugin();
+              },
+            })
           }
           NavFloatRow() {
             FloatIconButton({
@@ -441,7 +294,7 @@ export struct PluginsPage {
           }
         }
 
-        this.Toast()
+        ToastBar({ msg: this.toastMsg, isError: this.toastIsError, bordered: true })
       }
       .width('100%')
       .height('100%')
@@ -493,503 +346,29 @@ export struct PluginsPage {
             this.loadDetail(this.activeName);
           },
         }) {
-          this.DetailContent()
+          PluginDetailPane({
+            detail: this.detail,
+            activeName: this.activeName,
+            busy: this.detailLoading,
+            errorText: this.detailError,
+            row: this.activeRow() ?? undefined,
+            onToggle: () => {
+              const r: PluginRow | null = this.activeRow();
+              if (r !== null) {
+                this.togglePlugin(r);
+              }
+            },
+            onRemove: () => {
+              const r: PluginRow | null = this.activeRow();
+              if (r !== null) {
+                this.removePlugin(r);
+              }
+            },
+          })
         }
       }
     }
     .hideTitleBar(true)
     .backgroundColor(Color.Transparent)
   }
-
-  /** 安装表单:悬浮在安装按钮上方的一张玻璃卡(点悬浮按钮开合) */
-  @Builder
-  InstallForm() {
-    Row() {
-      TextInput({ placeholder: '.hmap 包下载 URL', text: this.installUrl })
-        .layoutWeight(1)
-        .height(36)
-        .fontSize(14)
-        .fontColor(this.palette().textPrimary)
-        .placeholderColor(this.palette().textMuted)
-        .backgroundColor(this.palette().bgInput)
-        .borderRadius(RADIUS_SM)
-        .border({ width: 1, color: this.palette().border })
-        .onChange((v: string) => {
-          this.installUrl = v;
-        })
-      Button('安装')
-        .height(36)
-        .fontSize(12)
-        .backgroundColor(this.palette().accent)
-        .fontColor('#FFFFFF')
-        .margin({ left: 6 })
-        .onClick(() => {
-          this.installPlugin();
-        })
-    }
-    .width('100%')
-    .padding(10)
-    .margin({ bottom: 10 })
-    .backgroundColor(this.palette().navBarBg)
-    .borderRadius(RADIUS_MD)
-    .border({ width: 1, color: this.palette().navBarBorder })
-    .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 })
-    .alignItems(VerticalAlign.Center)
-    .transition(TransitionEffect.OPACITY
-      .combine(TransitionEffect.translate({ y: 12 }))
-      .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut }))
-  }
-
-  /** 加载中 / 未配置 / 空列表三种占位态 */
-  @Builder
-  ListStates() {
-    if (this.loading && this.plugins.length === 0) {
-      LoadingProgress()
-        .width(32)
-        .height(32)
-        .color(this.palette().accent)
-        .margin({ top: 40 })
-        .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
-    }
-
-    if (!apiClient.hasConnection()) {
-      Text(noConnectionMessage())
-        .fontSize(13)
-        .fontColor(this.palette().textMuted)
-        .padding(20)
-        .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
-    }
-
-    if (!this.loading && this.plugins.length === 0 && apiClient.hasConnection()) {
-      Text('暂无已加载插件')
-        .fontSize(13)
-        .fontColor(this.palette().textMuted)
-        .padding(20)
-        .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
-    }
-  }
-
-  /**
-   * 一级列表:每个插件一张紧凑卡(名称 + 状态徽标 + 右尖角)。
-   * 描述、工具清单、启停/卸载操作全部下沉到详情页 —— 列表只负责选择。
-   */
-  @Builder
-  PluginList() {
-    ForEach(this.plugins, (plugin: PluginRow) => {
-      // 外层 Column 只为承载 transition:.transition() 不能直接挂在自定义组件
-      // 调用点上(会生成 __Common__ 包装节点)。按压缩放由 MotionBase 统一提供,
-      // 每行自带独立按压态,不再需要 pressedName 这种"哪一行被按"的手工记账。
-      Column() {
-        MotionBase({ pressEnabled: true }) {
-          Row() {
-            Column({ space: 3 }) {
-              Row({ space: 6 }) {
-                Text(plugin.name)
-                  .fontSize(15)
-                  .fontWeight(this.activeName === plugin.name ? FontWeight.Medium : FontWeight.Normal)
-                  .fontColor(this.activeName === plugin.name
-                    ? this.palette().accent : this.palette().textPrimary)
-                  .maxLines(1)
-                  .textOverflow({ overflow: TextOverflow.Ellipsis })
-                  .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
-                if (plugin.version !== undefined && plugin.version.length > 0) {
-                  Text('v' + plugin.version)
-                    .fontSize(10)
-                    .fontColor(this.palette().textMuted)
-                }
-              }
-              // 徽标全部去掉(用户要求):状态用一个 3vp 圆点表达,
-              // 其余信息退化为一行灰字副标题 —— 列表只负责"选谁",细节看详情页。
-              Row({ space: 6 }) {
-                Circle({ width: 6, height: 6 })
-                  .fill(this.statusDotColor(plugin))
-                Text(this.rowSubtitle(plugin))
-                  .fontSize(11)
-                  .fontColor(this.palette().textMuted)
-                  .maxLines(1)
-                  .textOverflow({ overflow: TextOverflow.Ellipsis })
-                  .layoutWeight(1)
-              }
-              .width('100%')
-              .alignItems(VerticalAlign.Center)
-            }
-            .layoutWeight(1)
-            .alignItems(HorizontalAlign.Start)
-
-            Image($r('app.media.ic_chevron_right'))
-              .width(15)
-              .height(15)
-              .fillColor(this.activeName === plugin.name
-                ? this.palette().accent : this.palette().textMuted)
-              .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
-              .draggable(false)
-          }
-          .width('100%')
-          .padding(14)
-          .borderRadius(RADIUS_LG)
-          .backgroundColor(this.activeName === plugin.name
-            ? this.palette().accentBg : this.palette().bgCard)
-          .border({
-            width: 1,
-            color: this.activeName === plugin.name
-              ? this.palette().accent : this.palette().glassBorder,
-          })
-          // 选中态的底色/描边渐变:MotionBase 的 .animation() 到不了这里
-          .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
-          .alignItems(VerticalAlign.Center)
-          .onClick(() => {
-            this.openDetail(plugin.name);
-          })
-        }
-      }
-      .width('100%')
-      .margin({ bottom: 10 })
-      // ForEach key 含 loaded/disabled:启停会整行重挂载,靠 transition 变成交叉淡入
-      .transition(TransitionEffect.OPACITY
-        .combine(TransitionEffect.translate({ y: 12 }))
-        .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut }))
-    }, (plugin: PluginRow) => plugin.name + (plugin.loaded ? 'L' : '') + (plugin.disabled ? 'D' : ''))
-  }
-
-  /**
-   * 二级页面:插件详情。
-   * WebGUI 这里只有一个 JSON.stringify 的 
,
-   * 移植时改成结构化卡片:状态 / 清单字段 / 工具 / 操作。
-   */
-  @Builder
-  DetailContent() {
-    if (this.detailLoading) {
-      Row() {
-        LoadingProgress()
-          .width(26)
-          .height(26)
-          .color(this.palette().accent)
-      }
-      .width('100%')
-      .justifyContent(FlexAlign.Center)
-      .padding({ top: 30, bottom: 30 })
-      .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
-    }
-
-    if (this.detailError.length > 0) {
-      Text(this.detailError)
-        .fontSize(12)
-        .fontColor(COLOR_ERROR)
-        .padding({ left: 4, bottom: 12 })
-        .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
-    }
-
-    // 概览卡:名称、版本、状态徽标、描述
-    PlainCard({ caption: '概览' }) {
-      Row({ space: 8 }) {
-        Text(this.detail.name.length > 0 ? this.detail.name : this.activeName)
-          .fontSize(17)
-          .fontWeight(FontWeight.Bold)
-          .fontColor(this.palette().textPrimary)
-          .layoutWeight(1)
-          .maxLines(1)
-          .textOverflow({ overflow: TextOverflow.Ellipsis })
-        if (this.detail.version.length > 0) {
-          Text('v' + this.detail.version)
-            .fontSize(12)
-            .fontColor(this.palette().textSecondary)
-        }
-      }
-      .width('100%')
-      .margin({ bottom: 10 })
-
-      this.DetailBadges()
-
-      if (this.detail.description.length > 0) {
-        Text(this.detail.description)
-          .fontSize(13)
-          .fontColor(this.palette().textSecondary)
-          .width('100%')
-          .margin({ top: 10 })
-      }
-    }
-
-    // 清单卡:只有真拿到字段才出卡,否则会留一张空壳(内置插件没有清单文件)
-    if (this.hasManifest()) {
-      PlainCard({ caption: '清单' }) {
-        this.KvRow('作者', this.detail.author)
-        this.KvRow('许可证', this.detail.license)
-        this.KvRow('主页', this.detail.homepage)
-        this.KvRow('仓库', this.detail.repository)
-        this.KvRow('入口', this.detail.entry)
-        this.KvRow('最低内核版本', this.detail.minVersion)
-      }
-    }
-
-    if (this.detail.tags.length > 0) {
-      PlainCard({ caption: '标签' }) {
-        Flex({ wrap: FlexWrap.Wrap }) {
-          ForEach(this.detail.tags, (t: string) => {
-            Text(t)
-              .fontSize(10)
-              .fontColor('#4A90D9')
-              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
-              .borderRadius(RADIUS_SM)
-              .backgroundColor(this.palette().frostSoftBg)
-              .margin({ right: 5, bottom: 5 })
-          }, (t: string) => t)
-        }
-      }
-    }
-
-    this.DetailTools()
-
-    // 插件配置:plugin..* 从后端 /settings?prefix= 取,就地编辑。
-    // 这些 key 属于插件本身,之前被平铺在「设置 → 后端配置」里,
-    // 现在归位到插件详情页 —— 「插件的设计页面就是插件的详情页」。
-    if (this.activeName.length > 0) {
-      PlainCard({ caption: '插件配置' }) {
-        SettingsEditor({
-          prefix: 'plugin.' + this.activeName + '.',
-          emptyHint: '该插件没有暴露可配置项',
-        })
-      }
-    }
-
-    if (this.detail.files.length > 0) {
-      PlainCard({ caption: '文件 (' + this.detail.files.length.toString() + ')' }) {
-        ForEach(this.detail.files, (f: string) => {
-          Text(f)
-            .fontSize(12)
-            .fontColor(this.palette().textSecondary)
-            .width('100%')
-            .maxLines(1)
-            .textOverflow({ overflow: TextOverflow.Ellipsis })
-            .margin({ bottom: 4 })
-        }, (f: string) => f)
-      }
-    }
-
-    this.DetailActions()
-  }
-
-  /** 详情页状态行:同样去掉徽标,一个状态点 + 一行纯文字 */
-  @Builder
-  DetailBadges() {
-    Row({ space: 6 }) {
-      Circle({ width: 7, height: 7 })
-        .fill(this.activeStatusColor())
-      Text(this.detailStatusLine())
-        .fontSize(12)
-        .fontColor(this.palette().textSecondary)
-        .layoutWeight(1)
-        .maxLines(1)
-        .textOverflow({ overflow: TextOverflow.Ellipsis })
-    }
-    .width('100%')
-    .alignItems(VerticalAlign.Center)
-  }
-
-  private detailStatusLine(): string {
-    const parts: string[] = [];
-    parts.push(this.activeStatusText());
-    parts.push(this.activeIsBuiltin() ? '内置' : '外部');
-    if (this.detail.deprecated) {
-      parts.push('已废弃');
-    }
-    const n: number = this.activeTools().length;
-    if (n > 0) {
-      parts.push(n.toString() + ' 个工具');
-    }
-    return parts.join(' · ');
-  }
-
-  /** 工具清单来自一级列表已合并的 kernel.tools(按 plugin 归属) */
-  @Builder
-  DetailTools() {
-    if (this.activeTools().length > 0) {
-      PlainCard({ caption: '注册的工具 (' + this.activeTools().length.toString() + ')' }) {
-        Flex({ wrap: FlexWrap.Wrap }) {
-          ForEach(this.activeTools(), (tool: string) => {
-            Text(tool)
-              .fontSize(11)
-              .fontColor('#4A90D9')
-              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
-              .borderRadius(RADIUS_SM)
-              .backgroundColor(this.palette().frostSoftBg)
-              .margin({ right: 5, bottom: 5 })
-          }, (tool: string) => tool)
-        }
-      }
-    }
-  }
-
-  @Builder
-  DetailActions() {
-    if (this.activeName.length > 0) {
-      PlainCard({ caption: '操作' }) {
-        Row() {
-          Button(this.activeIsDisabled() ? '启用' : '禁用')
-            .height(34)
-            .fontSize(12)
-            .backgroundColor(Color.Transparent)
-            .border({
-              width: 1,
-              color: this.activeIsDisabled()
-                ? this.palette().btnGhostBorder : 'rgba(217, 154, 43, 0.5)',
-            })
-            .fontColor(this.activeIsDisabled()
-              ? this.palette().textSecondary : '#D99A2B')
-            .animation({ duration: ANIM_FAST, curve: Curve.EaseOut })
-            .onClick(() => {
-              const r: PluginRow | null = this.activeRow();
-              if (r !== null) {
-                this.togglePlugin(r);
-              }
-            })
-          Blank()
-          if (!this.activeIsBuiltin()) {
-            Button('卸载')
-              .height(34)
-              .fontSize(12)
-              .backgroundColor(Color.Transparent)
-              .border({ width: 1, color: 'rgba(232, 64, 38, 0.45)' })
-              .fontColor(COLOR_ERROR)
-              .onClick(() => {
-                const r: PluginRow | null = this.activeRow();
-                if (r !== null) {
-                  this.removePlugin(r);
-                }
-              })
-              .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut }))
-          }
-        }
-        .width('100%')
-      }
-    }
-  }
-
-  // ---- 详情页取值助手:ArkTS 禁止非空断言,统一在这里做 null 收敛 ----
-
-  private hasManifest(): boolean {
-    return this.detail.author.length > 0 || this.detail.license.length > 0 ||
-      this.detail.homepage.length > 0 || this.detail.repository.length > 0 ||
-      this.detail.entry.length > 0 || this.detail.minVersion.length > 0;
-  }
-
-  private activeIsBuiltin(): boolean {
-    const r: PluginRow | null = this.activeRow();
-    return r !== null ? !r.external : false;
-  }
-
-  private activeIsDisabled(): boolean {
-    const r: PluginRow | null = this.activeRow();
-    return r !== null ? r.disabled : false;
-  }
-
-  private activeTools(): string[] {
-    const r: PluginRow | null = this.activeRow();
-    if (r === null) {
-      return [];
-    }
-    return r.tools ?? [];
-  }
-
-  private activeStatusText(): string {
-    const r: PluginRow | null = this.activeRow();
-    return r !== null ? this.statusBadgeText(r) : '未加载';
-  }
-
-  private activeStatusColor(): string {
-    const r: PluginRow | null = this.activeRow();
-    return r !== null ? this.statusBadgeColor(r) : this.palette().textMuted;
-  }
-
-  /** 明细行:值为空时整行不渲染,避免详情页出现一排 "-" */
-  @Builder
-  KvRow(label: string, value: string) {
-    if (value.length > 0) {
-      Row() {
-        Text(label)
-          .fontSize(13)
-          .fontColor(this.palette().textSecondary)
-          .layoutWeight(1)
-        Text(value)
-          .fontSize(13)
-          .fontColor(this.palette().textPrimary)
-          .textAlign(TextAlign.End)
-          .maxLines(2)
-          .textOverflow({ overflow: TextOverflow.Ellipsis })
-          .constraintSize({ maxWidth: 220 })
-          .margin({ left: 16 })
-      }
-      .width('100%')
-      .padding({ top: 8, bottom: 8 })
-      .alignItems(VerticalAlign.Top)
-    }
-  }
-
-  @Builder
-  Toast() {
-    if (this.toastMsg.length > 0) {
-      Row() {
-        Text(this.toastMsg)
-          .fontSize(13)
-          .fontColor(this.toastIsError ? this.palette().toastErrorText : this.palette().toastText)
-          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
-          .borderRadius(RADIUS_MD)
-          .backgroundColor(this.toastIsError ? this.palette().toastErrorBg : this.palette().toastBg)
-          .border({
-            width: 1,
-            color: this.toastIsError ? 'rgba(232, 64, 38, 0.3)' : 'rgba(23, 169, 100, 0.3)',
-          })
-      }
-      .width('100%')
-      .justifyContent(FlexAlign.End)
-      .padding({ right: 20 })
-      .margin({ bottom: 166 })
-      .transition(TransitionEffect.OPACITY
-        .combine(TransitionEffect.translate({ y: 12 }))
-        .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }))
-    }
-  }
-
-  /** 状态点颜色:绿=已加载,黄=待生效,红=已禁用,灰=未加载 */
-  private statusDotColor(plugin: PluginRow): string {
-    return this.statusBadgeColor(plugin);
-  }
-
-  /** 列表行副标题:状态 + 内置/外部 + 工具数,一行灰字,不用徽标 */
-  private rowSubtitle(plugin: PluginRow): string {
-    const parts: string[] = [];
-    parts.push(this.statusBadgeText(plugin));
-    parts.push(plugin.external ? '外部' : '内置');
-    if (plugin.tools !== undefined && plugin.tools.length > 0) {
-      parts.push(plugin.tools.length.toString() + ' 工具');
-    }
-    return parts.join(' · ');
-  }
-
-  private statusBadgeText(plugin: PluginRow): string {
-    const s: string = this.statusOf(plugin);
-    if (s === 'loaded') {
-      return '已加载';
-    }
-    if (s === 'pending') {
-      return '待生效';
-    }
-    if (s === 'disabled') {
-      return '已禁用';
-    }
-    return '未加载';
-  }
-
-  private statusBadgeColor(plugin: PluginRow): string {
-    const s: string = this.statusOf(plugin);
-    if (s === 'loaded') {
-      return '#17A964';
-    }
-    if (s === 'pending') {
-      return '#D99A2B';
-    }
-    if (s === 'disabled') {
-      return '#E84026';
-    }
-    return this.palette().textMuted;
-  }
 }