- 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
326 lines
11 KiB
Plaintext
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)
|
|
}
|
|
} |