chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,321 @@
/*
* AgentMail 鸿蒙客户端 — 写邮件页
* 收件人 / 主题 / 正文 / 附件选择 / 发送
* 路由参数可选: to, reply_to, session_alias
*/
import { ApiClient, ApiError } from '../api/ApiClient';
import { MailApi } from '../api/MailApi';
import { AccountManager, AccountInfo } from '../api/AccountManager';
import { SendMailRequest } from '../model/Models';
import { ComposeParams } from '../model/RouteParams';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { picker } from '@kit.CoreFileKit';
@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) {
promptAction.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;
promptAction.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) {
promptAction.showToast({ message: '请填写收件人' });
return;
}
if (this.subject.length === 0) {
promptAction.showToast({ message: '请填写主题' });
return;
}
const ctx: Context | undefined = this.getUIContext().getHostContext();
if (ctx === undefined) {
return;
}
const mailApi: MailApi | null = this.createSelectedMailApi(ctx);
if (mailApi === null) {
promptAction.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);
promptAction.showToast({ message: '✅ 邮件已发送' });
this.getUIContext().getRouter().back();
} catch (e) {
const apiError = e as ApiError;
promptAction.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('#FF4444')
.onClick(() => { this.getUIContext().getRouter().back(); })
Blank()
Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333')
Blank()
Button(this.sending ? '发送中…' : '发送')
.fontSize(13).height(32)
.backgroundColor('#1A73E8')
.enabled(!this.sending && !this.uploading)
.onClick(() => { this.doSend(); })
}
.width('100%').height(56)
.padding({ left: 12, right: 12 })
.backgroundColor('#FFFFFF')
Divider().color('#EEEEEE')
// 发信账号选择(多账号时显示)
if (this.accountList.length > 1) {
Row() {
Text('发信账号').fontSize(13).fontColor('#999999').width(72)
// 显示当前选中的账号名
Text(this.getSelectedAccountName())
.fontSize(13).fontColor('#1A73E8')
.layoutWeight(1)
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
Text(' ▾').fontSize(12).fontColor('#999999')
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
}
.width('100%').height(40).padding({ left: 12, right: 12 })
.backgroundColor('#FFFFFF')
// 账号选择下拉
if (this.showAccountPicker) {
ForEach(this.accountList, (acct: AccountInfo) => {
Row() {
Text(acct.displayName)
.fontSize(13)
.fontColor(this.selectedAccountId === acct.id ? '#1A73E8' : '#333333')
.fontWeight(this.selectedAccountId === acct.id ? FontWeight.Bold : FontWeight.Normal)
.layoutWeight(1)
if (this.selectedAccountId === acct.id) {
Text('✓').fontSize(14).fontColor('#1A73E8')
}
}
.width('100%').height(40).padding({ left: 40, right: 12 })
.backgroundColor(this.selectedAccountId === acct.id ? '#F0F7FF' : '#FFFFFF')
.onClick(() => {
this.selectedAccountId = acct.id;
this.showAccountPicker = false;
})
}, (acct: AccountInfo) => acct.id)
}
Divider().color('#F0F0F0')
}
// 收件人
Row() {
Text('收件人').fontSize(14).fontColor('#999999').width(60)
TextInput({ placeholder: 'name@path.session', text: this.to })
.layoutWeight(1).fontSize(14).backgroundColor('#00000000')
.onChange((v: string) => { this.to = v; })
}
.width('100%').height(48).padding({ left: 12, right: 12 })
.backgroundColor('#FFFFFF')
Divider().color('#F0F0F0')
// 主题
Row() {
Text('主题').fontSize(14).fontColor('#999999').width(60)
TextInput({ placeholder: '邮件主题', text: this.subject })
.layoutWeight(1).fontSize(14).backgroundColor('#00000000')
.onChange((v: string) => { this.subject = v; })
}
.width('100%').height(48).padding({ left: 12, right: 12 })
.backgroundColor('#FFFFFF')
Divider().color('#F0F0F0')
// 正文
TextArea({ placeholder: '输入邮件正文…' })
.layoutWeight(1).width('100%')
.fontSize(14)
.backgroundColor('#FFFFFF')
.onChange((v: string) => { this.body = v; })
// 附件区
if (this.attachmentNames.length > 0) {
Column() {
ForEach(this.attachmentNames, (name: string, idx: number) => {
Row() {
Text('📎 ' + name)
.fontSize(12).fontColor('#333333')
.layoutWeight(1)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('✕')
.fontSize(14).fontColor('#FF4444')
.onClick(() => { this.removeAttachment(idx); })
}
.width('100%').height(32)
.padding({ left: 12, right: 12 })
.backgroundColor('#F8F9FA')
}, (_name: string, idx: number) => idx.toString())
}
.width('100%')
}
// 底部工具栏
Row() {
Text('📎 添加附件')
.fontSize(13).fontColor('#1A73E8')
.onClick(() => { this.pickAttachment(); })
if (this.uploading) {
Text(this.status)
.fontSize(11).fontColor('#666666')
.margin({ left: 12 })
} else if (this.status.length > 0) {
Text(this.status)
.fontSize(11).fontColor('#4CAF50')
.margin({ left: 12 })
}
}
.width('100%').height(44)
.padding({ left: 12, right: 12 })
.backgroundColor('#FFFFFF')
}
.width('100%').height('100%')
.backgroundColor('#F5F7FA')
}
}