/* * AgentMail 鸿蒙客户端 — 写邮件页 * 收件人 / 主题 / 正文 / 附件选择 / 发送 * 路由参数可选: to, reply_to, session_alias */ import { ApiClient, ApiError } from '../api/ApiClient'; import { Theme } from '../common/Theme'; import { MailApi } from '../api/MailApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { SendMailRequest } from '../model/Models'; import { ComposeParams } from '../model/RouteParams'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { picker } from '@kit.CoreFileKit'; import { AmIcon } from '../common/Icons'; @Entry @Component struct ComposePage { @State to: string = ''; @State subject: string = ''; @State body: string = ''; @State replyTo: string = ''; @State sessionAlias: string = ''; @State attachmentIds: string[] = []; @State attachmentNames: string[] = []; @State sending: boolean = false; @State uploading: boolean = false; @State status: string = ''; // 发信账号选择 @State accountList: AccountInfo[] = []; @State selectedAccountId: string = ''; @State showAccountPicker: boolean = false; aboutToAppear(): void { const ctx = this.getUIContext().getHostContext(); if (ctx === undefined) { return; } const params = this.getUIContext().getRouter().getParams() as ComposeParams; const routeAccountId: string = params?.account_id ?? ''; this.to = params?.to ?? ''; this.replyTo = params?.reply_to ?? ''; this.sessionAlias = params?.session_alias ?? ''; const accountManager: AccountManager = AccountManager.getInstance(ctx); accountManager.load().then(() => { this.accountList = accountManager.getAccounts(); let selectedId: string = routeAccountId; if (accountManager.getAccount(selectedId) === null) { const active: AccountInfo | null = accountManager.getActiveAccount(); selectedId = active !== null ? active.id : ''; } this.selectedAccountId = selectedId; }); } getSelectedAccount(): AccountInfo | null { for (let i = 0; i < this.accountList.length; i++) { if (this.accountList[i].id === this.selectedAccountId) { return this.accountList[i]; } } return null; } createSelectedMailApi(ctx: Context): MailApi | null { const account: AccountInfo | null = this.getSelectedAccount(); if (account === null) { return null; } const accountClient: ApiClient = new ApiClient(ctx); accountClient.setBase(account.server); accountClient.setToken(account.token); return new MailApi(accountClient); } /** 选择附件(通过文件选择器) */ async pickAttachment(): Promise { const ctx: Context | undefined = this.getUIContext().getHostContext(); if (ctx === undefined || this.uploading) { return; } try { const documentPicker = new picker.DocumentViewPicker(ctx); const result: string[] = await documentPicker.select({ maxSelectNumber: 5, fileSuffixFilters: ['*'] }); if (result.length === 0) { return; } const mailApi: MailApi | null = this.createSelectedMailApi(ctx); if (mailApi === null) { this.getUIContext().getPromptAction().showToast({ message: '请先选择发信账号' }); return; } this.uploading = true; this.status = '上传中…'; for (let i = 0; i < result.length; i++) { const fileUri: string = result[i]; const nameStart: number = fileUri.lastIndexOf('/'); const name: string = nameStart >= 0 ? fileUri.substring(nameStart + 1) : fileUri; try { const attachmentId: string = await mailApi.uploadAttachment(fileUri, name); this.attachmentIds.push(attachmentId); this.attachmentNames.push(name); hilog.info(0x0001, 'Compose', 'uploaded: %{public}s → %{public}s', name, attachmentId); } catch (e) { const apiError = e as ApiError; this.getUIContext().getPromptAction().showToast({ message: '上传失败: ' + name + ' - ' + apiError.message }); } } this.status = this.attachmentIds.length + ' 个附件已上传'; } catch (e) { this.status = ''; } finally { this.uploading = false; } } /** 移除附件 */ removeAttachment(index: number): void { if (index >= 0 && index < this.attachmentIds.length) { this.attachmentIds.splice(index, 1); this.attachmentNames.splice(index, 1); } } /** 发送邮件 */ async doSend(): Promise { if (this.sending) { return; } if (this.to.length === 0) { this.getUIContext().getPromptAction().showToast({ message: '请填写收件人' }); return; } if (this.subject.length === 0) { this.getUIContext().getPromptAction().showToast({ message: '请填写主题' }); return; } const ctx: Context | undefined = this.getUIContext().getHostContext(); if (ctx === undefined) { return; } const mailApi: MailApi | null = this.createSelectedMailApi(ctx); if (mailApi === null) { this.getUIContext().getPromptAction().showToast({ message: '请先选择发信账号' }); return; } this.sending = true; try { const request: SendMailRequest = new SendMailRequest(); request.to = this.to; request.subject = this.subject; request.body = this.body; if (this.replyTo.length > 0) { request.reply_to = this.replyTo; } if (this.sessionAlias.length > 0) { request.session_alias = this.sessionAlias; } if (this.attachmentIds.length > 0) { request.attachment_ids = this.attachmentIds; } await mailApi.send(request); this.getUIContext().getPromptAction().showToast({ message: '邮件已发送' }); this.getUIContext().getRouter().back(); } catch (e) { const apiError = e as ApiError; this.getUIContext().getPromptAction().showToast({ message: '发送失败: ' + apiError.message }); } finally { this.sending = false; } } /** 获取选中的账号显示名 */ getSelectedAccountName(): string { const account: AccountInfo | null = this.getSelectedAccount(); return account !== null ? account.displayName : '选择账号'; } build() { Column() { // 顶栏 Row() { Text('取消') .fontSize(15).fontColor(Theme.danger) .onClick(() => { this.getUIContext().getRouter().back(); }) Blank() Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) Blank() Button(this.sending ? '发送中…' : '发送') .fontSize(13).height(32) .backgroundColor(Theme.accent) .enabled(!this.sending && !this.uploading) .onClick(() => { this.doSend(); }) } .width('100%').height(56) .padding({ left: 12, right: 12 }) .backgroundColor(Theme.surface) Divider().color(Theme.border) // 发信账号选择(多账号时显示) if (this.accountList.length > 1) { Row() { Text('发信账号').fontSize(13).fontColor(Theme.textSubtle).width(72) // 显示当前选中的账号名 Text(this.getSelectedAccountName()) .fontSize(13).fontColor(Theme.accent) .layoutWeight(1) .onClick(() => { this.showAccountPicker = !this.showAccountPicker; }) Text(' ▾').fontSize(12).fontColor(Theme.textSubtle) .onClick(() => { this.showAccountPicker = !this.showAccountPicker; }) } .width('100%').height(40).padding({ left: 12, right: 12 }) .backgroundColor(Theme.surface) // 账号选择下拉 if (this.showAccountPicker) { ForEach(this.accountList, (acct: AccountInfo) => { Row() { Text(acct.displayName) .fontSize(13) .fontColor(this.selectedAccountId === acct.id ? Theme.accent : Theme.textPrimary) .fontWeight(this.selectedAccountId === acct.id ? FontWeight.Bold : FontWeight.Normal) .layoutWeight(1) if (this.selectedAccountId === acct.id) { AmIcon({ iconName: 'check', iconSize: 14, iconColor: Theme.accent }) } } .width('100%').height(40).padding({ left: 40, right: 12 }) .backgroundColor(this.selectedAccountId === acct.id ? Theme.accentSoft : Theme.surface) .onClick(() => { this.selectedAccountId = acct.id; this.showAccountPicker = false; }) }, (acct: AccountInfo) => acct.id) } Divider().color(Theme.border) } // 收件人 Row() { Text('收件人').fontSize(14).fontColor(Theme.textSubtle).width(60) TextInput({ placeholder: 'name@path.session', text: this.to }) .layoutWeight(1).fontSize(14).backgroundColor(Color.Transparent) .onChange((v: string) => { this.to = v; }) } .width('100%').height(48).padding({ left: 12, right: 12 }) .backgroundColor(Theme.surface) Divider().color(Theme.border) // 主题 Row() { Text('主题').fontSize(14).fontColor(Theme.textSubtle).width(60) TextInput({ placeholder: '邮件主题', text: this.subject }) .layoutWeight(1).fontSize(14).backgroundColor(Color.Transparent) .onChange((v: string) => { this.subject = v; }) } .width('100%').height(48).padding({ left: 12, right: 12 }) .backgroundColor(Theme.surface) Divider().color(Theme.border) // 正文 TextArea({ placeholder: '输入邮件正文…' }) .layoutWeight(1).width('100%') .fontSize(14) .backgroundColor(Theme.surface) .onChange((v: string) => { this.body = v; }) // 附件区 if (this.attachmentNames.length > 0) { Column() { ForEach(this.attachmentNames, (name: string, idx: number) => { Row() { AmIcon({ iconName: 'paperclip', iconSize: 12, iconColor: Theme.textMuted }) Text(name) .fontSize(12).fontColor(Theme.textPrimary) .margin({ left: 6 }) .layoutWeight(1) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) AmIcon({ iconName: 'close', iconSize: 14, iconColor: Theme.danger }) .onClick(() => { this.removeAttachment(idx); }) } .width('100%').height(32) .padding({ left: 12, right: 12 }) .backgroundColor(Theme.pageBg) }, (_name: string, idx: number) => idx.toString()) } .width('100%') } // 底部工具栏 Row() { Row() { AmIcon({ iconName: 'paperclip', iconSize: 14, iconColor: Theme.accent }) Text('添加附件') .fontSize(13).fontColor(Theme.accent).margin({ left: 6 }) .onClick(() => { this.pickAttachment(); }) } if (this.uploading) { Text(this.status) .fontSize(11).fontColor(Theme.textMuted) .margin({ left: 12 }) } else if (this.status.length > 0) { Text(this.status) .fontSize(11).fontColor(Theme.approve) .margin({ left: 12 }) } } .width('100%').height(44) .padding({ left: 12, right: 12 }) .backgroundColor(Theme.surface) } .width('100%').height('100%') .backgroundColor(Theme.pageBg) } }