Files
AiTool/frontend/app/(main)/settings/email/page.tsx
2026-03-15 16:38:59 +08:00

365 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
emailConfigsApi,
type EmailConfigRead,
type EmailConfigCreate,
type EmailConfigUpdate,
type EmailFolder,
} from "@/lib/api/client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2, Mail, Plus, Pencil, Trash2, FolderOpen } from "lucide-react";
import { toast } from "sonner";
export default function SettingsEmailPage() {
const [configs, setConfigs] = useState<EmailConfigRead[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({
host: "",
port: "993",
user: "",
password: "",
mailbox: "INBOX",
active: true,
});
const [folders, setFolders] = useState<EmailFolder[] | null>(null);
const [foldersLoading, setFoldersLoading] = useState(false);
const loadConfigs = useCallback(async () => {
try {
const list = await emailConfigsApi.list();
setConfigs(list);
} catch {
toast.error("加载邮箱列表失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadConfigs();
}, [loadConfigs]);
const openAdd = () => {
setEditingId(null);
setForm({
host: "",
port: "993",
user: "",
password: "",
mailbox: "INBOX",
active: true,
});
setDialogOpen(true);
};
const openEdit = (c: EmailConfigRead) => {
setEditingId(c.id);
setFolders(null);
setForm({
host: c.host,
port: String(c.port),
user: c.user,
password: "",
mailbox: c.mailbox || "INBOX",
active: c.active,
});
setDialogOpen(true);
};
const loadFolders = async () => {
if (!editingId) return;
setFoldersLoading(true);
try {
const res = await emailConfigsApi.listFolders(editingId);
setFolders(res.folders);
if (res.folders.length > 0 && !form.mailbox) {
const inbox = res.folders.find((f) => f.decoded === "INBOX" || f.decoded === "收件箱");
if (inbox) setForm((f) => ({ ...f, mailbox: inbox.decoded }));
}
toast.success(`已加载 ${res.folders.length} 个邮箱夹`);
} catch (e) {
toast.error(e instanceof Error ? e.message : "获取邮箱列表失败");
} finally {
setFoldersLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
if (editingId) {
const payload: EmailConfigUpdate = {
host: form.host,
port: parseInt(form.port, 10) || 993,
user: form.user,
mailbox: form.mailbox,
active: form.active,
};
if (form.password) payload.password = form.password;
await emailConfigsApi.update(editingId, payload);
toast.success("已更新");
} else {
await emailConfigsApi.create({
host: form.host,
port: parseInt(form.port, 10) || 993,
user: form.user,
password: form.password,
mailbox: form.mailbox,
active: form.active,
});
toast.success("已添加");
}
setDialogOpen(false);
await loadConfigs();
} catch (e) {
toast.error(e instanceof Error ? e.message : "保存失败");
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("确定删除该邮箱账户?")) return;
try {
await emailConfigsApi.delete(id);
toast.success("已删除");
await loadConfigs();
} catch (e) {
toast.error(e instanceof Error ? e.message : "删除失败");
}
};
return (
<div className="p-6 max-w-4xl">
<div className="mb-4">
<a href="/settings" className="text-sm text-muted-foreground hover:text-foreground">
</a>
</div>
<Card>
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
<Button onClick={openAdd}>
<Plus className="h-4 w-4" />
<span className="ml-2"></span>
</Button>
</div>
</CardHeader>
<CardContent>
{loading ? (
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Loader2 className="h-4 w-4 animate-spin" />
</p>
) : configs.length === 0 ? (
<p className="text-sm text-muted-foreground py-4">
使使 IMAP_*
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Host</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>Mailbox</TableHead>
<TableHead></TableHead>
<TableHead className="w-[120px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{configs.map((c) => (
<TableRow key={c.id}>
<TableCell className="font-medium">{c.host}</TableCell>
<TableCell>{c.port}</TableCell>
<TableCell>{c.user}</TableCell>
<TableCell className="text-muted-foreground">{c.mailbox}</TableCell>
<TableCell>
{c.active ? (
<span className="text-xs bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 px-2 py-0.5 rounded">
</span>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={() => openEdit(c)}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleDelete(c.id)}>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
<Card className="mt-4">
<CardHeader className="py-3">
<CardTitle className="text-sm"> 163 </CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2 py-2">
<p><strong>IMAP </strong> imap.163.com 993SSLIMAP/SMTP </p>
<p><strong></strong> 使 POP3/SMTP/IMAP </p>
<p><strong></strong> INBOX </p>
</CardContent>
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{editingId ? "编辑邮箱账户" : "添加邮箱账户"}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-2">
<Label>IMAP Host</Label>
<Input
value={form.host}
onChange={(e) => setForm((f) => ({ ...f, host: e.target.value }))}
placeholder="imap.163.com"
required
/>
</div>
<div className="grid gap-2">
<Label></Label>
<Input
type="number"
value={form.port}
onChange={(e) => setForm((f) => ({ ...f, port: e.target.value }))}
placeholder="993"
/>
</div>
<div className="grid gap-2">
<Label> / </Label>
<Input
type="email"
value={form.user}
onChange={(e) => setForm((f) => ({ ...f, user: e.target.value }))}
placeholder="user@example.com"
required
/>
</div>
<div className="grid gap-2">
<Label> / {editingId && "(留空则不修改)"}</Label>
<Input
type="password"
value={form.password}
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
placeholder={editingId ? "••••••••" : "请输入"}
autoComplete="off"
required={!editingId}
/>
</div>
<div className="grid gap-2">
<Label> / (Mailbox)</Label>
{editingId && (
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={loadFolders}
disabled={foldersLoading}
>
{foldersLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <FolderOpen className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
</div>
)}
{folders && folders.length > 0 ? (
<Select
value={form.mailbox}
onValueChange={(v) => setForm((f) => ({ ...f, mailbox: v }))}
>
<SelectTrigger>
<SelectValue placeholder="选择邮箱夹" />
</SelectTrigger>
<SelectContent>
{folders.map((f) => (
<SelectItem key={f.raw} value={f.decoded}>
{f.decoded}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={form.mailbox}
onChange={(e) => setForm((f) => ({ ...f, mailbox: e.target.value }))}
placeholder="INBOX、收件箱或自定义标签163 等若 INBOX 失败会自动尝试收件箱)"
/>
)}
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="active"
checked={form.active}
onChange={(e) => setForm((f) => ({ ...f, active: e.target.checked }))}
className="rounded border-input"
/>
<Label htmlFor="active"></Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
</Button>
<Button type="submit" disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
<span className="ml-2">{editingId ? "保存" : "添加"}</span>
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}