diff --git a/static/js/app.js b/static/js/app.js
index 7b49337..73e8878 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -85,13 +85,13 @@ const elements = {
// 注册后自动操作
autoUploadCpa: document.getElementById('auto-upload-cpa'),
cpaServiceSelectGroup: document.getElementById('cpa-service-select-group'),
- cpaServiceCheckboxes: document.getElementById('cpa-service-checkboxes'),
+ cpaServiceSelect: document.getElementById('cpa-service-select'),
autoUploadSub2api: document.getElementById('auto-upload-sub2api'),
sub2apiServiceSelectGroup: document.getElementById('sub2api-service-select-group'),
- sub2apiServiceCheckboxes: document.getElementById('sub2api-service-checkboxes'),
+ sub2apiServiceSelect: document.getElementById('sub2api-service-select'),
autoUploadTm: document.getElementById('auto-upload-tm'),
tmServiceSelectGroup: document.getElementById('tm-service-select-group'),
- tmServiceCheckboxes: document.getElementById('tm-service-checkboxes'),
+ tmServiceSelect: document.getElementById('tm-service-select'),
};
// 初始化
@@ -108,14 +108,14 @@ document.addEventListener('DOMContentLoaded', () => {
// 初始化注册后自动操作选项(CPA / Sub2API / TM)
async function initAutoUploadOptions() {
await Promise.all([
- loadServiceCheckboxes('cpa', '/cpa-services?enabled=true', elements.cpaServiceCheckboxes, elements.autoUploadCpa, elements.cpaServiceSelectGroup),
- loadServiceCheckboxes('sub2api', '/sub2api-services?enabled=true', elements.sub2apiServiceCheckboxes, elements.autoUploadSub2api, elements.sub2apiServiceSelectGroup),
- loadServiceCheckboxes('tm', '/tm-services?enabled=true', elements.tmServiceCheckboxes, elements.autoUploadTm, elements.tmServiceSelectGroup),
+ loadServiceSelect('/cpa-services?enabled=true', elements.cpaServiceSelect, elements.autoUploadCpa, elements.cpaServiceSelectGroup),
+ loadServiceSelect('/sub2api-services?enabled=true', elements.sub2apiServiceSelect, elements.autoUploadSub2api, elements.sub2apiServiceSelectGroup),
+ loadServiceSelect('/tm-services?enabled=true', elements.tmServiceSelect, elements.autoUploadTm, elements.tmServiceSelectGroup),
]);
}
-// 通用:加载服务 checkbox 列表,并处理联动
-async function loadServiceCheckboxes(type, apiPath, container, checkbox, selectGroup) {
+// 通用:构建自定义多选下拉组件并处理联动
+async function loadServiceSelect(apiPath, container, checkbox, selectGroup) {
if (!checkbox || !container) return;
let services = [];
try {
@@ -127,14 +127,31 @@ async function loadServiceCheckboxes(type, apiPath, container, checkbox, selectG
checkbox.title = '请先在设置中添加对应服务';
const label = checkbox.closest('label');
if (label) label.style.opacity = '0.5';
- if (container) container.innerHTML = '
暂无可用服务
';
+ container.innerHTML = '暂无可用服务
';
} else {
- container.innerHTML = services.map(s => `
-
- `).join('');
+ const items = services.map(s =>
+ ``
+ ).join('');
+ container.innerHTML = `
+
+
+ 全部 (${services.length})
+ ▼
+
+
${items}
+
`;
+ // 监听 checkbox 变化,更新触发器文字
+ container.querySelectorAll('.msd-item input').forEach(cb => {
+ cb.addEventListener('change', () => updateMsdLabel(container.id + '-dd'));
+ });
+ // 点击外部关闭
+ document.addEventListener('click', (e) => {
+ const dd = document.getElementById(container.id + '-dd');
+ if (dd && !dd.contains(e.target)) dd.classList.remove('open');
+ }, true);
}
// 联动显示/隐藏服务选择区
@@ -143,13 +160,27 @@ async function loadServiceCheckboxes(type, apiPath, container, checkbox, selectG
});
}
-// 获取选中的服务 ID 列表
-function getCheckedServiceIds(type) {
- const ids = [];
- document.querySelectorAll(`.upload-svc-${type}:checked`).forEach(cb => {
- ids.push(parseInt(cb.value));
- });
- return ids;
+function toggleMsd(ddId) {
+ const dd = document.getElementById(ddId);
+ if (dd) dd.classList.toggle('open');
+}
+
+function updateMsdLabel(ddId) {
+ const dd = document.getElementById(ddId);
+ if (!dd) return;
+ const all = dd.querySelectorAll('.msd-item input');
+ const checked = dd.querySelectorAll('.msd-item input:checked');
+ const label = dd.querySelector('.msd-label');
+ if (!label) return;
+ if (checked.length === 0) label.textContent = '未选择';
+ else if (checked.length === all.length) label.textContent = `全部 (${all.length})`;
+ else label.textContent = Array.from(checked).map(c => c.nextElementSibling.textContent).join(', ');
+}
+
+// 获取自定义多选下拉中选中的服务 ID 列表
+function getSelectedServiceIds(container) {
+ if (!container) return [];
+ return Array.from(container.querySelectorAll('.msd-item input:checked')).map(cb => parseInt(cb.value));
}
// 事件监听
@@ -395,11 +426,11 @@ async function handleStartRegistration(e) {
const requestData = {
email_service_type: emailServiceType,
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false,
- cpa_service_ids: elements.autoUploadCpa && elements.autoUploadCpa.checked ? getCheckedServiceIds('cpa') : [],
+ cpa_service_ids: elements.autoUploadCpa && elements.autoUploadCpa.checked ? getSelectedServiceIds(elements.cpaServiceSelect) : [],
auto_upload_sub2api: elements.autoUploadSub2api ? elements.autoUploadSub2api.checked : false,
- sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getCheckedServiceIds('sub2api') : [],
+ sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
- tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getCheckedServiceIds('tm') : [],
+ tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
};
// 如果选择了数据库中的服务,传递 service_id
@@ -1099,11 +1130,11 @@ async function handleOutlookBatchRegistration() {
concurrency: Math.min(50, Math.max(1, concurrency)),
mode: mode,
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false,
- cpa_service_ids: elements.autoUploadCpa && elements.autoUploadCpa.checked ? getCheckedServiceIds('cpa') : [],
+ cpa_service_ids: elements.autoUploadCpa && elements.autoUploadCpa.checked ? getSelectedServiceIds(elements.cpaServiceSelect) : [],
auto_upload_sub2api: elements.autoUploadSub2api ? elements.autoUploadSub2api.checked : false,
- sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getCheckedServiceIds('sub2api') : [],
+ sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
- tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getCheckedServiceIds('tm') : [],
+ tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
};
addLog('info', `[系统] 正在启动 Outlook 批量注册 (${selectedIds.length} 个账户)...`);
diff --git a/static/js/settings.js b/static/js/settings.js
index 47f4468..4dc3a9e 100644
--- a/static/js/settings.js
+++ b/static/js/settings.js
@@ -1076,7 +1076,7 @@ function renderTmServicesTable(services) {
${s.priority} |
-
+ |
@@ -1241,7 +1241,7 @@ function renderCpaServicesTable(services) {
|
${s.priority} |
-
+ |
@@ -1395,20 +1395,23 @@ async function loadSub2ApiServices() {
function renderSub2ApiServices(services) {
if (!elements.sub2ApiServicesTable) return;
if (!services || services.length === 0) {
- elements.sub2ApiServicesTable.innerHTML = ' | | 暂无服务,点击"添加服务"按钮添加 |
';
+ elements.sub2ApiServicesTable.innerHTML = '| 暂无 Sub2API 服务,点击「添加服务」新增 |
';
return;
}
elements.sub2ApiServicesTable.innerHTML = services.map(s => `
| ${escapeHtml(s.name)} |
- ${escapeHtml(s.api_url)} |
- ${s.enabled ? '已启用' : '已禁用'} |
- ${s.priority} |
+ ${escapeHtml(s.api_url)} |
-
-
-
-
+
+ ${s.enabled ? '启用' : '禁用'}
+
+ |
+ ${s.priority} |
+
+
+
+
|
`).join('');
@@ -1486,6 +1489,19 @@ async function handleSaveSub2ApiService(e) {
}
}
+async function testSub2ApiServiceById(id) {
+ try {
+ const result = await api.post(`/sub2api-services/${id}/test`);
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error(result.message);
+ }
+ } catch (e) {
+ toast.error('测试失败: ' + e.message);
+ }
+}
+
async function handleTestSub2ApiService() {
const apiUrl = document.getElementById('sub2api-service-url').value.trim();
const apiKey = document.getElementById('sub2api-service-key').value.trim();
diff --git a/templates/index.html b/templates/index.html
index 62d4a21..12b9602 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -83,6 +83,53 @@
filter: blur(0);
}
+ /* 自定义多选下拉 */
+ .multi-select-dropdown {
+ position: relative;
+ width: 100%;
+ font-size: 0.85rem;
+ }
+ .msd-trigger {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--surface);
+ color: var(--text-primary);
+ cursor: pointer;
+ user-select: none;
+ }
+ .msd-trigger:hover { border-color: var(--primary-color); }
+ .msd-arrow { font-size: 0.7rem; transition: transform 0.15s; }
+ .msd-dropdown.open .msd-arrow { transform: rotate(180deg); }
+ .msd-list {
+ display: none;
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0; right: 0;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.12);
+ z-index: 100;
+ max-height: 150px;
+ overflow-y: auto;
+ padding: 4px 0;
+ }
+ .msd-dropdown.open .msd-list { display: block; }
+ .msd-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 12px;
+ cursor: pointer;
+ }
+ .msd-item:hover { background: var(--surface-hover); }
+ .msd-item input[type=checkbox] { margin: 0; cursor: pointer; }
+ .msd-empty { padding: 8px 12px; color: var(--text-muted); font-size: 0.8rem; }
+
/* 响应式 */
@media (max-width: 1024px) {
.two-column-layout {
@@ -231,8 +278,7 @@
上传到 CPA
@@ -241,8 +287,7 @@
上传到 Sub2API
@@ -251,8 +296,7 @@
上传到 Team Manager
diff --git a/templates/settings.html b/templates/settings.html
index 82a89ed..91cda3e 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -214,11 +214,11 @@
- | 名称 |
+ 名称 |
API URL |
状态 |
- 优先级 |
- 操作 |
+ 优先级 |
+ 操作 |
@@ -240,11 +240,11 @@
- | 名称 |
+ 名称 |
API URL |
状态 |
- 优先级 |
- 操作 |
+ 优先级 |
+ 操作 |
@@ -266,11 +266,11 @@
- | 名称 |
+ 名称 |
API URL |
状态 |
- 优先级 |
- 操作 |
+ 优先级 |
+ 操作 |