Files
MailUI4Agents/client/harmony/entry/src/main/ets/pages/ComposePage.ets
JianFeeeee cfe7808af4 ke: 鸿蒙 UI 整改:emoji→AmIcon 图标、宽屏侧栏、Md 第三方库、通知可配置项
- common/Icons.ets(生成器产出):43 枚与 WebUI icons.tsx 同几何的 Path 图标
- NavItems.ts:icon(emoji) → iconKey,与 WebUI 导航同图标
- 全页面 emoji 清除(空态改单行灰字,按钮/头像/刷新等换 AmIcon)
- MailDetailPage 接入 @luvi/lv-markdown-in(第三方,替代手写解析器)
- WideSidebar + MainPage isWide(≥768vp):宽屏侧栏 60vp 图标轨、藏底部条、onAreaChange 断点
- 通知=可配置项(默认关):PushService.isEnabled/setEnabled + reportToken 最前 gate (关=不取token/不弹权限/不打网关)+ SettingsPage Toggle + 服务端通道状态提示
- EntryAbility.onCreate 解析 want:冷启点通知也能路由(原来只有 onNewWant)
- 判据:harmony-widescreen(4) + harmony-push-optin(4) 新增;harmony-nav 同步 iconKey/条件 padding
2026-09-16 07:46:52 +08:00

326 lines
11 KiB
Plaintext

/*
* 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<void> {
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<void> {
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)
}
}