mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-24 19:08:10 +00:00
fix(ohos): 未连接后端时给出不可错过的连接入口
问题:全新安装(未配置后端)时,聊天页只有空列表+输入框,用户找不到 任何连后端的入口;设置页的连接入口在列表里也容易被略过。 改动: - 聊天空态按连接状态分流:未连接显示「尚未连接后端服务」+「去设置连接」 按钮;已连接显示「开始新的对话」。 - 跨页信号(AppStorage:K_HAS_CONN / K_REQUESTED_TAB / K_SETTINGS_SUB): 按钮 → Index 切到设置 Tab → SettingsPage 直接打开「后端连接」二级页。 - 设置页在未连接时主动把连接表单推到面前:窄屏 onNavigationModeChange(Stack) 直接 push,宽屏右栏默认页从「运行状态」改为「后端连接」。 - 连接增删改切后广播 K_HAS_CONN,聊天空态即时切换文案与入口。 已在手机(窄屏)与折叠展开(宽屏)模拟器验证:空态按钮可达、点击后 落到带「+ 添加」的连接表单;冷启动点设置 Tab 亦自动打开连接页。
This commit is contained in:
@ -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)';
|
||||
|
||||
@ -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<string>(K_SETTINGS_SUB, SUB_CONNECTIONS);
|
||||
AppStorage.setOrCreate<number>(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: '聊天' })
|
||||
|
||||
|
||||
@ -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<boolean>(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);
|
||||
});
|
||||
|
||||
@ -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<boolean>(K_HAS_CONN, apiClient.hasConnection());
|
||||
AppStorage.setOrCreate<number>(K_REQUESTED_TAB, -1);
|
||||
AppStorage.setOrCreate<string>(K_SETTINGS_SUB, '');
|
||||
// 种子化自定义背景图状态到 AppStorage,GradientBackground 响应读取
|
||||
const st = connStore.getSettings();
|
||||
AppStorage.setOrCreate<string>('bgImage', st.bgImage ?? '');
|
||||
@ -191,6 +199,23 @@ struct Index {
|
||||
AppStorage.set<number>('currentTab', this.currentTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应外部切 Tab 请求(聊天空态的“去设置连接”)。
|
||||
*
|
||||
* 为什么不能直接改 AppStorage 的 currentTab:Index 的 currentTab 是
|
||||
* @State,Swiper.index() 只认它;外部写 AppStorage 不会驱动 Swiper。
|
||||
* 所以用独立请求键 + @Watch 把请求转成自己的状态变更。
|
||||
*/
|
||||
private onRequestedTab(): void {
|
||||
const t: number = this.requestedTab;
|
||||
AppStorage.set<number>(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';
|
||||
|
||||
@ -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<boolean>(K_HAS_CONN, apiClient.hasConnection());
|
||||
// 可能从聊天空态带着“打开连接页”的请求进来(本页尚未挂载时请求已写入)
|
||||
const pending: string = AppStorage.get<string>(K_SETTINGS_SUB) ?? '';
|
||||
if (pending.length > 0) {
|
||||
AppStorage.set<string>(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<string>(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);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user