diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets index 46b5fa2..012f72b 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets @@ -17,6 +17,18 @@ export const DEFAULT_WS_PORT: number = 9890; /** 聊天历史首屏条数:只拉最新 N 条,向上滚动触顶再加载更早的 */ export const CHAT_PAGE_SIZE: number = 40; +// ===== AppStorage 跨页面信号键 ===== +// +// 未连接后端时的“入口可达性”靠这三个键串起来:聊天空态按钮 → 切主 Tab → +// 设置页打开连接二级页。不用组件回调是因为按钮与目标分属不同的 Swiper 子页, +// 中间还隔着 Index,逐层传回调会把两个无关页面耦在一起。 +/** 当前是否已配置并激活后端连接(空态/入口的响应式判断) */ +export const K_HAS_CONN: string = 'hasConn'; +/** 外部请求切换主 Tab(-1 = 无请求),由 Index 监听 */ +export const K_REQUESTED_TAB: string = 'requestedTab'; +/** 请求设置页打开某个二级页(空串 = 无请求),由 SettingsPage 监听 */ +export const K_SETTINGS_SUB: string = 'settingsSubRequest'; + // ===== sakura / frost palette (style.css :root) ===== export const COLOR_SAKURA_100: string = 'rgba(10, 89, 247, 0.1)'; export const COLOR_SAKURA_200: string = 'rgba(10, 89, 247, 0.16)'; diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ChatStream.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ChatStream.ets index 97b596f..5228eed 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ChatStream.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ChatStream.ets @@ -8,7 +8,8 @@ */ import { ChatMessage } from '../model/Model'; -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, ANIM_FAST } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, ANIM_FAST, K_HAS_CONN, K_REQUESTED_TAB, K_SETTINGS_SUB } from '../common/Constants'; +import { SUB_CONNECTIONS } from '../common/SettingsModel'; import { navBar } from '../common/NavBarController'; import { ChatAttachment } from '../model/Model'; import { chatStore, K_CHAT_REV, K_CHAT_SCROLL_REV, K_CHAT_LOADING, K_CHAT_STAGE } from '../common/ChatStore'; @@ -20,6 +21,8 @@ import { PageTopBar } from './PageTopBar'; @Component export struct ChatStream { @StorageProp('themeIsDark') private isDark: boolean = true; + /** 是否已配置后端连接(决定空态是引导连接还是引导开聊) */ + @StorageProp(K_HAS_CONN) private hasConn: boolean = false; @StorageProp(K_CHAT_LOADING) private loading: boolean = false; @StorageProp(K_CHAT_STAGE) private stage: string = ''; /** 数组快照的订阅信号 */ @@ -186,6 +189,50 @@ export struct ChatStream { .width('100%') .height('100%') + // 层1.05:空态 —— 未连接后端时给出明确的“去设置连接”入口。 + // + // 为什么必须有:全新安装时聊天页只有一条空列表 + 输入框,用户看不到 + // 任何连后端的入口(入口在设置页的二级页里,很容易找不到)。 + if (this.messages.length === 0 && !this.loading) { + Column({ space: 10 }) { + Image($r('app.media.ic_link')) + .width(34) + .height(34) + .fillColor(this.palette().textMuted) + .draggable(false) + Text(this.hasConn ? '开始新的对话' : '尚未连接后端服务') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor(this.palette().textPrimary) + Text(this.hasConn + ? '在下方输入框发送第一条消息' + : '请先在“后端连接”里填写服务地址与 API Key') + .fontSize(12) + .fontColor(this.palette().textMuted) + .textAlign(TextAlign.Center) + if (!this.hasConn) { + Button('去设置连接') + .height(34) + .fontSize(13) + .backgroundColor(this.palette().accent) + .fontColor(Color.White) + .margin({ top: 4 }) + .onClick(() => { + // 跨页信号:切到设置 Tab,并让设置页直接打开连接二级页 + AppStorage.setOrCreate(K_SETTINGS_SUB, SUB_CONNECTIONS); + AppStorage.setOrCreate(K_REQUESTED_TAB, 3); + }) + } + } + .width('100%') + .height('100%') + .padding({ left: 44, right: 44 }) + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + // 自身不吃触摸(空白处仍可滑列表),但子节点(按钮)正常响应 + .hitTestBehavior(HitTestMode.Transparent) + } + // 层1.5:顶栏遮罩(自身撑满并顶部对齐,全链路 hitTest None,触摸完全穿透) PageTopBar({ title: '聊天' }) diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ConnectionsPane.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ConnectionsPane.ets index 4d0279f..3175970 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ConnectionsPane.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ConnectionsPane.ets @@ -13,7 +13,7 @@ import { apiClient } from '../common/ApiClient'; import { connStore } from '../common/ConnStore'; import { restartForegroundBridge } from '../common/DeviceBridgeSession'; import { ConnectionConfig } from '../model/Model'; -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_MD, RADIUS_SM } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_MD, RADIUS_SM, K_HAS_CONN } from '../common/Constants'; import { SubPageLayer, PlainCard } from './SubPage'; import { common } from '@kit.AbilityKit'; @@ -49,6 +49,11 @@ export struct ConnectionsPane { } } + /** 连接变更后广播状态:聊天空态据此隐藏“去设置连接”入口。 */ + private syncConnFlag(): void { + AppStorage.setOrCreate(K_HAS_CONN, apiClient.hasConnection()); + } + private currentConnName(): string { for (let i = 0; i < this.connections.length; i++) { if (this.connections[i].id === this.currentId) { @@ -64,6 +69,7 @@ export struct ConnectionsPane { if (cur !== null) { apiClient.setConnection(cur); } + this.syncConnFlag(); this.connections = connStore.getConnections(); restartForegroundBridge(getContext(this) as common.UIAbilityContext); this.toast('已切换连接', false); @@ -88,6 +94,7 @@ export struct ConnectionsPane { if (cur !== null) { apiClient.setConnection(cur); } + this.syncConnFlag(); this.connections = connStore.getConnections(); restartForegroundBridge(getContext(this) as common.UIAbilityContext); this.toast('连接已添加', false); @@ -107,6 +114,7 @@ export struct ConnectionsPane { if (cur !== null) { apiClient.setConnection(cur); } + this.syncConnFlag(); this.connections = connStore.getConnections(); restartForegroundBridge(getContext(this) as common.UIAbilityContext); this.toast('连接已更新', false); @@ -151,6 +159,7 @@ export struct ConnectionsPane { } else { apiClient.clearConnection(); } + this.syncConnFlag(); restartForegroundBridge(getContext(this) as common.UIAbilityContext); this.toast('连接已删除', false); }); diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets index b2143ba..7e2811a 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets @@ -7,7 +7,7 @@ import { apiClient } from '../common/ApiClient'; import { navBar } from '../common/NavBarController'; import { handleBackPress } from '../common/NavStackRegistry'; import { ConnectionConfig } from '../model/Model'; -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_MIN_WIDTH, WIDE_NAV_BAR_WIDTH } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_MIN_WIDTH, WIDE_NAV_BAR_WIDTH, K_HAS_CONN, K_REQUESTED_TAB, K_SETTINGS_SUB } from '../common/Constants'; import { ANIM_NORMAL, ANIM_SLOW } from '../common/Constants'; import { MotionBase } from '../components/MotionBase'; import { GradientBackground } from '../components/GradientBackground'; @@ -83,6 +83,8 @@ struct Index { /** 底部手势条高度(vp) */ @State bottomGesture: number = 16; @StorageProp('themeIsDark') @Watch('onThemeChanged') private isDark: boolean = true; + /** 外部请求切换主 Tab(未连接时聊天空态的“去设置连接”用) */ + @StorageProp(K_REQUESTED_TAB) @Watch('onRequestedTab') private requestedTab: number = -1; private swiper: SwiperController = new SwiperController(); private screensueTimer: number = -1; private snapshotBuilder: CustomBuilder = (): void => { }; // 由 @Builder 传入的实际锚点 @@ -101,6 +103,12 @@ struct Index { if (cur !== null) { apiClient.setConnection(cur); } + // 后端连接状态广播:聊天空态根据它决定是否显示“去设置连接”。 + // Index.aboutToAppear 在 EntryAbility 等 connStore.init 之后才跑, + // 所以此处读到的连接状态就是真实的启动态。 + AppStorage.setOrCreate(K_HAS_CONN, apiClient.hasConnection()); + AppStorage.setOrCreate(K_REQUESTED_TAB, -1); + AppStorage.setOrCreate(K_SETTINGS_SUB, ''); // 种子化自定义背景图状态到 AppStorage,GradientBackground 响应读取 const st = connStore.getSettings(); AppStorage.setOrCreate('bgImage', st.bgImage ?? ''); @@ -191,6 +199,23 @@ struct Index { AppStorage.set('currentTab', this.currentTab); } + /** + * 响应外部切 Tab 请求(聊天空态的“去设置连接”)。 + * + * 为什么不能直接改 AppStorage 的 currentTab:Index 的 currentTab 是 + * @State,Swiper.index() 只认它;外部写 AppStorage 不会驱动 Swiper。 + * 所以用独立请求键 + @Watch 把请求转成自己的状态变更。 + */ + private onRequestedTab(): void { + const t: number = this.requestedTab; + AppStorage.set(K_REQUESTED_TAB, -1); + if (t < 0 || t >= this.tabs.length) { + return; + } + this.currentTab = t; + navBar.setVisible(true); + } + private syncSystemBar(): void { const dark: boolean = this.isDark; const bg: string = dark ? '#000000' : '#F1F3F5'; diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets index b1ccef3..fdf66fd 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets @@ -3,7 +3,7 @@ import { userMessage, noConnectionMessage } from '../common/UserError'; import { connStore } from '../common/ConnStore'; import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry'; import { ConnectionConfig, AppSettings } from '../model/Model'; -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT, K_HAS_CONN, K_SETTINGS_SUB } from '../common/Constants'; import { RADIUS_MD, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; import { SubPageLayer, markSubPageOpen, subPageParam } from '../components/SubPage'; import { StatusDetailContent } from '../components/StatusCards'; @@ -42,6 +42,10 @@ export struct SettingsPage { /** 二级页面导航栈:系统返回手势/三键返回直接作用于它 */ private navStack: NavPathStack = new NavPathStack(); + /** 未连接时是否已自动弹过连接页——避免用户关掉后又被 onNavigationModeChange 弹回来 */ + private autoOpenedConn: boolean = false; + /** 外部请求打开某个二级页(聊天空态的“去设置连接”用) */ + @StorageProp(K_SETTINGS_SUB) @Watch('onSubRequest') private subRequest: string = ''; // ===== backend key/value editor state ===== @State sections: SettingsSection[] = []; @@ -86,6 +90,39 @@ export struct SettingsPage { this.connections = connStore.getConnections(); const cur = connStore.getCurrentConnection(); this.currentId = cur !== null ? cur.id : ''; + // 广播连接状态:聊天空态据此显示“去设置连接” + AppStorage.setOrCreate(K_HAS_CONN, apiClient.hasConnection()); + // 可能从聊天空态带着“打开连接页”的请求进来(本页尚未挂载时请求已写入) + const pending: string = AppStorage.get(K_SETTINGS_SUB) ?? ''; + if (pending.length > 0) { + AppStorage.set(K_SETTINGS_SUB, ''); + this.autoOpenedConn = true; + setTimeout(() => { + this.openSub(pending); + }, 0); + } + } + + /** + * 宽屏右栏默认该展示哪一页:没连上就把“后端连接”给出来。 + * + * 为什么不能只靠一级入口行:入口行在列表里,用户很容易略过; + * 而“未连接”恰恰是最需要直接看到连接表单的时刻。窄屏同理, + * 在 onNavigationModeChange(Stack) 里会把连接页直接推到面前。 + */ + private initialSub(): string { + return apiClient.hasConnection() ? SUB_STATUS : SUB_CONNECTIONS; + } + + /** 外部请求打开二级页(“去设置连接”) */ + private onSubRequest(): void { + const id: string = this.subRequest; + if (id.length === 0) { + return; + } + AppStorage.set(K_SETTINGS_SUB, ''); + this.autoOpenedConn = true; + this.openSub(id); } private palette(): ThemePalette { @@ -287,12 +324,17 @@ export struct SettingsPage { if (mode === NavigationMode.Split) { markSubPageOpen(true); if (this.navStack.size() === 0) { - this.openSub(SUB_STATUS); + this.openSub(this.initialSub()); } } else { this.navStack.clear(false); this.activeSub = SUB_NONE; markSubPageOpen(false); + // 窄屏:未配置后端时直接推连接页,保证“设置里一定能找到连后端的入口”。 + if (!this.autoOpenedConn && !apiClient.hasConnection()) { + this.autoOpenedConn = true; + this.openSub(SUB_CONNECTIONS); + } } }) }