From 7103675db89b322dd17eeb6adc8a96f7d9adb718 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Fri, 11 Sep 2026 11:02:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=8A=B6=E6=80=81=E8=83=B6=E5=9B=8A?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E2=80=94=E2=80=94=E9=A1=B6=E6=A0=8F=E7=B4=A7?= =?UTF-8?q?=E5=87=91=E7=8A=B6=E6=80=81=E6=9D=A1=20+=20=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E5=AE=BF=E4=B8=BB=E6=B8=B2=E6=9F=93=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=BC=B9=E5=B1=82=EF=BC=88StatusItem.icon/detail?= =?UTF-8?q?=EF=BC=9A=E5=88=86=E7=BB=84/=E8=A1=8C/=E5=BE=BD=E6=A0=87/?= =?UTF-8?q?=E8=A1=8C=E5=86=85=E6=B7=B1=E9=93=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/plugins/PluginStatusCapsules.tsx | 128 +++++++++++++++--- docs/plugins.md | 2 +- docs/plugins/04-ui-slots.md | 36 ++++- packages/plugin-sdk/src/status.ts | 42 +++++- packages/plugin-template/main.mjs | 16 +++ 5 files changed, 202 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx b/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx index 61dbb4d..4749160 100644 --- a/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx +++ b/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx @@ -1,9 +1,15 @@ /** * 插件状态胶囊(TitleBar B2 槽位):数据驱动渲染主进程广播的状态条目。 - * 点击条目 = 执行其深链(openView)。聊天视图与插件视图下均可见。 + * 顶栏胶囊只是紧凑状态条;点击行为分三档: + * 1. 声明了 detail → 展开宿主渲染的详情弹层(分组列表,行内数据再负责深链) + * 2. 声明了 openView → 执行深链 + * 3. 都没有 → 纯展示 + * 聊天视图与插件视图下均可见。弹层点击外部/Esc 关闭。 */ +import { useEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' import type { PluginStatusEntry } from '@shared/types' +import { PluginIcon } from './PluginPageFrame' const TONE_CLASS: Record = { default: 'border-line text-ink-2', @@ -12,6 +18,63 @@ const TONE_CLASS: Record = { danger: 'border-red/40 text-red bg-red-tint' } +const BADGE_TONE_CLASS: Record = { + default: 'bg-hover text-ink-3', + accent: 'bg-accent-tint text-accent-ink', + warn: 'bg-orange-tint text-orange', + danger: 'bg-red-tint text-red' +} + +/** 详情弹层:分组列表,行数据自带动线 */ +function StatusPopover({ entry, onOpenView }: { entry: PluginStatusEntry; onOpenView: (view: string, params?: unknown) => void }): ReactNode { + const { item } = entry + return ( +
+
{item.detail?.title ?? item.text}
+
+ {item.detail?.sections.map((section, si) => ( +
0 ? 'mt-1.5' : ''}> + {section.title &&
{section.title}
} + {section.rows.map((row, ri) => { + const clickable = Boolean(row.openView) + const content = ( + <> + {row.icon && } + + {row.label} + {row.detail && {row.detail}} + + {row.trailing && ( + + {row.trailing} + + )} + + ) + return clickable ? ( + + ) : ( +
+ {content} +
+ ) + })} +
+ ))} +
+
+ ) +} + export function PluginStatusCapsules({ entries, onOpenView @@ -19,26 +82,59 @@ export function PluginStatusCapsules({ entries: PluginStatusEntry[] onOpenView: (view: string, params?: unknown) => void }): ReactNode { + const [openKey, setOpenKey] = useState(null) + const rootRef = useRef(null) + + // 弹层开着时:点外部 / Esc 关闭;条目消失(插件停用/重载)也收起 + useEffect(() => { + if (!openKey) return + const onDown = (e: MouseEvent): void => { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpenKey(null) + } + const onKey = (e: KeyboardEvent): void => { + if (e.key === 'Escape') setOpenKey(null) + } + document.addEventListener('mousedown', onDown) + document.addEventListener('keydown', onKey) + return () => { + document.removeEventListener('mousedown', onDown) + document.removeEventListener('keydown', onKey) + } + }, [openKey]) + + // 条目集合变化(停用/重载/删除)时收起弹层;按 key 序列比较,避免引用变化误关 + const entryKeys = entries.map((e) => `${e.pluginId}:${e.itemId}`).join(',') + useEffect(() => { + setOpenKey(null) + }, [entryKeys]) + if (entries.length === 0) return null return ( -
+
{entries.slice(0, 4).map(({ pluginId, itemId, item }) => { const tone = item.tone ?? 'default' - const clickable = Boolean(item.openView) + const key = `${pluginId}:${itemId}` + const expandable = Boolean(item.detail) + const clickable = expandable || Boolean(item.openView) + const open = openKey === key return ( - +
+ + {open && item.detail && } +
) })} {entries.length > 4 && ( diff --git a/docs/plugins.md b/docs/plugins.md index 505f3ae..bc8420f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -170,7 +170,7 @@ api.ui.onSettingsAction((key) => {}) // button 条目点击回调(key = 按钮 | 侧栏入口(B1) | manifest icon/name/order;点进插件整页视图 | ✅ | | 插件整页 Main | 内置组件级 / 外置 renderer 组件 / 外置 iframe;聊天列 display:none 保活 | ✅ | | 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件 / 外置 renderer 组件 / iframe) | ✅ | -| 状态胶囊(B2) | `setStatusItem` 数据驱动,TitleBar 渲染,两种视图均可见 | ✅ | +| 状态胶囊(B2) | `setStatusItem` 数据驱动,TitleBar 渲染,两种视图均可见;顶栏为紧凑状态条,点击可展开宿主渲染的详情弹层(`detail`:分组/行/徽标/行内深链) | ✅ | | 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ | | 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 | | 聊天流富组件(B4) | ```类型``` 围栏块 → 宿主元组件(data-table/stat-grid/step-list/kv-card) | ⏳ 下版本 | diff --git a/docs/plugins/04-ui-slots.md b/docs/plugins/04-ui-slots.md index 07fac0d..814e08a 100644 --- a/docs/plugins/04-ui-slots.md +++ b/docs/plugins/04-ui-slots.md @@ -4,17 +4,47 @@ ## 1. 状态胶囊(TitleBar 顶部) -轻量状态提示,点击可深链跳转: +顶栏胶囊是**紧凑状态条**,点击行为三档: ```js +// ① 纯展示 +api.ui.setStatusItem('conn', { text: '已连接 · 3 任务' }) + +// ② 点击深链 api.ui.setStatusItem('conn', { text: '已连接 · 3 任务', tone: 'accent', // default | accent | warn | danger - openView: { view: 'my-plugin' } // 点击跳到插件整页视图(可省略) + icon: '', // 可选,12px 线条风 + openView: { view: 'my-plugin' } // 点击跳到插件整页视图 }) -api.ui.removeStatusItem('conn') + +// ③ 点击展开详情弹层(声明了 detail 时优先于 openView) +api.ui.setStatusItem('tasks', { + text: '任务 2/5', + tone: 'accent', + detail: { + title: '任务概览', // 缺省用胶囊文本 + sections: [ + { + title: '进行中', // 分组标题可省略 + rows: [ + { label: '构建索引', detail: '已完成 60%', trailing: '2/5', tone: 'accent' }, + { label: '清理临时文件' } + ] + }, + { + title: '操作', + rows: [{ label: '打开任务面板', openView: { view: 'my-plugin' } }] // 行点击深链 + } + ] + } +}) +api.ui.removeStatusItem('tasks') ``` +弹层由宿主统一渲染(分组标题 / 行 / 右侧徽标 / 深链),样式与宿主一致——插件只提交 +数据;点击弹层外部或 Esc 关闭;dispose 时宿主自动清理本插件全部条目。 + ## 2. 工具栏按钮(输入区工具行) ```js diff --git a/packages/plugin-sdk/src/status.ts b/packages/plugin-sdk/src/status.ts index bad8180..6cf6d18 100644 --- a/packages/plugin-sdk/src/status.ts +++ b/packages/plugin-sdk/src/status.ts @@ -10,12 +10,50 @@ export interface OpenViewAction { params?: unknown } +/** 徽标/胶囊共用色调语义 */ +export type StatusTone = 'default' | 'accent' | 'warn' | 'danger' + +/** 详情弹层里的一行(数据驱动;openView 缺省为纯展示行) */ +export interface StatusDetailRow { + /** svg 字符串(14px 线条风格,currentColor) */ + icon?: string + label: string + /** 次级说明(label 下方小字) */ + detail?: string + /** 右侧徽标文本(如 +76 -40、7/7、✓) */ + trailing?: string + /** 徽标色调,缺省 default */ + tone?: StatusTone + /** 整行点击的深链;缺省不可点击 */ + openView?: OpenViewAction +} + +/** 详情弹层分组 */ +export interface StatusDetailSection { + /** 分组标题(如「Git 工具」「计划」);缺省无标题 */ + title?: string + rows: StatusDetailRow[] +} + +/** 状态胶囊的展开详情:点击胶囊弹出宿主渲染的分组列表弹层。 + * 声明了 detail 的胶囊,点击 = 展开弹层(行内 openView 负责深链); + * 未声明的胶囊保持原行为:点击 = item.openView。 */ +export interface StatusDetail { + /** 弹层标题;缺省用胶囊文本 */ + title?: string + sections: StatusDetailSection[] +} + /** 状态胶囊条目(TitleBar 胶囊区 / 聊天区浮动胶囊共用一份数据) */ export interface StatusItem { text: string - tone?: 'default' | 'accent' | 'warn' | 'danger' - /** 点击行为:深链;缺省不可点击 */ + /** svg 字符串(12-14px 线条风格,currentColor;缺省纯文字胶囊) */ + icon?: string + tone?: StatusTone + /** 点击行为:深链;有 detail 时被弹层取代;两者都缺省不可点击 */ openView?: OpenViewAction + /** 点击展开的详情弹层(数据驱动,宿主统一渲染) */ + detail?: StatusDetail } /** 输入区工具行按钮 */ diff --git a/packages/plugin-template/main.mjs b/packages/plugin-template/main.mjs index d305f03..02ba98b 100644 --- a/packages/plugin-template/main.mjs +++ b/packages/plugin-template/main.mjs @@ -34,6 +34,22 @@ export default definePlugin((api) => { } }) + // ---- 状态胶囊示范:顶栏紧凑状态条,点击展开详情弹层(数据驱动,宿主统一渲染) ---- + api.ui.setStatusItem('summary', { + text: '示例 1/2', + tone: 'accent', + detail: { + sections: [ + { + rows: [ + { label: '示例任务', detail: '把这段换成真正的状态数据', trailing: '进行中', tone: 'accent' }, + { label: '打开面板', openView: { view: api.manifest.id } } + ] + } + ] + } + }) + // ---- 面板/视图组件在 renderer.mjs(JSX 构建见 README);无 UI 需求可整段去掉 ---- log.info('ready', { hasRenderer: true })