mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-07-07 15:21:39 +08:00
feat(accounts): 增加全选所有页功能并重构批量操作API
- 前端增加全选所有页横幅和状态管理 - 后端批量API支持select_all参数和筛选条件传递 - 统一批量操作逻辑,支持全选和筛选条件组合
This commit is contained in:
@@ -60,7 +60,11 @@ class AccountUpdateRequest(BaseModel):
|
|||||||
|
|
||||||
class BatchDeleteRequest(BaseModel):
|
class BatchDeleteRequest(BaseModel):
|
||||||
"""批量删除请求"""
|
"""批量删除请求"""
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class BatchUpdateRequest(BaseModel):
|
class BatchUpdateRequest(BaseModel):
|
||||||
@@ -71,6 +75,30 @@ class BatchUpdateRequest(BaseModel):
|
|||||||
|
|
||||||
# ============== Helper Functions ==============
|
# ============== Helper Functions ==============
|
||||||
|
|
||||||
|
def resolve_account_ids(
|
||||||
|
db,
|
||||||
|
ids: List[int],
|
||||||
|
select_all: bool = False,
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
email_service_filter: Optional[str] = None,
|
||||||
|
search_filter: Optional[str] = None,
|
||||||
|
) -> List[int]:
|
||||||
|
"""当 select_all=True 时查询全部符合条件的 ID,否则直接返回传入的 ids"""
|
||||||
|
if not select_all:
|
||||||
|
return ids
|
||||||
|
query = db.query(Account.id)
|
||||||
|
if status_filter:
|
||||||
|
query = query.filter(Account.status == status_filter)
|
||||||
|
if email_service_filter:
|
||||||
|
query = query.filter(Account.email_service == email_service_filter)
|
||||||
|
if search_filter:
|
||||||
|
pattern = f"%{search_filter}%"
|
||||||
|
query = query.filter(
|
||||||
|
(Account.email.ilike(pattern)) | (Account.account_id.ilike(pattern))
|
||||||
|
)
|
||||||
|
return [row[0] for row in query.all()]
|
||||||
|
|
||||||
|
|
||||||
def account_to_response(account: Account) -> AccountResponse:
|
def account_to_response(account: Account) -> AccountResponse:
|
||||||
"""转换 Account 模型为响应模型"""
|
"""转换 Account 模型为响应模型"""
|
||||||
return AccountResponse(
|
return AccountResponse(
|
||||||
@@ -162,9 +190,9 @@ async def get_account_tokens(account_id: int):
|
|||||||
return {
|
return {
|
||||||
"id": account.id,
|
"id": account.id,
|
||||||
"email": account.email,
|
"email": account.email,
|
||||||
"access_token": account.access_token[:50] + "..." if account.access_token else None,
|
"access_token": account.access_token,
|
||||||
"refresh_token": account.refresh_token[:50] + "..." if account.refresh_token else None,
|
"refresh_token": account.refresh_token,
|
||||||
"id_token": account.id_token[:50] + "..." if account.id_token else None,
|
"id_token": account.id_token,
|
||||||
"has_tokens": bool(account.access_token and account.refresh_token),
|
"has_tokens": bool(account.access_token and account.refresh_token),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,10 +236,14 @@ async def delete_account(account_id: int):
|
|||||||
async def batch_delete_accounts(request: BatchDeleteRequest):
|
async def batch_delete_accounts(request: BatchDeleteRequest):
|
||||||
"""批量删除账号"""
|
"""批量删除账号"""
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for account_id in request.ids:
|
for account_id in ids:
|
||||||
try:
|
try:
|
||||||
account = crud.get_account_by_id(db, account_id)
|
account = crud.get_account_by_id(db, account_id)
|
||||||
if account:
|
if account:
|
||||||
@@ -255,14 +287,22 @@ async def batch_update_accounts(request: BatchUpdateRequest):
|
|||||||
|
|
||||||
class BatchExportRequest(BaseModel):
|
class BatchExportRequest(BaseModel):
|
||||||
"""批量导出请求"""
|
"""批量导出请求"""
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/export/json")
|
@router.post("/export/json")
|
||||||
async def export_accounts_json(request: BatchExportRequest):
|
async def export_accounts_json(request: BatchExportRequest):
|
||||||
"""导出账号为 JSON 格式"""
|
"""导出账号为 JSON 格式"""
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
accounts = db.query(Account).filter(Account.id.in_(request.ids)).all()
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
accounts = db.query(Account).filter(Account.id.in_(ids)).all()
|
||||||
|
|
||||||
export_data = []
|
export_data = []
|
||||||
for acc in accounts:
|
for acc in accounts:
|
||||||
@@ -304,7 +344,11 @@ async def export_accounts_csv(request: BatchExportRequest):
|
|||||||
import io
|
import io
|
||||||
|
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
accounts = db.query(Account).filter(Account.id.in_(request.ids)).all()
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
accounts = db.query(Account).filter(Account.id.in_(ids)).all()
|
||||||
|
|
||||||
# 创建 CSV 内容
|
# 创建 CSV 内容
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
@@ -357,7 +401,11 @@ async def export_accounts_cpa(request: BatchExportRequest):
|
|||||||
from ...core.cpa_upload import generate_token_json
|
from ...core.cpa_upload import generate_token_json
|
||||||
|
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
accounts = db.query(Account).filter(Account.id.in_(request.ids)).all()
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
accounts = db.query(Account).filter(Account.id.in_(ids)).all()
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
@@ -427,8 +475,12 @@ class TokenRefreshRequest(BaseModel):
|
|||||||
|
|
||||||
class BatchRefreshRequest(BaseModel):
|
class BatchRefreshRequest(BaseModel):
|
||||||
"""批量刷新请求"""
|
"""批量刷新请求"""
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
proxy: Optional[str] = None
|
proxy: Optional[str] = None
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TokenValidateRequest(BaseModel):
|
class TokenValidateRequest(BaseModel):
|
||||||
@@ -438,8 +490,12 @@ class TokenValidateRequest(BaseModel):
|
|||||||
|
|
||||||
class BatchValidateRequest(BaseModel):
|
class BatchValidateRequest(BaseModel):
|
||||||
"""批量验证请求"""
|
"""批量验证请求"""
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
proxy: Optional[str] = None
|
proxy: Optional[str] = None
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{account_id}/refresh")
|
@router.post("/{account_id}/refresh")
|
||||||
@@ -478,7 +534,13 @@ async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: B
|
|||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
|
|
||||||
for account_id in request.ids:
|
with get_db() as db:
|
||||||
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
for account_id in ids:
|
||||||
try:
|
try:
|
||||||
result = do_refresh(account_id, proxy)
|
result = do_refresh(account_id, proxy)
|
||||||
if result.success:
|
if result.success:
|
||||||
@@ -523,7 +585,13 @@ async def batch_validate_tokens(request: BatchValidateRequest):
|
|||||||
"details": []
|
"details": []
|
||||||
}
|
}
|
||||||
|
|
||||||
for account_id in request.ids:
|
with get_db() as db:
|
||||||
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
for account_id in ids:
|
||||||
try:
|
try:
|
||||||
is_valid, error = do_validate(account_id, proxy)
|
is_valid, error = do_validate(account_id, proxy)
|
||||||
results["details"].append({
|
results["details"].append({
|
||||||
@@ -555,8 +623,12 @@ class CPAUploadRequest(BaseModel):
|
|||||||
|
|
||||||
class BatchCPAUploadRequest(BaseModel):
|
class BatchCPAUploadRequest(BaseModel):
|
||||||
"""批量 CPA 上传请求"""
|
"""批量 CPA 上传请求"""
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
proxy: Optional[str] = None
|
proxy: Optional[str] = None
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{account_id}/upload-cpa")
|
@router.post("/{account_id}/upload-cpa")
|
||||||
@@ -609,6 +681,12 @@ async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
|||||||
# 使用传入的代理或全局代理配置
|
# 使用传入的代理或全局代理配置
|
||||||
proxy = request.proxy if request.proxy else get_settings().proxy_url
|
proxy = request.proxy if request.proxy else get_settings().proxy_url
|
||||||
|
|
||||||
results = batch_upload_to_cpa(request.ids, proxy)
|
with get_db() as db:
|
||||||
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
results = batch_upload_to_cpa(ids, proxy)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from pydantic import BaseModel
|
|||||||
from ...database.session import get_db
|
from ...database.session import get_db
|
||||||
from ...database.models import Account
|
from ...database.models import Account
|
||||||
from ...config.settings import get_settings
|
from ...config.settings import get_settings
|
||||||
|
from .accounts import resolve_account_ids
|
||||||
from ...core.payment import (
|
from ...core.payment import (
|
||||||
generate_plus_link,
|
generate_plus_link,
|
||||||
generate_team_link,
|
generate_team_link,
|
||||||
@@ -48,8 +49,12 @@ class MarkSubscriptionRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class BatchCheckSubscriptionRequest(BaseModel):
|
class BatchCheckSubscriptionRequest(BaseModel):
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
proxy: Optional[str] = None
|
proxy: Optional[str] = None
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UploadTMRequest(BaseModel):
|
class UploadTMRequest(BaseModel):
|
||||||
@@ -57,7 +62,11 @@ class UploadTMRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class BatchUploadTMRequest(BaseModel):
|
class BatchUploadTMRequest(BaseModel):
|
||||||
ids: List[int]
|
ids: List[int] = []
|
||||||
|
select_all: bool = False
|
||||||
|
status_filter: Optional[str] = None
|
||||||
|
email_service_filter: Optional[str] = None
|
||||||
|
search_filter: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# ============== 支付链接生成 ==============
|
# ============== 支付链接生成 ==============
|
||||||
@@ -143,7 +152,11 @@ def batch_check_subscription(request: BatchCheckSubscriptionRequest):
|
|||||||
results = {"success_count": 0, "failed_count": 0, "details": []}
|
results = {"success_count": 0, "failed_count": 0, "details": []}
|
||||||
|
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
for account_id in request.ids:
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
for account_id in ids:
|
||||||
account = db.query(Account).filter(Account.id == account_id).first()
|
account = db.query(Account).filter(Account.id == account_id).first()
|
||||||
if not account:
|
if not account:
|
||||||
results["failed_count"] += 1
|
results["failed_count"] += 1
|
||||||
@@ -201,5 +214,11 @@ def batch_upload_tm(request: BatchUploadTMRequest):
|
|||||||
api_url = settings.tm_api_url
|
api_url = settings.tm_api_url
|
||||||
api_key = settings.tm_api_key.get_secret_value() if settings.tm_api_key else ""
|
api_key = settings.tm_api_key.get_secret_value() if settings.tm_api_key else ""
|
||||||
|
|
||||||
results = batch_upload_to_team_manager(request.ids, api_url, api_key)
|
with get_db() as db:
|
||||||
|
ids = resolve_account_ids(
|
||||||
|
db, request.ids, request.select_all,
|
||||||
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
results = batch_upload_to_team_manager(ids, api_url, api_key)
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ let pageSize = 20;
|
|||||||
let totalAccounts = 0;
|
let totalAccounts = 0;
|
||||||
let selectedAccounts = new Set();
|
let selectedAccounts = new Set();
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
let selectAllPages = false; // 是否选中了全部页
|
||||||
|
let currentFilters = { status: '', email_service: '', search: '' }; // 当前筛选条件
|
||||||
|
|
||||||
// DOM 元素
|
// DOM 元素
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -44,6 +46,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadAccounts();
|
loadAccounts();
|
||||||
initEventListeners();
|
initEventListeners();
|
||||||
updateBatchButtons(); // 初始化按钮状态
|
updateBatchButtons(); // 初始化按钮状态
|
||||||
|
renderSelectAllBanner();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 事件监听
|
// 事件监听
|
||||||
@@ -51,17 +54,20 @@ function initEventListeners() {
|
|||||||
// 筛选
|
// 筛选
|
||||||
elements.filterStatus.addEventListener('change', () => {
|
elements.filterStatus.addEventListener('change', () => {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
resetSelectAllPages();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.filterService.addEventListener('change', () => {
|
elements.filterService.addEventListener('change', () => {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
resetSelectAllPages();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 搜索(防抖)
|
// 搜索(防抖)
|
||||||
elements.searchInput.addEventListener('input', debounce(() => {
|
elements.searchInput.addEventListener('input', debounce(() => {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
resetSelectAllPages();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
}, 300));
|
}, 300));
|
||||||
|
|
||||||
@@ -70,6 +76,7 @@ function initEventListeners() {
|
|||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
elements.searchInput.blur();
|
elements.searchInput.blur();
|
||||||
elements.searchInput.value = '';
|
elements.searchInput.value = '';
|
||||||
|
resetSelectAllPages();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -99,7 +106,7 @@ function initEventListeners() {
|
|||||||
// 批量删除
|
// 批量删除
|
||||||
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
||||||
|
|
||||||
// 全选
|
// 全选(当前页)
|
||||||
elements.selectAll.addEventListener('change', (e) => {
|
elements.selectAll.addEventListener('change', (e) => {
|
||||||
const checkboxes = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
const checkboxes = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
||||||
checkboxes.forEach(cb => {
|
checkboxes.forEach(cb => {
|
||||||
@@ -111,7 +118,11 @@ function initEventListeners() {
|
|||||||
selectedAccounts.delete(id);
|
selectedAccounts.delete(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!e.target.checked) {
|
||||||
|
selectAllPages = false;
|
||||||
|
}
|
||||||
updateBatchButtons();
|
updateBatchButtons();
|
||||||
|
renderSelectAllBanner();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
@@ -204,21 +215,26 @@ async function loadAccounts() {
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 记录当前筛选条件
|
||||||
|
currentFilters.status = elements.filterStatus.value;
|
||||||
|
currentFilters.email_service = elements.filterService.value;
|
||||||
|
currentFilters.search = elements.searchInput.value.trim();
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (elements.filterStatus.value) {
|
if (currentFilters.status) {
|
||||||
params.append('status', elements.filterStatus.value);
|
params.append('status', currentFilters.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elements.filterService.value) {
|
if (currentFilters.email_service) {
|
||||||
params.append('email_service', elements.filterService.value);
|
params.append('email_service', currentFilters.email_service);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elements.searchInput.value.trim()) {
|
if (currentFilters.search) {
|
||||||
params.append('search', elements.searchInput.value.trim());
|
params.append('search', currentFilters.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -336,10 +352,24 @@ function renderAccounts(accounts) {
|
|||||||
selectedAccounts.add(id);
|
selectedAccounts.add(id);
|
||||||
} else {
|
} else {
|
||||||
selectedAccounts.delete(id);
|
selectedAccounts.delete(id);
|
||||||
|
selectAllPages = false;
|
||||||
}
|
}
|
||||||
|
// 同步全选框状态
|
||||||
|
const allChecked = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
||||||
|
const checkedCount = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked').length;
|
||||||
|
elements.selectAll.checked = allChecked.length > 0 && checkedCount === allChecked.length;
|
||||||
|
elements.selectAll.indeterminate = checkedCount > 0 && checkedCount < allChecked.length;
|
||||||
updateBatchButtons();
|
updateBatchButtons();
|
||||||
|
renderSelectAllBanner();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 渲染后同步全选框状态
|
||||||
|
const allCbs = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
||||||
|
const checkedCbs = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked');
|
||||||
|
elements.selectAll.checked = allCbs.length > 0 && checkedCbs.length === allCbs.length;
|
||||||
|
elements.selectAll.indeterminate = checkedCbs.length > 0 && checkedCbs.length < allCbs.length;
|
||||||
|
renderSelectAllBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换密码显示
|
// 切换密码显示
|
||||||
@@ -365,9 +395,73 @@ function updatePagination() {
|
|||||||
elements.pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
|
elements.pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置全选所有页状态
|
||||||
|
function resetSelectAllPages() {
|
||||||
|
selectAllPages = false;
|
||||||
|
selectedAccounts.clear();
|
||||||
|
updateBatchButtons();
|
||||||
|
renderSelectAllBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建批量请求体(含 select_all 和筛选参数)
|
||||||
|
function buildBatchPayload(extraFields = {}) {
|
||||||
|
if (selectAllPages) {
|
||||||
|
return {
|
||||||
|
ids: [],
|
||||||
|
select_all: true,
|
||||||
|
status_filter: currentFilters.status || null,
|
||||||
|
email_service_filter: currentFilters.email_service || null,
|
||||||
|
search_filter: currentFilters.search || null,
|
||||||
|
...extraFields
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ids: Array.from(selectedAccounts), ...extraFields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取有效选中数量(select_all 时用总数)
|
||||||
|
function getEffectiveCount() {
|
||||||
|
return selectAllPages ? totalAccounts : selectedAccounts.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染全选横幅
|
||||||
|
function renderSelectAllBanner() {
|
||||||
|
let banner = document.getElementById('select-all-banner');
|
||||||
|
const totalPages = Math.ceil(totalAccounts / pageSize);
|
||||||
|
const currentPageSize = elements.table.querySelectorAll('input[type="checkbox"][data-id]').length;
|
||||||
|
const checkedOnPage = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked').length;
|
||||||
|
const allPageSelected = currentPageSize > 0 && checkedOnPage === currentPageSize;
|
||||||
|
|
||||||
|
// 只在全选了当前页且有多页时显示横幅
|
||||||
|
if (!allPageSelected || totalPages <= 1 || totalAccounts <= pageSize) {
|
||||||
|
if (banner) banner.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!banner) {
|
||||||
|
banner = document.createElement('div');
|
||||||
|
banner.id = 'select-all-banner';
|
||||||
|
banner.style.cssText = 'background:var(--primary-light,#e8f0fe);color:var(--primary-color,#1a73e8);padding:8px 16px;text-align:center;font-size:0.875rem;border-bottom:1px solid var(--border-color);';
|
||||||
|
const tableContainer = document.querySelector('.table-container');
|
||||||
|
if (tableContainer) tableContainer.insertAdjacentElement('beforebegin', banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectAllPages) {
|
||||||
|
banner.innerHTML = `已选中全部 <strong>${totalAccounts}</strong> 条记录。<button onclick="resetSelectAllPages()" style="margin-left:8px;color:var(--primary-color,#1a73e8);background:none;border:none;cursor:pointer;text-decoration:underline;">取消全选</button>`;
|
||||||
|
} else {
|
||||||
|
banner.innerHTML = `当前页已全选 <strong>${checkedOnPage}</strong> 条。<button onclick="selectAllPagesAction()" style="margin-left:8px;color:var(--primary-color,#1a73e8);background:none;border:none;cursor:pointer;text-decoration:underline;">选择全部 ${totalAccounts} 条</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中所有页
|
||||||
|
function selectAllPagesAction() {
|
||||||
|
selectAllPages = true;
|
||||||
|
updateBatchButtons();
|
||||||
|
renderSelectAllBanner();
|
||||||
|
}
|
||||||
|
|
||||||
// 更新批量操作按钮
|
// 更新批量操作按钮
|
||||||
function updateBatchButtons() {
|
function updateBatchButtons() {
|
||||||
const count = selectedAccounts.size;
|
const count = getEffectiveCount();
|
||||||
elements.batchDeleteBtn.disabled = count === 0;
|
elements.batchDeleteBtn.disabled = count === 0;
|
||||||
elements.batchRefreshBtn.disabled = count === 0;
|
elements.batchRefreshBtn.disabled = count === 0;
|
||||||
elements.batchValidateBtn.disabled = count === 0;
|
elements.batchValidateBtn.disabled = count === 0;
|
||||||
@@ -403,19 +497,17 @@ async function refreshToken(id) {
|
|||||||
|
|
||||||
// 批量刷新Token
|
// 批量刷新Token
|
||||||
async function handleBatchRefresh() {
|
async function handleBatchRefresh() {
|
||||||
if (selectedAccounts.size === 0) return;
|
const count = getEffectiveCount();
|
||||||
|
if (count === 0) return;
|
||||||
|
|
||||||
const confirmed = await confirm(`确定要刷新选中的 ${selectedAccounts.size} 个账号的Token吗?`);
|
const confirmed = await confirm(`确定要刷新选中的 ${count} 个账号的Token吗?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
elements.batchRefreshBtn.disabled = true;
|
elements.batchRefreshBtn.disabled = true;
|
||||||
elements.batchRefreshBtn.textContent = '刷新中...';
|
elements.batchRefreshBtn.textContent = '刷新中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/accounts/batch-refresh', {
|
const result = await api.post('/accounts/batch-refresh', buildBatchPayload());
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(`成功刷新 ${result.success_count} 个,失败 ${result.failed_count} 个`);
|
toast.success(`成功刷新 ${result.success_count} 个,失败 ${result.failed_count} 个`);
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -427,16 +519,13 @@ async function handleBatchRefresh() {
|
|||||||
|
|
||||||
// 批量验证Token
|
// 批量验证Token
|
||||||
async function handleBatchValidate() {
|
async function handleBatchValidate() {
|
||||||
if (selectedAccounts.size === 0) return;
|
if (getEffectiveCount() === 0) return;
|
||||||
|
|
||||||
elements.batchValidateBtn.disabled = true;
|
elements.batchValidateBtn.disabled = true;
|
||||||
elements.batchValidateBtn.textContent = '验证中...';
|
elements.batchValidateBtn.textContent = '验证中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/accounts/batch-validate', {
|
const result = await api.post('/accounts/batch-validate', buildBatchPayload());
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
|
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -561,18 +650,17 @@ async function deleteAccount(id, email) {
|
|||||||
|
|
||||||
// 批量删除
|
// 批量删除
|
||||||
async function handleBatchDelete() {
|
async function handleBatchDelete() {
|
||||||
if (selectedAccounts.size === 0) return;
|
const count = getEffectiveCount();
|
||||||
|
if (count === 0) return;
|
||||||
|
|
||||||
const confirmed = await confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?此操作不可恢复。`);
|
const confirmed = await confirm(`确定要删除选中的 ${count} 个账号吗?此操作不可恢复。`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/accounts/batch-delete', {
|
const result = await api.post('/accounts/batch-delete', buildBatchPayload());
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(`成功删除 ${result.deleted_count} 个账号`);
|
toast.success(`成功删除 ${result.deleted_count} 个账号`);
|
||||||
selectedAccounts.clear();
|
selectedAccounts.clear();
|
||||||
|
selectAllPages = false;
|
||||||
loadStats();
|
loadStats();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -582,12 +670,13 @@ async function handleBatchDelete() {
|
|||||||
|
|
||||||
// 导出账号
|
// 导出账号
|
||||||
async function exportAccounts(format) {
|
async function exportAccounts(format) {
|
||||||
if (selectedAccounts.size === 0) {
|
const count = getEffectiveCount();
|
||||||
|
if (count === 0) {
|
||||||
toast.warning('请先选择要导出的账号');
|
toast.warning('请先选择要导出的账号');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.info(`正在导出 ${selectedAccounts.size} 个账号...`);
|
toast.info(`正在导出 ${count} 个账号...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/accounts/export/' + format, {
|
const response = await fetch('/api/accounts/export/' + format, {
|
||||||
@@ -595,9 +684,7 @@ async function exportAccounts(format) {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(buildBatchPayload())
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -661,18 +748,17 @@ async function uploadToCpa(id) {
|
|||||||
|
|
||||||
// 批量上传到CPA
|
// 批量上传到CPA
|
||||||
async function handleBatchUploadCpa() {
|
async function handleBatchUploadCpa() {
|
||||||
if (selectedAccounts.size === 0) return;
|
const count = getEffectiveCount();
|
||||||
|
if (count === 0) return;
|
||||||
|
|
||||||
const confirmed = await confirm(`确定要将选中的 ${selectedAccounts.size} 个账号上传到CPA吗?`);
|
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到CPA吗?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
elements.batchUploadCpaBtn.disabled = true;
|
elements.batchUploadCpaBtn.disabled = true;
|
||||||
elements.batchUploadCpaBtn.textContent = '上传中...';
|
elements.batchUploadCpaBtn.textContent = '上传中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/accounts/batch-upload-cpa', {
|
const result = await api.post('/accounts/batch-upload-cpa', buildBatchPayload());
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
});
|
|
||||||
|
|
||||||
let message = `成功: ${result.success_count}`;
|
let message = `成功: ${result.success_count}`;
|
||||||
if (result.failed_count > 0) {
|
if (result.failed_count > 0) {
|
||||||
@@ -714,17 +800,16 @@ async function markSubscription(id) {
|
|||||||
|
|
||||||
// 批量检测订阅状态
|
// 批量检测订阅状态
|
||||||
async function handleBatchCheckSubscription() {
|
async function handleBatchCheckSubscription() {
|
||||||
if (selectedAccounts.size === 0) return;
|
const count = getEffectiveCount();
|
||||||
const confirmed = await confirm(`确定要检测选中的 ${selectedAccounts.size} 个账号的订阅状态吗?`);
|
if (count === 0) return;
|
||||||
|
const confirmed = await confirm(`确定要检测选中的 ${count} 个账号的订阅状态吗?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
elements.batchCheckSubBtn.disabled = true;
|
elements.batchCheckSubBtn.disabled = true;
|
||||||
elements.batchCheckSubBtn.textContent = '检测中...';
|
elements.batchCheckSubBtn.textContent = '检测中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/payment/accounts/batch-check-subscription', {
|
const result = await api.post('/payment/accounts/batch-check-subscription', buildBatchPayload());
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
});
|
|
||||||
let message = `成功: ${result.success_count}`;
|
let message = `成功: ${result.success_count}`;
|
||||||
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
@@ -755,17 +840,16 @@ async function uploadToTm(id) {
|
|||||||
|
|
||||||
// 批量上传到 Team Manager
|
// 批量上传到 Team Manager
|
||||||
async function handleBatchUploadTm() {
|
async function handleBatchUploadTm() {
|
||||||
if (selectedAccounts.size === 0) return;
|
const count = getEffectiveCount();
|
||||||
const confirmed = await confirm(`确定要将选中的 ${selectedAccounts.size} 个账号上传到 Team Manager 吗?`);
|
if (count === 0) return;
|
||||||
|
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Team Manager 吗?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
elements.batchUploadTmBtn.disabled = true;
|
elements.batchUploadTmBtn.disabled = true;
|
||||||
elements.batchUploadTmBtn.textContent = '上传中...';
|
elements.batchUploadTmBtn.textContent = '上传中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/payment/accounts/batch-upload-tm', {
|
const result = await api.post('/payment/accounts/batch-upload-tm', buildBatchPayload());
|
||||||
ids: Array.from(selectedAccounts)
|
|
||||||
});
|
|
||||||
let message = `成功: ${result.success_count}`;
|
let message = `成功: ${result.success_count}`;
|
||||||
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
||||||
if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`;
|
if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user