核心变更: - 数据库:Schema v3,accounts 表新增 account_type/provider/imap_host/imap_port/imap_password 字段 - 后端:新增通用 IMAP 服务(imap_generic.py)与邮箱提供商配置(providers.py) - 导入:支持 Outlook/QQ/163/Gmail/自定义 IMAP 多格式批量导入,兼容注释行与导出格式 - 导出:按邮箱类型分组导出,保留 provider 信息便于重新导入 - 邮件读取:IMAP 账号走通用 IMAP 服务,Outlook 保持原 Graph/XOAUTH2 链路 - 验证码提取:IMAP 账号通过 IMAP 获取邮件详情后提取 - 安全约束:IMAP 账号禁用远程删除,敏感字段(imap_password)加密存储 - 前端:新增邮箱类型下拉、自定义 IMAP 配置区、provider 标签显示 - 调度器:定时刷新仅处理 Outlook 账号(IMAP 无 OAuth token 刷新语义) - 日志:配置 outlook_web 命名空间日志输出到 stderr 技术细节: - 保持 Outlook 旧格式完全兼容(四段式:email----password----client_id----refresh_token) - IMAP 账号使用空字符串占位 client_id/refresh_token(满足 DB NOT NULL 约束) - 支持导出格式智能识别(5 段 custom、3 段 provider、2 段配合输入框) - 前端 DOMPurify 未加载时回退为基本 HTML 过滤(CDN 不可达场景) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
618 lines
26 KiB
JavaScript
618 lines
26 KiB
JavaScript
// ==================== 账号相关 ====================
|
||
|
||
// 选择账号
|
||
function selectAccount(email) {
|
||
if (typeof stopPolling === 'function') {
|
||
stopPolling(true);
|
||
}
|
||
|
||
currentAccount = email;
|
||
isTempEmailGroup = false;
|
||
currentFolder = 'inbox';
|
||
|
||
document.getElementById('currentAccountBar').style.display = '';
|
||
document.getElementById('currentAccountEmail').textContent = email;
|
||
|
||
// Update active state on account cards
|
||
document.querySelectorAll('.account-card').forEach(item => {
|
||
item.classList.remove('active');
|
||
const emailEl = item.querySelector('.account-email');
|
||
if (emailEl && emailEl.textContent.includes(email)) {
|
||
item.classList.add('active');
|
||
}
|
||
});
|
||
|
||
const folderTabs = document.getElementById('folderTabs');
|
||
if (folderTabs) {
|
||
folderTabs.style.display = 'flex';
|
||
document.querySelectorAll('.email-tab').forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.folder === 'inbox');
|
||
});
|
||
}
|
||
|
||
const cacheKey = `${email}_inbox`;
|
||
|
||
if (emailListCache[cacheKey]) {
|
||
const cache = emailListCache[cacheKey];
|
||
currentEmails = cache.emails;
|
||
hasMoreEmails = cache.has_more;
|
||
currentSkip = cache.skip;
|
||
currentMethod = cache.method || 'graph';
|
||
|
||
const methodTag = document.getElementById('methodTag');
|
||
methodTag.textContent = currentMethod;
|
||
methodTag.style.display = 'inline';
|
||
document.getElementById('emailCount').textContent = `(${currentEmails.length})`;
|
||
|
||
renderEmailList(currentEmails);
|
||
} else {
|
||
document.getElementById('emailList').innerHTML = `
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📬</span>
|
||
<p>点击"获取邮件"按钮获取邮件</p>
|
||
</div>
|
||
`;
|
||
document.getElementById('emailCount').textContent = '';
|
||
document.getElementById('methodTag').style.display = 'none';
|
||
currentEmails = [];
|
||
}
|
||
|
||
document.getElementById('emailDetail').innerHTML = `
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📄</span>
|
||
<p>选择一封邮件查看详情</p>
|
||
</div>
|
||
`;
|
||
document.getElementById('emailDetailToolbar').style.display = 'none';
|
||
}
|
||
|
||
// Provider 下拉缓存
|
||
let providersLoaded = false;
|
||
let providerOptions = [];
|
||
|
||
// 加载邮箱 providers(用于导入下拉)
|
||
async function loadProviders() {
|
||
if (providersLoaded) return;
|
||
|
||
const select = document.getElementById('accountProvider');
|
||
if (!select) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/providers');
|
||
const data = await resp.json();
|
||
if (!data.success || !Array.isArray(data.providers)) return;
|
||
|
||
providerOptions = data.providers;
|
||
select.innerHTML = data.providers.map(p => `
|
||
<option value="${escapeHtml(p.key)}">${escapeHtml(p.label || p.key)}</option>
|
||
`).join('');
|
||
|
||
providersLoaded = true;
|
||
} catch (e) {
|
||
// 静默失败:保留默认 Outlook 选项
|
||
}
|
||
}
|
||
|
||
// Provider 切换:更新 placeholder / hint / custom IMAP 配置区显示
|
||
function onProviderChange(provider) {
|
||
const p = (provider || 'outlook').toLowerCase();
|
||
const input = document.getElementById('accountInput');
|
||
const hint = document.getElementById('accountFormatHint');
|
||
const customFields = document.getElementById('customImapFields');
|
||
const getTokenBtn = document.getElementById('getRefreshTokenBtnInAdd');
|
||
|
||
if (!input || !hint || !customFields) return;
|
||
|
||
// "获取Token"按钮仅 Outlook 类型可见
|
||
if (getTokenBtn) {
|
||
getTokenBtn.style.display = (p === 'outlook') ? '' : 'none';
|
||
}
|
||
|
||
if (p === 'outlook') {
|
||
customFields.style.display = 'none';
|
||
input.placeholder = '邮箱----密码----client_id----refresh_token';
|
||
hint.textContent = '格式:邮箱----密码----client_id----refresh_token,支持批量导入(每行一个)';
|
||
return;
|
||
}
|
||
|
||
if (p === 'custom') {
|
||
customFields.style.display = '';
|
||
input.placeholder = '邮箱----IMAP授权码/应用密码';
|
||
hint.textContent = '格式:邮箱----IMAP授权码/应用密码(每行一个)。自定义 IMAP 需填写上方服务器/端口;也支持:邮箱----授权码----imap_host----imap_port';
|
||
return;
|
||
}
|
||
|
||
customFields.style.display = 'none';
|
||
input.placeholder = '邮箱----IMAP授权码/应用密码';
|
||
hint.textContent = '格式:邮箱----IMAP授权码/应用密码,支持批量导入(每行一个)';
|
||
}
|
||
|
||
// 显示添加账号模态框
|
||
function showAddAccountModal() {
|
||
document.getElementById('accountInput').value = '';
|
||
// 设置默认分组为当前选中的分组
|
||
if (currentGroupId) {
|
||
document.getElementById('importGroupSelect').value = currentGroupId;
|
||
}
|
||
// 加载 providers 并初始化默认状态
|
||
loadProviders().finally(() => {
|
||
const sel = document.getElementById('accountProvider');
|
||
if (sel) {
|
||
sel.value = 'outlook';
|
||
onProviderChange('outlook');
|
||
} else {
|
||
onProviderChange('outlook');
|
||
}
|
||
|
||
const hostEl = document.getElementById('imapHost');
|
||
const portEl = document.getElementById('imapPort');
|
||
if (hostEl) hostEl.value = '';
|
||
if (portEl) portEl.value = '993';
|
||
});
|
||
document.getElementById('addAccountModal').classList.add('show');
|
||
}
|
||
|
||
// 隐藏添加账号模态框
|
||
function hideAddAccountModal() {
|
||
document.getElementById('addAccountModal').classList.remove('show');
|
||
}
|
||
|
||
// 添加账号
|
||
async function addAccount() {
|
||
const input = document.getElementById('accountInput').value.trim();
|
||
const groupId = parseInt(document.getElementById('importGroupSelect').value);
|
||
const providerEl = document.getElementById('accountProvider');
|
||
const provider = providerEl ? (providerEl.value || 'outlook') : 'outlook';
|
||
|
||
if (!input) {
|
||
showToast('请输入账号信息', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const payload = { account_string: input, group_id: groupId };
|
||
if (provider && provider !== 'outlook') {
|
||
payload.provider = provider;
|
||
if (provider === 'custom') {
|
||
const host = (document.getElementById('imapHost')?.value || '').trim();
|
||
const portRaw = (document.getElementById('imapPort')?.value || '').trim();
|
||
const port = parseInt(portRaw || '993', 10) || 993;
|
||
|
||
if (!host) {
|
||
// 允许每行内嵌 host/port:email----授权码----imap_host----imap_port(或导出格式 5 段)
|
||
const lines = input.split('\n').map(l => (l || '').trim()).filter(l => l && !l.startsWith('#'));
|
||
const hasInlineHost = lines.some(l => (l.split('----').length >= 4));
|
||
if (!hasInlineHost) {
|
||
showToast('请填写 IMAP 服务器地址(或在文本中每行包含 host/port)', 'error');
|
||
return;
|
||
}
|
||
} else {
|
||
payload.imap_host = host;
|
||
payload.imap_port = port;
|
||
}
|
||
}
|
||
}
|
||
|
||
const response = await fetch('/api/accounts', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast(data.message, 'success');
|
||
hideAddAccountModal();
|
||
|
||
// 清除该分组的缓存
|
||
delete accountsCache[groupId];
|
||
|
||
// 刷新分组列表(更新数量)
|
||
loadGroups();
|
||
|
||
// 如果当前选中的就是这个分组,刷新邮箱列表
|
||
if (currentGroupId === groupId) {
|
||
loadAccountsByGroup(groupId, true);
|
||
}
|
||
} else {
|
||
handleApiError(data, '导入邮箱失败');
|
||
}
|
||
} catch (error) {
|
||
showToast('添加失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 显示编辑账号模态框
|
||
async function showEditAccountModal(accountId) {
|
||
try {
|
||
const response = await fetch(`/api/accounts/${accountId}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const acc = data.account;
|
||
const isImap = (acc.account_type || 'outlook') === 'imap';
|
||
|
||
document.getElementById('editAccountId').value = acc.id;
|
||
document.getElementById('editAccountType').value = acc.account_type || 'outlook';
|
||
document.getElementById('editEmail').value = acc.email;
|
||
document.getElementById('editPassword').value = acc.password || '';
|
||
document.getElementById('editClientId').value = acc.client_id;
|
||
document.getElementById('editRefreshToken').value = acc.refresh_token;
|
||
document.getElementById('editGroupSelect').value = acc.group_id || 1;
|
||
document.getElementById('editRemark').value = acc.remark || '';
|
||
document.getElementById('editStatus').value = acc.status || 'active';
|
||
|
||
// IMAP 账号:隐藏 Client ID / Refresh Token,调整密码标签
|
||
const clientIdGroup = document.getElementById('editClientIdGroup');
|
||
const refreshTokenGroup = document.getElementById('editRefreshTokenGroup');
|
||
const passwordLabel = document.getElementById('editPasswordLabel');
|
||
|
||
if (isImap) {
|
||
clientIdGroup.style.display = 'none';
|
||
refreshTokenGroup.style.display = 'none';
|
||
passwordLabel.textContent = '授权码 / 应用密码';
|
||
document.getElementById('editPassword').placeholder = '留空则不修改';
|
||
} else {
|
||
clientIdGroup.style.display = '';
|
||
refreshTokenGroup.style.display = '';
|
||
passwordLabel.textContent = '密码';
|
||
document.getElementById('editPassword').placeholder = '可选,留空则不修改';
|
||
}
|
||
|
||
document.getElementById('editAccountModal').classList.add('show');
|
||
}
|
||
} catch (error) {
|
||
showToast('加载账号信息失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 隐藏编辑账号模态框
|
||
function hideEditAccountModal() {
|
||
document.getElementById('editAccountModal').classList.remove('show');
|
||
}
|
||
|
||
// 更新账号
|
||
async function updateAccount() {
|
||
const accountId = document.getElementById('editAccountId').value;
|
||
const accountType = document.getElementById('editAccountType').value || 'outlook';
|
||
const isImap = accountType === 'imap';
|
||
const oldGroupId = currentGroupId;
|
||
const newGroupId = parseInt(document.getElementById('editGroupSelect').value);
|
||
|
||
const data = {
|
||
email: document.getElementById('editEmail').value.trim(),
|
||
password: document.getElementById('editPassword').value,
|
||
client_id: document.getElementById('editClientId').value.trim(),
|
||
refresh_token: document.getElementById('editRefreshToken').value.trim(),
|
||
group_id: newGroupId,
|
||
remark: document.getElementById('editRemark').value.trim(),
|
||
status: document.getElementById('editStatus').value
|
||
};
|
||
|
||
if (!data.email) {
|
||
showToast('邮箱地址不能为空', 'error');
|
||
return;
|
||
}
|
||
|
||
// Outlook 账号需要 Client ID 和 Refresh Token
|
||
if (!isImap && (!data.client_id || !data.refresh_token)) {
|
||
showToast('邮箱、Client ID 和 Refresh Token 不能为空', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/accounts/${accountId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast(result.message, 'success');
|
||
hideEditAccountModal();
|
||
|
||
// 清除相关分组的缓存
|
||
delete accountsCache[oldGroupId];
|
||
if (oldGroupId !== newGroupId) {
|
||
delete accountsCache[newGroupId];
|
||
}
|
||
|
||
// 刷新分组列表
|
||
loadGroups();
|
||
|
||
// 刷新当前分组的邮箱列表
|
||
if (currentGroupId) {
|
||
loadAccountsByGroup(currentGroupId, true);
|
||
}
|
||
} else {
|
||
showToast(result.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('更新失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 删除当前编辑的账号
|
||
async function deleteCurrentAccount() {
|
||
const accountId = document.getElementById('editAccountId').value;
|
||
const email = document.getElementById('editEmail').value;
|
||
const groupId = parseInt(document.getElementById('editGroupSelect').value);
|
||
|
||
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/accounts/${accountId}`, { method: 'DELETE' });
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast('删除成功', 'success');
|
||
hideEditAccountModal();
|
||
|
||
// 清除缓存
|
||
delete accountsCache[groupId];
|
||
|
||
if (currentAccount === email) {
|
||
currentAccount = null;
|
||
document.getElementById('currentAccountBar').style.display = 'none';
|
||
document.getElementById('emailList').innerHTML = `
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📬</span><p>请从左侧选择一个邮箱账号</p>
|
||
</div>
|
||
`;
|
||
document.getElementById('emailDetail').innerHTML = `
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📄</span><p>选择一封邮件查看详情</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 刷新分组列表
|
||
loadGroups();
|
||
|
||
// 刷新当前分组的邮箱列表
|
||
if (currentGroupId) {
|
||
loadAccountsByGroup(currentGroupId, true);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
showToast('删除失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 切换账号状态(启用/停用)
|
||
async function toggleAccountStatus(accountId, currentStatus) {
|
||
const newStatus = currentStatus === 'inactive' ? 'active' : 'inactive';
|
||
const action = newStatus === 'inactive' ? '停用' : '启用';
|
||
|
||
try {
|
||
const response = await fetch(`/api/accounts/${accountId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status: newStatus })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast(`${action}成功`, 'success');
|
||
|
||
// 清除当前分组的缓存
|
||
if (currentGroupId) {
|
||
delete accountsCache[currentGroupId];
|
||
loadAccountsByGroup(currentGroupId, true);
|
||
}
|
||
} else {
|
||
handleApiError(data, `${action}账号失败`);
|
||
}
|
||
} catch (error) {
|
||
showToast(`${action}失败`, 'error');
|
||
}
|
||
}
|
||
|
||
// 删除账号(快捷方式)
|
||
async function deleteAccount(accountId, email) {
|
||
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/accounts/${accountId}`, { method: 'DELETE' });
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast('删除成功', 'success');
|
||
|
||
// 清除当前分组的缓存
|
||
if (currentGroupId) {
|
||
delete accountsCache[currentGroupId];
|
||
}
|
||
|
||
if (currentAccount === email) {
|
||
currentAccount = null;
|
||
document.getElementById('currentAccountBar').style.display = 'none';
|
||
document.getElementById('emailList').innerHTML = `
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📬</span><p>请从左侧选择一个邮箱账号</p>
|
||
</div>
|
||
`;
|
||
document.getElementById('emailDetail').innerHTML = `
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📄</span><p>选择一封邮件查看详情</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 刷新分组列表
|
||
loadGroups();
|
||
|
||
// 刷新当前分组的邮箱列表
|
||
if (currentGroupId) {
|
||
loadAccountsByGroup(currentGroupId, true);
|
||
}
|
||
} else {
|
||
handleApiError(data, '删除账号失败');
|
||
}
|
||
} catch (error) {
|
||
showToast('删除失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 显示导出邮箱模态框
|
||
async function showExportModal() {
|
||
document.getElementById('exportModal').classList.add('show');
|
||
await loadExportGroupList();
|
||
}
|
||
|
||
// 隐藏导出邮箱模态框
|
||
function hideExportModal() {
|
||
document.getElementById('exportModal').classList.remove('show');
|
||
}
|
||
|
||
// 加载导出分组列表
|
||
async function loadExportGroupList() {
|
||
const container = document.getElementById('exportGroupList');
|
||
container.innerHTML = '<div class="loading-overlay"><span class="spinner"></span></div>';
|
||
|
||
try {
|
||
if (groups.length === 0) {
|
||
container.innerHTML = '<div class="empty-state"><p>暂无分组</p></div>';
|
||
} else {
|
||
container.innerHTML = groups.map(group => `
|
||
<label style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer; border-radius: var(--radius); transition: background-color 0.15s;"
|
||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||
onmouseout="this.style.backgroundColor='transparent'">
|
||
<input type="checkbox" class="export-group-checkbox" value="${group.id}">
|
||
<span style="display: flex; align-items: center; gap: 8px; flex: 1;">
|
||
<span class="group-color-dot" style="background-color: ${group.color || '#666'}"></span>
|
||
<span style="font-size: 0.9rem; color: var(--text);">${escapeHtml(group.name)}</span>
|
||
</span>
|
||
<span class="badge-count">${group.account_count || 0}</span>
|
||
</label>
|
||
`).join('');
|
||
}
|
||
} catch (error) {
|
||
container.innerHTML = '<div class="empty-state"><p style="color:var(--clr-danger)">加载失败</p></div>';
|
||
}
|
||
|
||
document.getElementById('selectAllGroups').checked = false;
|
||
}
|
||
|
||
// 全选/取消全选分组
|
||
function toggleSelectAllGroups() {
|
||
const selectAll = document.getElementById('selectAllGroups').checked;
|
||
document.querySelectorAll('.export-group-checkbox').forEach(cb => {
|
||
cb.checked = selectAll;
|
||
});
|
||
}
|
||
|
||
// 存储待导出的分组ID
|
||
let pendingExportGroupIds = [];
|
||
|
||
// 导出选中的分组
|
||
async function exportSelectedGroups() {
|
||
const checkboxes = document.querySelectorAll('.export-group-checkbox:checked');
|
||
const groupIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||
|
||
if (groupIds.length === 0) {
|
||
showToast('请选择要导出的分组', 'error');
|
||
return;
|
||
}
|
||
|
||
// 保存待导出的分组ID
|
||
pendingExportGroupIds = groupIds;
|
||
|
||
// 显示密码确认对话框
|
||
hideExportModal();
|
||
showExportVerifyModal();
|
||
}
|
||
|
||
// 显示导出密码确认对话框
|
||
function showExportVerifyModal() {
|
||
document.getElementById('exportVerifyModal').classList.add('show');
|
||
document.getElementById('exportVerifyPassword').value = '';
|
||
document.getElementById('exportVerifyPassword').focus();
|
||
}
|
||
|
||
// 隐藏导出密码确认对话框
|
||
function hideExportVerifyModal() {
|
||
document.getElementById('exportVerifyModal').classList.remove('show');
|
||
document.getElementById('exportVerifyPassword').value = '';
|
||
}
|
||
|
||
// 确认导出验证
|
||
async function confirmExportVerify() {
|
||
const password = document.getElementById('exportVerifyPassword').value;
|
||
|
||
if (!password) {
|
||
showToast('请输入密码', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 获取验证token
|
||
const verifyResponse = await fetch('/api/export/verify', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ password })
|
||
});
|
||
|
||
const verifyData = await verifyResponse.json();
|
||
|
||
if (!verifyData.success) {
|
||
showToast(verifyData.error || '密码错误', 'error');
|
||
return;
|
||
}
|
||
|
||
const verifyToken = verifyData.verify_token;
|
||
|
||
// 执行导出(使用请求头传递 token,避免 URL/日志泄露)
|
||
const response = await fetch('/api/accounts/export-selected', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Export-Token': verifyToken
|
||
},
|
||
body: JSON.stringify({
|
||
group_ids: pendingExportGroupIds
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 获取文件名
|
||
const contentDisposition = response.headers.get('Content-Disposition');
|
||
let filename = 'accounts.txt';
|
||
if (contentDisposition) {
|
||
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;\n]+)/i);
|
||
if (match) {
|
||
filename = decodeURIComponent(match[1]);
|
||
}
|
||
}
|
||
|
||
// 下载文件
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
showToast('导出成功', 'success');
|
||
hideExportVerifyModal();
|
||
} else {
|
||
const data = await response.json();
|
||
handleApiError(data, '导出失败');
|
||
}
|
||
} catch (error) {
|
||
showToast('导出失败', 'error');
|
||
}
|
||
}
|
||
|