/* * AgentMail 鸿蒙客户端 — 主框架(底部 Tab 导航) * 收件箱 / 会话 / 联系人 三个 TabContent */ import { ApiClient, ApiError } from '../api/ApiClient'; import { MailApi, InboxResponse } from '../api/MailApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { SseService, SseEvent } from '../api/SseService'; import { MailSummary, Session, Contact } from '../model/Models'; import { MailDetailParams, ComposeParams } from '../model/RouteParams'; import { promptAction } from '@kit.ArkUI'; @Component struct InboxTab { @State mails: MailSummary[] = []; @State loading: boolean = false; @State total: number = 0; @State unread: number = 0; @State error: string = ''; @State accountName: string = ''; @State accountFilter: string = 'all'; // 'all' 或 accountId @State showAccountPicker: boolean = false; @State accountList: AccountInfo[] = []; private sseService: SseService | null = null; aboutToAppear(): void { const ctx = this.getUIContext().getHostContext(); if (ctx !== undefined) { this.sseService = SseService.getInstance(); // 加载账号列表 const acctMgr: AccountManager = AccountManager.getInstance(ctx); acctMgr.load().then(() => { const active = acctMgr.getActiveAccount(); if (active !== null) { this.accountName = active.displayName; } this.accountList = acctMgr.getAccounts(); if (this.accountList.length <= 1) { this.accountFilter = 'all'; } }); this.loadData(); // 全局 SSE 监听(所有账号的事件都会收到);保存同一函数引用以便页面退出时移除。 this.sseService.addListener(this.onSseEvent); } } aboutToDisappear(): void { if (this.sseService !== null) { this.sseService.removeListener(this.onSseEvent); } } private onSseEvent = (event: SseEvent): void => { if (event.type === 'new_mail') { let sourceName: string = ''; for (let i = 0; i < this.accountList.length; i++) { if (this.accountList[i].id === event.accountId) { sourceName = this.accountList[i].displayName; break; } } promptAction.showToast({ message: sourceName.length > 0 ? '📨 ' + sourceName + ' 收到新邮件' : '📨 新邮件到达' }); this.loadData(); } }; async loadData(): Promise { const ctx = this.getUIContext().getHostContext(); if (ctx === undefined) { return; } this.loading = true; this.error = ''; try { const acctMgr: AccountManager = AccountManager.getInstance(ctx); await acctMgr.load(); const allAccounts: AccountInfo[] = acctMgr.getAccounts(); this.accountList = allAccounts; let filterExists: boolean = this.accountFilter === 'all'; for (let i = 0; i < allAccounts.length; i++) { if (allAccounts[i].id === this.accountFilter) { filterExists = true; this.accountName = allAccounts[i].displayName; break; } } if (!filterExists) { this.accountFilter = 'all'; this.accountName = '全部邮箱'; } const mergedMails: MailSummary[] = []; let mergedTotal: number = 0; for (let i = 0; i < allAccounts.length; i++) { const acct: AccountInfo = allAccounts[i]; if (this.accountFilter !== 'all' && this.accountFilter !== acct.id) { continue; } try { const accountClient: ApiClient = new ApiClient(ctx); accountClient.setBase(acct.server); accountClient.setToken(acct.token); const response: InboxResponse = await new MailApi(accountClient).inbox('all', 50); for (let j = 0; j < response.mails.length; j++) { const mail: MailSummary = response.mails[j]; mail.source_account_id = acct.id; mail.source_account_name = acct.displayName; mergedMails.push(mail); } mergedTotal += response.total; } catch (e) { if (this.accountFilter !== 'all') { throw e as ApiError; } } } mergedMails.sort((a: MailSummary, b: MailSummary) => { if (a.created_at > b.created_at) { return -1; } if (a.created_at < b.created_at) { return 1; } return 0; }); this.mails = mergedMails; this.total = mergedTotal; let unreadCount: number = 0; for (let i = 0; i < this.mails.length; i++) { if (this.mails[i].status === 'unread') { unreadCount++; } } this.unread = unreadCount; } catch (e) { const ae = e as ApiError; this.error = ae.message.length > 0 ? ae.message : '加载失败'; } finally { this.loading = false; } } openCompose(): void { let accountId: string = this.accountFilter === 'all' ? '' : this.accountFilter; if (accountId.length === 0) { const ctx = this.getUIContext().getHostContext(); if (ctx !== undefined) { accountId = AccountManager.getInstance(ctx).getActiveId(); } } const params: ComposeParams = { to: '', reply_to: '', session_alias: '', account_id: accountId }; this.getUIContext().getRouter().pushUrl({ url: 'pages/ComposePage', params: params }); } build() { Column() { Row() { Column() { Row() { Text('收件箱').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') if (this.accountList.length > 1) { Text(' ▾').fontSize(14).fontColor('#999999') .onClick(() => { this.showAccountPicker = !this.showAccountPicker; }) } } Text(this.accountFilter === 'all' ? '全部邮箱' : this.accountName) .fontSize(11).fontColor('#999999').margin({ top: 2 }) .onClick(() => { if (this.accountList.length > 1) { this.showAccountPicker = !this.showAccountPicker; } }) } .alignItems(HorizontalAlign.Start) Blank() if (this.unread > 0) { Text(this.unread + ' 未读') .fontSize(13).fontColor('#FFFFFF') .backgroundColor('#FF4444') .borderRadius(10) .padding({ left: 8, right: 8, top: 2, bottom: 2 }) } Text('⚙') .fontSize(20).fontColor('#666666') .width(36).height(36).textAlign(TextAlign.Center) .onClick(() => { this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' }); }) } .width('100%').height(56).padding({ left: 16, right: 8 }) .backgroundColor('#FFFFFF') // 账号选择器下拉 if (this.showAccountPicker && this.accountList.length > 1) { Column() { Text('全部邮箱') .fontSize(14).fontColor(this.accountFilter === 'all' ? '#1A73E8' : '#333333') .fontWeight(this.accountFilter === 'all' ? FontWeight.Bold : FontWeight.Normal) .width('100%').height(40).padding({ left: 16 }) .backgroundColor(this.accountFilter === 'all' ? '#F0F7FF' : '#FFFFFF') .onClick(() => { this.accountFilter = 'all'; this.accountName = '全部邮箱'; this.showAccountPicker = false; this.loadData(); }) ForEach(this.accountList, (acct: AccountInfo) => { Row() { Column() { Text(acct.displayName) .fontSize(14) .fontColor(this.accountFilter === acct.id ? '#1A73E8' : '#333333') .fontWeight(this.accountFilter === acct.id ? FontWeight.Bold : FontWeight.Normal) Text(acct.username) .fontSize(11).fontColor('#999999') } .alignItems(HorizontalAlign.Start) .layoutWeight(1) } .width('100%').height(48).padding({ left: 16 }) .backgroundColor(this.accountFilter === acct.id ? '#F0F7FF' : '#FFFFFF') .onClick(() => { this.accountFilter = acct.id; this.accountName = acct.displayName; this.showAccountPicker = false; this.loadData(); }) }, (acct: AccountInfo) => acct.id) } .width('100%') .backgroundColor('#FFFFFF') .border({ width: { bottom: 1 }, color: '#EEEEEE' }) } if (this.loading) { Column() { LoadingProgress().width(40).height(40) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { Text(this.error).fontSize(14).fontColor('#FF4444') Button('重试').margin({ top: 12 }).onClick(() => { this.loadData(); }) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.mails.length === 0) { Column() { Text('📭 收件箱为空').fontSize(16).fontColor('#999999') } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else { List({ space: 1 }) { ForEach(this.mails, (mail: MailSummary) => { ListItem() { this.MailItem(mail) } .height(80) .backgroundColor(mail.status === 'unread' ? '#F0F7FF' : '#FFFFFF') .onClick(() => { const params: MailDetailParams = { mail_id: mail.mail_id, account_id: mail.source_account_id }; this.getUIContext().getRouter().pushUrl({ url: 'pages/MailDetailPage', params: params }); }) }, (mail: MailSummary) => mail.source_account_id + ':' + mail.mail_id) } .width('100%').layoutWeight(1) .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) } // 底部信息栏 + 悬浮写邮件按钮 Stack({ alignContent: Alignment.BottomEnd }) { Row() { Text('共 ' + this.total + ' 封').fontSize(12).fontColor('#999999') Blank() Text('未读 ' + this.unread).fontSize(12).fontColor('#999999') } .width('100%').height(36).padding({ left: 16, right: 16 }) .backgroundColor('#FFFFFF') Text('+') .fontSize(28).fontColor('#FFFFFF') .width(56).height(56) .borderRadius(28) .backgroundColor('#1A73E8') .textAlign(TextAlign.Center) .margin({ right: 20, bottom: 44 }) .onClick(() => { this.openCompose(); }) } .width('100%') } .width('100%').height('100%') .backgroundColor('#F5F7FA') .bindSheet($$this.composeVisible, this.ComposeSheet()) } @State composeVisible: boolean = false; @Builder ComposeSheet() { Column() { Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') .margin({ top: 16, bottom: 8 }) Button('新建邮件') .width('90%').height(44).backgroundColor('#1A73E8') .onClick(() => { this.composeVisible = false; this.openCompose(); }) Button('取消') .width('90%').height(40).backgroundColor('#F5F5F5').fontColor('#666666') .margin({ top: 8 }) .onClick(() => { this.composeVisible = false; }) } .width('100%').height(180).padding(16) } @Builder MailItem(mail: MailSummary) { Row() { Column() { if (mail.status === 'unread') { Circle({ width: 8, height: 8 }).fill('#1A73E8') } } .width(20).height('100%').justifyContent(FlexAlign.Center) Column() { Row() { Text(mail.from_name).fontSize(14).fontWeight(mail.status === 'unread' ? FontWeight.Bold : FontWeight.Normal).fontColor('#333333') if (this.accountFilter === 'all' && mail.source_account_name.length > 0) { Text(mail.source_account_name) .fontSize(10).fontColor('#1A73E8') .backgroundColor('#E8F0FE').borderRadius(4) .padding({ left: 5, right: 5, top: 1, bottom: 1 }) .margin({ left: 6 }) } Blank() Text(mail.created_at).fontSize(11).fontColor('#999999') } .width('100%') Text(mail.subject).fontSize(13).fontColor('#333333') .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) Text(mail.body_preview).fontSize(12).fontColor('#999999') .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) } .layoutWeight(1).height('100%') .alignItems(HorizontalAlign.Start) .padding({ left: 8 }) if (mail.permission_mode.length > 0) { Text(mail.permission_mode) .fontSize(10).fontColor('#666666') .backgroundColor('#EEEEEE').borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) } } .width('100%').height('100%') .padding({ left: 12, right: 12 }) .alignItems(VerticalAlign.Center) } } @Component struct SessionsTab { @State sessions: Session[] = []; @State loading: boolean = false; @State error: string = ''; private mailApi: MailApi | null = null; aboutToAppear(): void { const ctx = this.getUIContext().getHostContext(); if (ctx !== undefined) { this.mailApi = new MailApi(ApiClient.getInstance(ctx)); this.loadData(); } } async loadData(): Promise { const m: MailApi | null = this.mailApi; if (m === null) { return; } this.loading = true; try { const resp = await m.sessions(); this.sessions = resp.sessions; } catch (e) { const ae = e as ApiError; this.error = ae.message.length > 0 ? ae.message : '加载失败'; } finally { this.loading = false; } } build() { Column() { Row() { Text('会话').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') } .width('100%').height(56).padding({ left: 16 }) .backgroundColor('#FFFFFF') if (this.loading) { Column() { LoadingProgress().width(40).height(40) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { Text(this.error).fontSize(14).fontColor('#FF4444') } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.sessions.length === 0) { Column() { Text('📭 暂无会话').fontSize(16).fontColor('#999999') } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else { List({ space: 1 }) { ForEach(this.sessions, (s: Session, idx: number) => { ListItem() { this.SessionItem(s, idx) } .height(90) }, (_s: Session, idx: number) => idx.toString()) } .width('100%').layoutWeight(1) .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) } } .width('100%').height('100%') .backgroundColor('#F5F7FA') } @Builder SessionItem(s: Session, idx: number) { Row() { Column() { Row() { Text(s.session_alias.length > 0 ? s.session_alias : ('会话 #' + (idx + 1))) .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333') .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) if (s.max_rounds > 0) { Text(s.used_rounds + '/' + s.max_rounds) .fontSize(11).fontColor(s.used_rounds >= s.max_rounds ? '#FF4444' : '#666666') .backgroundColor(s.used_rounds >= s.max_rounds ? '#FFEBEE' : '#F5F5F5') .borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 }) } } .width('100%') Text('对方: ' + s.from_agent).fontSize(12).fontColor('#666666').margin({ top: 4 }) Row() { Text(s.permission_mode.length > 0 ? s.permission_mode : '—') .fontSize(11).fontColor('#666666') .backgroundColor('#F0F0F0').borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) Blank() Text(s.mail_count + ' 封').fontSize(11).fontColor('#999999') Text(' · ' + s.status).fontSize(11).fontColor(s.status === 'active' ? '#4CAF50' : '#999999') } .width('100%').margin({ top: 4 }) } .layoutWeight(1).height('100%') .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.SpaceBetween) } .width('100%').height('100%') .padding({ left: 16, right: 16, top: 12, bottom: 12 }) .alignItems(VerticalAlign.Center) } } @Component struct ContactsTab { @State contacts: Contact[] = []; @State loading: boolean = false; @State error: string = ''; private mailApi: MailApi | null = null; aboutToAppear(): void { const ctx = this.getUIContext().getHostContext(); if (ctx !== undefined) { this.mailApi = new MailApi(ApiClient.getInstance(ctx)); this.loadData(); } } async loadData(): Promise { const m: MailApi | null = this.mailApi; if (m === null) { return; } this.loading = true; try { const resp = await m.contacts(); this.contacts = resp.contacts; } catch (e) { const ae = e as ApiError; this.error = ae.message.length > 0 ? ae.message : '加载失败'; } finally { this.loading = false; } } build() { Column() { Row() { Text('联系人').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') } .width('100%').height(56).padding({ left: 16 }) .backgroundColor('#FFFFFF') if (this.loading) { Column() { LoadingProgress().width(40).height(40) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { Text(this.error).fontSize(14).fontColor('#FF4444') Button('重试').margin({ top: 12 }).onClick(() => { this.loadData(); }) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.contacts.length === 0) { Column() { Text('📭 暂无联系人').fontSize(16).fontColor('#999999') } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else { List({ space: 1 }) { ForEach(this.contacts, (c: Contact, idx: number) => { ListItem() { this.ContactItem(c, idx) } .height(85) }, (_c: Contact, idx: number) => idx.toString()) } .width('100%').layoutWeight(1) .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) } } .width('100%').height('100%') .backgroundColor('#F5F7FA') } @Builder ContactItem(c: Contact, idx: number) { Row() { Column() { Row() { Text(c.agent_name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333') Blank() if (c.unread_count > 0) { Text(c.unread_count + '') .fontSize(11).fontColor('#FFFFFF') .backgroundColor('#FF4444') .borderRadius(10).width(20).height(20) .textAlign(TextAlign.Center) } } .width('100%') Text(c.subject.length > 0 ? c.subject : c.session_alias) .fontSize(12).fontColor('#666666') .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 3 }) Row() { Text(c.permission_mode.length > 0 ? c.permission_mode : '—') .fontSize(10).fontColor('#666666') .backgroundColor('#F0F0F0').borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) Blank() Text(c.last_preview.length > 0 ? c.last_preview : '—') .fontSize(11).fontColor('#999999') .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) } .width('100%').margin({ top: 3 }) } .layoutWeight(1).height('100%') .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.SpaceBetween) } .width('100%').height('100%') .padding({ left: 16, right: 16, top: 10, bottom: 10 }) .alignItems(VerticalAlign.Center) } } @Entry @Component struct MainPage { @State currentIndex: number = 0; @Builder TabBarBuilder(title: string, icon: string) { Column() { Text(icon).fontSize(20) Text(title).fontSize(11).fontColor(this.currentIndex === 0 ? '#1A73E8' : '#666666') } .width('100%') .justifyContent(FlexAlign.Center) } build() { Tabs({ barPosition: BarPosition.End }) { TabContent() { InboxTab() } .tabBar(this.TabBarBuilder('收件箱', '📬')) TabContent() { SessionsTab() } .tabBar(this.TabBarBuilder('会话', '💬')) TabContent() { ContactsTab() } .tabBar(this.TabBarBuilder('联系人', '👤')) } .onChange((index: number) => { this.currentIndex = index; }) .width('100%') .height('100%') } }