|
<!DOCTYPE html> |
|
<html lang="zh-CN"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>JetBrains激活链接管理系统</title> |
|
|
|
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/index.css" /> |
|
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script> |
|
<script src="https://unpkg.com/[email protected]"></script> |
|
<script src="https://unpkg.com/@element-plus/[email protected]"></script> |
|
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script> |
|
<style> |
|
body { |
|
margin: 0; |
|
padding: 0; |
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; |
|
-webkit-font-smoothing: antialiased; |
|
-moz-osx-font-smoothing: grayscale; |
|
background-color: #f5f7fa; |
|
color: #303133; |
|
} |
|
.app-container { |
|
padding: 8px; |
|
} |
|
.dashboard-card { |
|
margin-bottom: 20px; |
|
border-radius: 4px; |
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
|
background-color: #fff; |
|
} |
|
.dashboard-header { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
padding: 10px 20px; |
|
border-bottom: 1px solid #ebeef5; |
|
} |
|
.el-table .status-column { |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
.el-table .status-0 { |
|
color: #909399; |
|
} |
|
.el-table .status-1 { |
|
color: #409EFF; |
|
} |
|
.el-table .status-2 { |
|
color: #F56C6C; |
|
} |
|
.el-table .status-3 { |
|
color: #67C23A; |
|
} |
|
.operation-btn { |
|
margin-right: 5px; |
|
} |
|
.statistics-card { |
|
text-align: center; |
|
padding: 20px; |
|
} |
|
.statistics-value { |
|
font-size: 36px; |
|
font-weight: bold; |
|
} |
|
.statistics-label { |
|
font-size: 14px; |
|
color: #909399; |
|
} |
|
.task-card { |
|
margin-bottom: 15px; |
|
} |
|
.copy-button { |
|
margin-left: 5px; |
|
} |
|
.el-tag { |
|
margin: 2px; |
|
} |
|
.tab-container { |
|
padding: 15px; |
|
} |
|
.sr-only { |
|
position: absolute; |
|
width: 1px; |
|
height: 1px; |
|
padding: 0; |
|
margin: -1px; |
|
overflow: hidden; |
|
clip: rect(0, 0, 0, 0); |
|
white-space: nowrap; |
|
border-width: 0; |
|
} |
|
.required-field::after { |
|
content: ' *'; |
|
color: #F56C6C; |
|
} |
|
.el-table .id-column, |
|
.el-table .register-time-column, |
|
.el-table .username-column, |
|
.el-table .email-column { |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
.el-table .email-column { |
|
max-width: 250px; |
|
} |
|
|
|
.el-table { |
|
border: 1px solid #dcdfe6; |
|
} |
|
.el-table th { |
|
background-color: #f5f7fa !important; |
|
color: #606266; |
|
font-weight: bold; |
|
text-align: center !important; |
|
} |
|
.el-table td { |
|
text-align: center; |
|
} |
|
|
|
.el-table .id-column { |
|
min-width: 80px !important; |
|
width: 80px !important; |
|
} |
|
.el-table .register-time-column { |
|
min-width: 180px !important; |
|
width: 180px !important; |
|
} |
|
.el-table .username-column { |
|
min-width: 250px !important; |
|
} |
|
.el-table .email-column { |
|
min-width: 180px !important; |
|
} |
|
|
|
.required::after { |
|
content: '*'; |
|
color: #F56C6C; |
|
margin-left: 4px; |
|
} |
|
@media (max-width: 900px) { |
|
.app-container { |
|
padding: 2px !important; |
|
} |
|
.dashboard-card { |
|
margin-bottom: 8px; |
|
padding: 0 2px; |
|
} |
|
.statistics-card { |
|
border-radius: 10px; |
|
box-shadow: 0 2px 8px #eee; |
|
padding: 10px 0; |
|
text-align: center !important; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
.statistics-value { |
|
font-size: 28px; |
|
text-align: center !important; |
|
width: 100%; |
|
margin: 0 auto; |
|
} |
|
.statistics-label { |
|
font-size: 16px; |
|
text-align: center !important; |
|
width: 100%; |
|
margin: 0 auto; |
|
} |
|
.tab-container { |
|
padding: 4px; |
|
} |
|
.el-row.filter-row { |
|
margin: 0 -2px; |
|
} |
|
.el-col.filter-col { |
|
margin-bottom: 4px; |
|
padding: 0 2px; |
|
} |
|
.el-table { |
|
border-radius: 6px; |
|
box-shadow: 0 1px 4px #eee; |
|
font-size: 15px; |
|
} |
|
.el-table th, .el-table td { |
|
padding-left: 2px; |
|
padding-right: 2px; |
|
} |
|
.el-pagination { |
|
font-size: 15px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<div id="app" class="app-container"> |
|
<el-container> |
|
<el-header style="height: auto; padding: 0; margin-bottom: 20px;"> |
|
<div class="dashboard-card"> |
|
<div class="dashboard-header"> |
|
<h2>JetBrains激活链接管理系统</h2> |
|
<div> |
|
<el-button type="primary" @click="showImportDialog">导入数据</el-button> |
|
<el-button type="success" @click="showExportDialog">导出数据</el-button> |
|
<el-button type="danger" @click="resetFailedAccounts">重置失败账号</el-button> |
|
<el-button type="warning" @click="logout" style="margin-left: 10px;">退出登录</el-button> |
|
</div> |
|
</div> |
|
</div> |
|
</el-header> |
|
|
|
<el-main> |
|
|
|
<el-row :gutter="20"> |
|
<el-col :span="4"> |
|
<el-card shadow="hover" class="statistics-card"> |
|
<div class="statistics-value">{{ statistics.total }}</div> |
|
<div class="statistics-label">总数</div> |
|
</el-card> |
|
</el-col> |
|
|
|
|
|
|
|
|
|
|
|
|
|
<el-col :span="5"> |
|
<el-card shadow="hover" class="statistics-card" style="background-color: #f9f9e3;"> |
|
<div class="statistics-value" style="color: #e6a23c;">{{ statistics.unused }}</div> |
|
<div class="statistics-label">未用</div> |
|
</el-card> |
|
</el-col> |
|
<el-col :span="5"> |
|
<el-card shadow="hover" class="statistics-card" style="background-color: #ecf5ff;"> |
|
<div class="statistics-value" style="color: #409EFF;">{{ statistics.submitted }}</div> |
|
<div class="statistics-label">提交</div> |
|
</el-card> |
|
</el-col> |
|
<el-col :span="5"> |
|
<el-card shadow="hover" class="statistics-card" style="background-color: #fef0f0;"> |
|
<div class="statistics-value" style="color: #F56C6C;">{{ statistics.failed }}</div> |
|
<div class="statistics-label">失败</div> |
|
</el-card> |
|
</el-col> |
|
<el-col :span="5"> |
|
<el-card shadow="hover" class="statistics-card" style="background-color: #f4f4f5;"> |
|
<div class="statistics-value" style="color: #909399;">{{ statistics.pending }}</div> |
|
<div class="statistics-label">未取</div> |
|
</el-card> |
|
</el-col> |
|
</el-row> |
|
|
|
|
|
<el-tabs type="border-card" v-model="activeTab" @tab-click="handleTabClick"> |
|
|
|
<el-tab-pane label="账号管理" name="accounts"> |
|
<div class="tab-container"> |
|
<div style="margin-bottom: 20px;"> |
|
<el-row :gutter="10" class="filter-row" style="flex-wrap: nowrap; display: flex;"> |
|
<el-col :span="3" class="filter-col" style="min-width: 120px;"> |
|
<el-select v-model="accountsFilter.status" placeholder="选择状态" clearable style="width: 100%;" @change="handleStatusChange"> |
|
<el-option label="未提交" :value="0"></el-option> |
|
<el-option label="已提交" :value="1"></el-option> |
|
<el-option label="提交失败" :value="2"></el-option> |
|
<el-option label="已提取链接" :value="3"></el-option> |
|
</el-select> |
|
</el-col> |
|
<el-col :span="3" class="filter-col" style="min-width: 140px;"> |
|
<el-date-picker v-model="accountsFilter.registerTimeStart" type="date" placeholder="注册起始日期" style="width: 100%;" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker> |
|
</el-col> |
|
<el-col :span="3" class="filter-col" style="min-width: 140px;"> |
|
<el-date-picker v-model="accountsFilter.registerTimeEnd" type="date" placeholder="注册结束日期" style="width: 100%;" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker> |
|
</el-col> |
|
<el-col :span="2" class="filter-col" style="min-width: 90px;"> |
|
<el-input v-model="accountsFilter.idMin" placeholder="最小ID" clearable></el-input> |
|
</el-col> |
|
<el-col :span="2" class="filter-col" style="min-width: 90px;"> |
|
<el-input v-model="accountsFilter.idMax" placeholder="最大ID" clearable></el-input> |
|
</el-col> |
|
<el-col :span="3" class="filter-col" style="min-width: 120px;"> |
|
<el-select v-model="accountsFilter.hasActivationLink" placeholder="激活链接" clearable style="width: 100%;"> |
|
<el-option label="全部" value=""></el-option> |
|
<el-option label="有激活链接" value="yes"></el-option> |
|
<el-option label="无激活链接" value="no"></el-option> |
|
</el-select> |
|
</el-col> |
|
<el-col :span="3" class="filter-col" style="min-width: 120px;"> |
|
<el-select v-model="accountsFilter.used" placeholder="使用状态" clearable style="width: 100%;"> |
|
<el-option label="全部" :value="''"></el-option> |
|
<el-option label="未被使用" :value="0"></el-option> |
|
<el-option label="已被使用" :value="1"></el-option> |
|
</el-select> |
|
</el-col> |
|
<el-col :span="4" class="filter-col" style="min-width: 160px;"> |
|
<el-input v-model="accountsFilter.search" placeholder="邮箱/备注" clearable></el-input> |
|
</el-col> |
|
</el-row> |
|
</div> |
|
|
|
<el-row :gutter="20" style="margin-bottom: 10px;"> |
|
<el-col :span="24" style="text-align: right;"> |
|
<el-button type="primary" :disabled="selectedAccounts.length === 0" @click="batchToggleUsed(1)"> |
|
批量标记为已使用 |
|
</el-button> |
|
<el-button type="info" :disabled="selectedAccounts.length === 0" @click="batchToggleUsed(0)"> |
|
批量标记为未使用 |
|
</el-button> |
|
</el-col> |
|
</el-row> |
|
|
|
<el-table |
|
:data="accounts" |
|
style="width: 100%;overflow-x:auto;" |
|
v-loading="loading.accounts" |
|
@selection-change="handleSelectionChange" |
|
border |
|
stripe |
|
highlight-current-row |
|
> |
|
<el-table-column type="selection" width="50"> </el-table-column> |
|
<el-table-column label="ID" width="80" sortable> |
|
<template #default="scope"> |
|
{{ scope.row.id || '-' }} |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="注册时间" width="180" sortable> |
|
<template #default="scope"> |
|
{{ scope.row.register_time || '-' }} |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="用户名" min-width="250" show-overflow-tooltip> |
|
<template #default="scope"> |
|
{{ scope.row.username || '-' }} |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="状态" width="120"> |
|
<template #default="scope"> |
|
<el-tag :type="getStatusType(scope.row.status)" size="small"> |
|
{{ getStatusText(scope.row.status) }} |
|
</el-tag> |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="激活链接" min-width="300"> |
|
<template #default="scope"> |
|
<div v-if="scope.row.activation_link" style="display: flex; align-items: center;"> |
|
<el-tooltip :content="scope.row.activation_link" placement="top" effect="light"> |
|
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px;"> |
|
{{ scope.row.activation_link }} |
|
</span> |
|
</el-tooltip> |
|
<el-button |
|
size="small" |
|
type="primary" |
|
class="copy-button" |
|
@click="copyToClipboard(scope.row.activation_link, scope.row)" |
|
> |
|
复制 |
|
</el-button> |
|
</div> |
|
<span v-else>-</span> |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="已被使用" width="100"> |
|
<template #default="scope"> |
|
<el-tag :type="scope.row.used ? 'success' : 'info'" @click="toggleAccountUsed(scope.row)" style="cursor:pointer;"> |
|
{{ scope.row.used ? '✔ 已使用' : '未使用' }} |
|
</el-tag> |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="操作" width="250" fixed="right"> |
|
<template #default="scope"> |
|
<el-button |
|
size="small" |
|
type="primary" |
|
@click="submitSingleAccount(scope.row)" |
|
:disabled="scope.row.status === 1 || scope.row.status === 3" |
|
> |
|
提交 |
|
</el-button> |
|
<el-button |
|
size="small" |
|
type="success" |
|
@click="extractSingleAccount(scope.row)" |
|
:disabled="scope.row.status !== 1" |
|
> |
|
提取 |
|
</el-button> |
|
<el-button |
|
size="small" |
|
@click="viewAccountDetails(scope.row)" |
|
> |
|
详情 |
|
</el-button> |
|
<el-button |
|
size="small" |
|
type="danger" |
|
@click="deleteAccount(scope.row)" |
|
> |
|
删除 |
|
</el-button> |
|
</template> |
|
</el-table-column> |
|
</el-table> |
|
|
|
<div style="margin-top: 20px; display: flex; justify-content: center;"> |
|
<el-pagination |
|
v-model:current-page="pagination.current" |
|
v-model:page-size="pagination.pageSize" |
|
:page-sizes="[10, 20, 50, 100]" |
|
layout="total, sizes, prev, pager, next, jumper" |
|
:total="pagination.total" |
|
@size-change="handleSizeChange" |
|
@current-change="handleCurrentChange" |
|
/> |
|
</div> |
|
</div> |
|
</el-tab-pane> |
|
|
|
|
|
<el-tab-pane label="任务管理" name="tasks"> |
|
<div class="tab-container"> |
|
<el-button type="primary" @click="loadTasks" style="margin-bottom: 20px;">刷新任务列表</el-button> |
|
|
|
<el-timeline> |
|
<el-timeline-item |
|
v-for="(task, id) in tasks" |
|
:key="id" |
|
:type="getTaskStatusType(task.status)" |
|
:icon="getTaskStatusIcon(task.status)" |
|
:timestamp="task.start_time" |
|
> |
|
<el-card class="task-card"> |
|
<div style="display: flex; justify-content: space-between; align-items: center;"> |
|
<div> |
|
<span style="font-weight: bold;">{{ getTaskTypeText(task.type) }} - {{ id }}</span> |
|
<el-tag size="small" style="margin-left: 10px;" :type="getTaskStatusType(task.status)"> |
|
{{ getTaskStatusText(task.status) }} |
|
</el-tag> |
|
</div> |
|
<div v-if="task.status === 'completed'"> |
|
<span style="margin-right: 10px;">成功: {{ task.success_count || 0 }}</span> |
|
<span>失败: {{ task.error_count || 0 }}</span> |
|
</div> |
|
</div> |
|
<div v-if="task.error" style="color: #F56C6C; margin-top: 10px;"> |
|
错误: {{ task.error }} |
|
</div> |
|
<div v-if="task.account_ids && task.account_ids.length > 0" style="margin-top: 10px;"> |
|
<span>处理账号: {{ task.account_ids.length }}个</span> |
|
</div> |
|
</el-card> |
|
</el-timeline-item> |
|
</el-timeline> |
|
|
|
<div v-if="Object.keys(tasks).length === 0" style="text-align: center; padding: 30px;"> |
|
<el-empty description="暂无任务记录" /> |
|
</div> |
|
</div> |
|
</el-tab-pane> |
|
|
|
|
|
<el-tab-pane label="操作日志" name="logs"> |
|
<div class="tab-container"> |
|
<div style="margin-bottom: 20px;"> |
|
<el-row :gutter="20"> |
|
<el-col :span="8"> |
|
<el-input v-model="logsFilter.username" placeholder="用户名" clearable></el-input> |
|
</el-col> |
|
<el-col :span="8"> |
|
<el-select v-model="logsFilter.operation" placeholder="操作类型" clearable style="width: 100%;"> |
|
<el-option label="提交邮箱" value="submit_email"></el-option> |
|
<el-option label="提取链接" value="extract_link"></el-option> |
|
<el-option label="后台提交" value="submit_email_background"></el-option> |
|
<el-option label="后台提取" value="extract_link_background"></el-option> |
|
<el-option label="更新状态" value="update_status"></el-option> |
|
</el-select> |
|
</el-col> |
|
<el-col :span="8"> |
|
<el-select v-model="logsFilter.status" placeholder="操作状态" clearable style="width: 100%;"> |
|
<el-option label="成功" value="success"></el-option> |
|
<el-option label="失败" value="failed"></el-option> |
|
<el-option label="处理中" value="processing"></el-option> |
|
<el-option label="错误" value="error"></el-option> |
|
</el-select> |
|
</el-col> |
|
</el-row> |
|
</div> |
|
|
|
<el-table |
|
:data="logs" |
|
style="width: 100%" |
|
v-loading="loading.logs" |
|
border |
|
> |
|
<el-table-column label="ID" width="80"> |
|
<template #default="scope"> |
|
{{ scope.row.log_id || scope.row.id || '-' }} |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="用户名" min-width="150"> |
|
<template #default="scope"> |
|
{{ scope.row.account_username || scope.row.username || '-' }} |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="操作" width="180"> |
|
<template #default="scope"> |
|
<el-tag size="small">{{ getOperationText(scope.row.operation) }}</el-tag> |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="状态" width="120"> |
|
<template #default="scope"> |
|
<el-tag :type="getLogStatusType(scope.row.status)" size="small"> |
|
{{ scope.row.status }} |
|
</el-tag> |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="消息" min-width="200"> |
|
<template #default="scope"> |
|
<el-tag size="small">{{ getMessageText(scope.row.message) }}</el-tag> |
|
</template> |
|
</el-table-column> |
|
<el-table-column label="时间" width="180"> |
|
<template #default="scope"> |
|
{{ scope.row.created_at || scope.row.timestamp || '-' }} |
|
</template> |
|
</el-table-column> |
|
</el-table> |
|
|
|
<div style="margin-top: 20px; display: flex; justify-content: center;"> |
|
<el-pagination |
|
v-model:current-page="logsPagination.current" |
|
v-model:page-size="logsPagination.pageSize" |
|
:page-sizes="[20, 50, 100]" |
|
layout="total, sizes, prev, pager, next, jumper" |
|
:total="logsPagination.total" |
|
@size-change="handleLogsSizeChange" |
|
@current-change="handleLogsCurrentChange" |
|
/> |
|
</div> |
|
</div> |
|
</el-tab-pane> |
|
</el-tabs> |
|
</el-main> |
|
</el-container> |
|
|
|
|
|
<el-dialog title="导入数据" v-model="dialogs.import" width="500px"> |
|
<el-form label-width="100px"> |
|
<el-form-item label="选择文件" required> |
|
<input type="file" @change="handleImportFileChange" accept=".txt,.csv" /> |
|
</el-form-item> |
|
</el-form> |
|
<template #footer> |
|
<span class="dialog-footer"> |
|
<el-button @click="dialogs.import = false">取消</el-button> |
|
<el-button type="primary" @click="importAccounts" :loading="loading.import">导入</el-button> |
|
</span> |
|
</template> |
|
</el-dialog> |
|
|
|
|
|
<el-dialog title="导出数据" v-model="dialogs.export" width="500px"> |
|
<el-form :model="exportForm" label-width="100px"> |
|
<el-form-item label="文件名"> |
|
<el-input v-model="exportForm.filename" placeholder="导出文件名"></el-input> |
|
</el-form-item> |
|
</el-form> |
|
<template #footer> |
|
<span class="dialog-footer"> |
|
<el-button @click="dialogs.export = false">取消</el-button> |
|
<el-button type="primary" @click="exportAccounts" :loading="loading.export">导出</el-button> |
|
</span> |
|
</template> |
|
</el-dialog> |
|
|
|
|
|
<el-dialog title="账号详情" v-model="dialogs.accountDetails" width="700px"> |
|
<el-descriptions v-if="currentAccount" :column="1" border> |
|
<el-descriptions-item label="ID">{{ currentAccount.id }}</el-descriptions-item> |
|
<el-descriptions-item label="注册时间">{{ currentAccount.register_time }}</el-descriptions-item> |
|
<el-descriptions-item label="用户名">{{ currentAccount.username }}</el-descriptions-item> |
|
<el-descriptions-item label="密码">{{ currentAccount.password }}</el-descriptions-item> |
|
<el-descriptions-item label="密保邮箱">{{ currentAccount.security_email || '-' }}</el-descriptions-item> |
|
<el-descriptions-item label="状态"> |
|
<el-tag :type="getStatusType(currentAccount.status)"> |
|
{{ getStatusText(currentAccount.status) }} |
|
</el-tag> |
|
</el-descriptions-item> |
|
<el-descriptions-item label="激活链接"> |
|
<div v-if="currentAccount.activation_link" style="display: flex; align-items: center;"> |
|
<span style="word-break: break-all;">{{ currentAccount.activation_link }}</span> |
|
<el-button |
|
size="small" |
|
type="primary" |
|
class="copy-button" |
|
@click="copyToClipboard(currentAccount.activation_link, currentAccount)" |
|
> |
|
复制 |
|
</el-button> |
|
</div> |
|
<span v-else>-</span> |
|
</el-descriptions-item> |
|
<el-descriptions-item label="更新时间">{{ currentAccount.updated_at || '-' }}</el-descriptions-item> |
|
<el-descriptions-item label="备注">{{ currentAccount.notes || '-' }}</el-descriptions-item> |
|
</el-descriptions> |
|
|
|
<div style="margin-top: 20px;"> |
|
<h3>更新状态</h3> |
|
<el-form :model="statusUpdateForm" label-width="100px"> |
|
<el-form-item label="状态"> |
|
<el-select v-model="statusUpdateForm.status" placeholder="选择状态" style="width: 100%;"> |
|
<el-option label="未提交" :value="0"></el-option> |
|
<el-option label="已提交" :value="1"></el-option> |
|
<el-option label="提交失败" :value="2"></el-option> |
|
<el-option label="已提取链接" :value="3"></el-option> |
|
</el-select> |
|
</el-form-item> |
|
<el-form-item label="激活链接"> |
|
<el-input v-model="statusUpdateForm.activation_link" placeholder="可选"></el-input> |
|
</el-form-item> |
|
<el-form-item label="备注"> |
|
<el-input v-model="statusUpdateForm.notes" type="textarea" placeholder="可选"></el-input> |
|
</el-form-item> |
|
</el-form> |
|
</div> |
|
|
|
<template #footer> |
|
<span class="dialog-footer"> |
|
<el-button @click="dialogs.accountDetails = false">取消</el-button> |
|
<el-button type="primary" @click="updateAccountStatus" :loading="loading.updateStatus">更新</el-button> |
|
</span> |
|
</template> |
|
</el-dialog> |
|
|
|
|
|
<el-dialog title="提交邮箱" v-model="dialogs.submit" width="500px"> |
|
<el-form :model="submitForm" label-width="100px"> |
|
<el-form-item label="最大线程数"> |
|
<el-input-number v-model="submitForm.max_workers" :min="1" :max="10"></el-input-number> |
|
</el-form-item> |
|
<el-form-item label="代理服务器"> |
|
<el-input v-model="submitForm.proxy" placeholder="可选,例如:http://127.0.0.1:60060"></el-input> |
|
</el-form-item> |
|
</el-form> |
|
<template #footer> |
|
<span class="dialog-footer"> |
|
<el-button @click="dialogs.submit = false">取消</el-button> |
|
<el-button type="primary" @click="confirmSubmit" :loading="loading.submit">提交</el-button> |
|
</span> |
|
</template> |
|
</el-dialog> |
|
|
|
|
|
<el-dialog title="提取链接" v-model="dialogs.extract" width="500px"> |
|
<el-form :model="extractForm" label-width="100px"> |
|
<el-form-item label="最大线程数"> |
|
<el-input-number v-model="extractForm.max_workers" :min="1" :max="5"></el-input-number> |
|
</el-form-item> |
|
</el-form> |
|
<template #footer> |
|
<span class="dialog-footer"> |
|
<el-button @click="dialogs.extract = false">取消</el-button> |
|
<el-button type="primary" @click="confirmExtract" :loading="loading.extract">提取</el-button> |
|
</span> |
|
</template> |
|
</el-dialog> |
|
|
|
|
|
<el-dialog title="登录验证" v-model="dialogs.login" width="350px" :close-on-click-modal="false" :show-close="false"> |
|
<el-form @submit.prevent="handleLogin"> |
|
<el-form-item label="密码"> |
|
<el-input v-model="loginForm.password" type="password" autocomplete="off" @keyup.enter="handleLogin"></el-input> |
|
</el-form-item> |
|
</el-form> |
|
<template #footer> |
|
<span class="dialog-footer"> |
|
<el-button type="primary" @click="handleLogin" :loading="loading.login">登录</el-button> |
|
</span> |
|
</template> |
|
</el-dialog> |
|
</div> |
|
|
|
<script> |
|
const { createApp, ref, reactive, onMounted, computed, nextTick, watch, onUnmounted } = Vue; |
|
|
|
|
|
const API_BASE_URL = 'https://168.fucku.tech'; |
|
|
|
const app = createApp({ |
|
setup() { |
|
|
|
const activeTab = ref('accounts'); |
|
const accounts = ref([]); |
|
const selectedAccounts = ref([]); |
|
const currentAccount = ref(null); |
|
const tasks = ref({}); |
|
const logs = ref([]); |
|
const statistics = reactive({ |
|
total: 0, |
|
pending: 0, |
|
submitted: 0, |
|
failed: 0, |
|
link_extracted: 0, |
|
unused: 0 |
|
}); |
|
|
|
|
|
const runningTaskPolls = reactive({}); |
|
|
|
|
|
const pagination = reactive({ |
|
current: 1, |
|
pageSize: 10, |
|
total: 0 |
|
}); |
|
|
|
const logsPagination = reactive({ |
|
current: 1, |
|
pageSize: 20, |
|
total: 0 |
|
}); |
|
|
|
|
|
const accountsFilter = reactive({ |
|
status: null, |
|
search: '', |
|
registerTimeStart: '', |
|
registerTimeEnd: '', |
|
idMin: '', |
|
idMax: '', |
|
hasActivationLink: '', |
|
used: '' |
|
}); |
|
|
|
const logsFilter = reactive({ |
|
username: '', |
|
operation: '', |
|
status: '' |
|
}); |
|
|
|
|
|
const importForm = reactive({ |
|
filepath: 'user-4-21-success.txt' |
|
}); |
|
|
|
const exportForm = reactive({ |
|
filename: 'export_results.txt', |
|
status: null |
|
}); |
|
|
|
const statusUpdateForm = reactive({ |
|
status: 0, |
|
activation_link: '', |
|
notes: '' |
|
}); |
|
|
|
const submitForm = reactive({ |
|
max_workers: 1, |
|
proxy: '' |
|
}); |
|
|
|
const extractForm = reactive({ |
|
max_workers: 1 |
|
}); |
|
|
|
const loginForm = reactive({ password: '' }); |
|
|
|
|
|
const dialogs = reactive({ |
|
import: false, |
|
export: false, |
|
accountDetails: false, |
|
submit: false, |
|
extract: false, |
|
login: false |
|
}); |
|
|
|
|
|
const loading = reactive({ |
|
accounts: false, |
|
tasks: false, |
|
logs: false, |
|
statistics: false, |
|
import: false, |
|
export: false, |
|
updateStatus: false, |
|
submit: false, |
|
extract: false, |
|
login: false |
|
}); |
|
|
|
|
|
const getToken = () => { |
|
return document.cookie.replace(/(?:(?:^|.*;\s*)token\s*\=\s*([^;]*).*$)|^.*$/, "$1"); |
|
}; |
|
const setToken = (token) => { |
|
document.cookie = `token=${token};path=/;max-age=2592000`; |
|
}; |
|
const clearToken = () => { |
|
document.cookie = 'token=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;'; |
|
}; |
|
|
|
|
|
axios.interceptors.request.use(config => { |
|
const token = getToken(); |
|
if (token) config.headers.token = token; |
|
return config; |
|
}); |
|
|
|
|
|
onMounted(async () => { |
|
if (!(await checkLogin())) return; |
|
console.log('应用已加载,开始获取数据'); |
|
loadAccounts(); |
|
loadStatistics(); |
|
loadTasks(); |
|
|
|
|
|
watch([ |
|
() => accountsFilter.status, |
|
() => accountsFilter.search, |
|
() => accountsFilter.registerTimeStart, |
|
() => accountsFilter.registerTimeEnd, |
|
() => accountsFilter.idMin, |
|
() => accountsFilter.idMax, |
|
() => accountsFilter.hasActivationLink, |
|
() => accountsFilter.used |
|
], () => { |
|
pagination.current = 1; |
|
loadAccounts(); |
|
}); |
|
|
|
watch([ |
|
() => logsFilter.username, |
|
() => logsFilter.operation, |
|
() => logsFilter.status |
|
], () => { |
|
logsPagination.current = 1; |
|
loadLogs(); |
|
}); |
|
}); |
|
|
|
|
|
const handleLogin = async () => { |
|
if (!loginForm.password) { |
|
ElementPlus.ElMessage.warning('请输入密码'); |
|
return; |
|
} |
|
loading.login = true; |
|
try { |
|
const res = await axios.post(`${API_BASE_URL}/login`, { password: loginForm.password }); |
|
setToken(res.data.token); |
|
dialogs.login = false; |
|
ElementPlus.ElMessage.success('登录成功'); |
|
|
|
loadAccounts(); |
|
loadStatistics(); |
|
loadTasks(); |
|
} catch (e) { |
|
ElementPlus.ElMessage.error(e.response?.data?.detail || '登录失败'); |
|
} finally { |
|
loading.login = false; |
|
} |
|
}; |
|
|
|
|
|
const logout = () => { |
|
clearToken(); |
|
dialogs.login = true; |
|
}; |
|
|
|
|
|
const checkLogin = async () => { |
|
const token = getToken(); |
|
if (!token) { |
|
dialogs.login = true; |
|
return false; |
|
} |
|
|
|
try { |
|
await axios.get(`${API_BASE_URL}/statistics`); |
|
return true; |
|
} catch { |
|
dialogs.login = true; |
|
return false; |
|
} |
|
}; |
|
|
|
|
|
const startTaskPolling = (taskId) => { |
|
console.log(`开始轮询任务 ${taskId} 的状态`); |
|
|
|
if (runningTaskPolls[taskId]) { |
|
|
|
return; |
|
} |
|
|
|
|
|
const intervalId = setInterval(async () => { |
|
try { |
|
const response = await axios.get(`${API_BASE_URL}/tasks/${taskId}`); |
|
const taskData = response.data; |
|
|
|
|
|
tasks.value = { ...tasks.value, [taskId]: taskData }; |
|
|
|
|
|
if (taskData.status === 'completed' || taskData.status === 'failed') { |
|
console.log(`任务 ${taskId} 已${taskData.status === 'completed' ? '完成' : '失败'},停止轮询`); |
|
clearInterval(runningTaskPolls[taskId]); |
|
delete runningTaskPolls[taskId]; |
|
|
|
|
|
ElementPlus.ElMessage({ |
|
message: `任务 ${taskId} 已${taskData.status === 'completed' ? '完成' : '失败'},成功: ${taskData.success_count || 0},失败: ${taskData.error_count || 0}`, |
|
type: taskData.status === 'completed' ? 'success' : 'warning', |
|
duration: 5000 |
|
}); |
|
|
|
|
|
if (taskData.status === 'completed') { |
|
loadAccounts(); |
|
loadStatistics(); |
|
} |
|
} |
|
} catch (error) { |
|
console.error(`轮询任务 ${taskId} 状态失败:`, error); |
|
|
|
clearInterval(runningTaskPolls[taskId]); |
|
delete runningTaskPolls[taskId]; |
|
} |
|
}, 10000); |
|
|
|
|
|
runningTaskPolls[taskId] = intervalId; |
|
}; |
|
|
|
|
|
const loadAccounts = async () => { |
|
loading.accounts = true; |
|
try { |
|
const params = { |
|
page: pagination.current, |
|
per_page: pagination.pageSize |
|
}; |
|
|
|
if (accountsFilter.status !== null && accountsFilter.status !== '') params.status = accountsFilter.status; |
|
if (accountsFilter.search) params.search = accountsFilter.search; |
|
if (accountsFilter.registerTimeStart) params.register_time_start = accountsFilter.registerTimeStart; |
|
if (accountsFilter.registerTimeEnd) params.register_time_end = accountsFilter.registerTimeEnd; |
|
if (accountsFilter.idMin) params.id_min = accountsFilter.idMin; |
|
if (accountsFilter.idMax) params.id_max = accountsFilter.idMax; |
|
if (accountsFilter.hasActivationLink) params.has_activation_link = accountsFilter.hasActivationLink; |
|
if (accountsFilter.used !== '' && accountsFilter.used !== null && accountsFilter.used !== undefined) params.used = accountsFilter.used; |
|
|
|
console.log('请求参数:', params); |
|
const response = await axios.get(`${API_BASE_URL}/accounts`, { params }); |
|
console.log('API响应:', response.data); |
|
|
|
if (response.data && Array.isArray(response.data.accounts)) { |
|
accounts.value = response.data.accounts; |
|
pagination.total = response.data.total; |
|
|
|
|
|
accounts.value.forEach((account, index) => { |
|
console.log(`账号[${index}]:`, account); |
|
if (!account.id) console.warn(`账号[${index}] 缺少ID字段`); |
|
if (!account.register_time) console.warn(`账号[${index}] 缺少注册时间字段`); |
|
if (!account.username) console.warn(`账号[${index}] 缺少用户名字段`); |
|
}); |
|
|
|
|
|
if (accounts.value.length > 0) { |
|
nextTick(() => { |
|
console.log('视图已更新'); |
|
}); |
|
} |
|
} else { |
|
console.error('API返回的数据格式不正确:', response.data); |
|
ElementPlus.ElMessage.warning('API返回的数据格式不正确'); |
|
} |
|
} catch (error) { |
|
console.error('加载账号失败:', error); |
|
ElementPlus.ElMessage.error('加载账号失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.accounts = false; |
|
} |
|
}; |
|
|
|
|
|
const loadStatistics = async () => { |
|
loading.statistics = true; |
|
try { |
|
const response = await axios.get(`${API_BASE_URL}/statistics`); |
|
Object.assign(statistics, response.data); |
|
} catch (error) { |
|
console.error('加载统计信息失败:', error); |
|
ElementPlus.ElMessage.error('加载统计信息失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.statistics = false; |
|
} |
|
}; |
|
|
|
|
|
const loadTasks = async () => { |
|
loading.tasks = true; |
|
try { |
|
const response = await axios.get(`${API_BASE_URL}/tasks`); |
|
tasks.value = response.data; |
|
|
|
|
|
Object.entries(response.data).forEach(([taskId, taskData]) => { |
|
if (taskData.status === 'running' && !runningTaskPolls[taskId]) { |
|
startTaskPolling(taskId); |
|
} |
|
}); |
|
} catch (error) { |
|
console.error('加载任务列表失败:', error); |
|
ElementPlus.ElMessage.error('加载任务列表失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.tasks = false; |
|
} |
|
}; |
|
|
|
|
|
const loadLogs = async () => { |
|
loading.logs = true; |
|
try { |
|
const params = { |
|
page: logsPagination.current, |
|
per_page: logsPagination.pageSize |
|
}; |
|
|
|
if (logsFilter.username) { |
|
params.username = logsFilter.username; |
|
} |
|
|
|
if (logsFilter.operation) { |
|
params.operation = logsFilter.operation; |
|
} |
|
|
|
if (logsFilter.status) { |
|
params.status = logsFilter.status; |
|
} |
|
|
|
const response = await axios.get(`${API_BASE_URL}/logs`, { params }); |
|
logs.value = response.data.logs; |
|
logsPagination.total = response.data.total; |
|
} catch (error) { |
|
console.error('加载日志失败:', error); |
|
ElementPlus.ElMessage.error('加载日志失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.logs = false; |
|
} |
|
}; |
|
|
|
|
|
const handleSizeChange = (size) => { |
|
pagination.pageSize = size; |
|
loadAccounts(); |
|
}; |
|
|
|
const handleCurrentChange = (page) => { |
|
pagination.current = page; |
|
loadAccounts(); |
|
}; |
|
|
|
const handleLogsSizeChange = (size) => { |
|
logsPagination.pageSize = size; |
|
loadLogs(); |
|
}; |
|
|
|
const handleLogsCurrentChange = (page) => { |
|
logsPagination.current = page; |
|
loadLogs(); |
|
}; |
|
|
|
|
|
const handleSelectionChange = (selection) => { |
|
selectedAccounts.value = selection; |
|
}; |
|
|
|
|
|
const handleTabClick = (tab) => { |
|
if (tab.props.name === 'logs') { |
|
loadLogs(); |
|
} |
|
}; |
|
|
|
|
|
const showImportDialog = () => { |
|
dialogs.import = true; |
|
}; |
|
|
|
const handleImportFileChange = (event) => { |
|
const file = event.target.files[0]; |
|
if (file) { |
|
importForm.filepath = file; |
|
} |
|
}; |
|
|
|
const importAccounts = async () => { |
|
if (!importForm.filepath) { |
|
ElementPlus.ElMessage.warning('请选择文件'); |
|
return; |
|
} |
|
|
|
loading.import = true; |
|
try { |
|
const formData = new FormData(); |
|
formData.append('file', importForm.filepath); |
|
|
|
const response = await axios.post(`${API_BASE_URL}/import`, formData, { |
|
headers: { |
|
'Content-Type': 'multipart/form-data' |
|
} |
|
}); |
|
|
|
ElementPlus.ElMessage.success(`成功导入 ${response.data.imported_count} 个账号`); |
|
dialogs.import = false; |
|
|
|
|
|
loadAccounts(); |
|
loadStatistics(); |
|
} catch (error) { |
|
console.error('导入账号失败:', error); |
|
ElementPlus.ElMessage.error('导入账号失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.import = false; |
|
} |
|
}; |
|
|
|
|
|
const showExportDialog = () => { |
|
dialogs.export = true; |
|
}; |
|
|
|
const exportAccounts = async () => { |
|
if (!exportForm.filename) { |
|
ElementPlus.ElMessage.warning('请输入文件名'); |
|
return; |
|
} |
|
loading.export = true; |
|
try { |
|
|
|
const params = { |
|
filename: exportForm.filename |
|
}; |
|
if (accountsFilter.status !== null && accountsFilter.status !== '') params.status = accountsFilter.status; |
|
if (accountsFilter.used !== '' && accountsFilter.used !== null && accountsFilter.used !== undefined) params.used = accountsFilter.used; |
|
if (accountsFilter.hasActivationLink) params.has_activation_link = accountsFilter.hasActivationLink; |
|
if (accountsFilter.search) params.search = accountsFilter.search; |
|
if (accountsFilter.registerTimeStart) params.register_time_start = accountsFilter.registerTimeStart; |
|
if (accountsFilter.registerTimeEnd) params.register_time_end = accountsFilter.registerTimeEnd; |
|
if (accountsFilter.idMin) params.id_min = accountsFilter.idMin; |
|
if (accountsFilter.idMax) params.id_max = accountsFilter.idMax; |
|
|
|
|
|
const response = await axios.post(`${API_BASE_URL}/export`, params, { |
|
responseType: 'blob' |
|
}); |
|
|
|
|
|
const blob = new Blob([response.data], { type: 'text/plain' }); |
|
const url = window.URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.href = url; |
|
link.setAttribute('download', exportForm.filename); |
|
document.body.appendChild(link); |
|
link.click(); |
|
|
|
|
|
document.body.removeChild(link); |
|
window.URL.revokeObjectURL(url); |
|
|
|
ElementPlus.ElMessage.success('文件已导出并下载到本地'); |
|
dialogs.export = false; |
|
} catch (error) { |
|
console.error('导出账号失败:', error); |
|
ElementPlus.ElMessage.error('导出账号失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.export = false; |
|
} |
|
}; |
|
|
|
|
|
const resetFailedAccounts = async () => { |
|
try { |
|
const response = await axios.post(`${API_BASE_URL}/reset-failed`); |
|
ElementPlus.ElMessage.success(response.data.message); |
|
|
|
|
|
loadAccounts(); |
|
loadStatistics(); |
|
} catch (error) { |
|
console.error('重置失败账号出错:', error); |
|
ElementPlus.ElMessage.error('重置失败账号出错: ' + (error.response?.data?.detail || error.message)); |
|
} |
|
}; |
|
|
|
|
|
const viewAccountDetails = (account) => { |
|
currentAccount.value = {...account}; |
|
statusUpdateForm.status = account.status; |
|
statusUpdateForm.activation_link = account.activation_link || ''; |
|
statusUpdateForm.notes = account.notes || ''; |
|
dialogs.accountDetails = true; |
|
}; |
|
|
|
|
|
const updateAccountStatus = async () => { |
|
if (currentAccount.value === null) { |
|
return; |
|
} |
|
|
|
loading.updateStatus = true; |
|
try { |
|
const response = await axios.post( |
|
`${API_BASE_URL}/accounts/${currentAccount.value.id}/status`, |
|
statusUpdateForm |
|
); |
|
|
|
ElementPlus.ElMessage.success('账号状态更新成功'); |
|
|
|
|
|
const index = accounts.value.findIndex(a => a.id === currentAccount.value.id); |
|
if (index !== -1) { |
|
accounts.value[index] = response.data; |
|
} |
|
|
|
dialogs.accountDetails = false; |
|
|
|
|
|
loadStatistics(); |
|
} catch (error) { |
|
console.error('更新账号状态失败:', error); |
|
ElementPlus.ElMessage.error('更新账号状态失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.updateStatus = false; |
|
} |
|
}; |
|
|
|
|
|
const deleteAccount = (account) => { |
|
ElementPlus.ElMessageBox.confirm( |
|
`确定要删除账号【${account.username}】吗?`, |
|
'删除确认', |
|
{ |
|
confirmButtonText: '删除', |
|
cancelButtonText: '取消', |
|
type: 'warning', |
|
} |
|
).then(async () => { |
|
try { |
|
await axios.delete(`${API_BASE_URL}/accounts/${account.id}`); |
|
ElementPlus.ElMessage.success('账号已删除'); |
|
loadAccounts(); |
|
loadStatistics(); |
|
} catch (error) { |
|
ElementPlus.ElMessage.error('删除失败: ' + (error.response?.data?.detail || error.message)); |
|
} |
|
}).catch(() => {}); |
|
}; |
|
|
|
|
|
const submitSelectedAccounts = () => { |
|
submitForm.account_ids = selectedAccounts.value.map(account => account.id); |
|
dialogs.submit = true; |
|
}; |
|
|
|
const submitSingleAccount = (account) => { |
|
submitForm.account_ids = [account.id]; |
|
dialogs.submit = true; |
|
}; |
|
|
|
const confirmSubmit = async () => { |
|
loading.submit = true; |
|
try { |
|
const response = await axios.post(`${API_BASE_URL}/submit`, { |
|
account_ids: submitForm.account_ids, |
|
max_workers: submitForm.max_workers, |
|
proxy: submitForm.proxy || null |
|
}); |
|
|
|
const taskId = response.data.task_id; |
|
ElementPlus.ElMessage.success('提交任务已启动,任务ID: ' + taskId); |
|
dialogs.submit = false; |
|
|
|
|
|
activeTab.value = 'tasks'; |
|
|
|
|
|
setTimeout(() => { |
|
loadTasks(); |
|
|
|
|
|
startTaskPolling(taskId); |
|
}, 1000); |
|
} catch (error) { |
|
console.error('提交邮箱失败:', error); |
|
ElementPlus.ElMessage.error('提交邮箱失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.submit = false; |
|
} |
|
}; |
|
|
|
|
|
const extractSelectedAccounts = () => { |
|
extractForm.account_ids = selectedAccounts.value |
|
.filter(account => account.status === 1) |
|
.map(account => account.id); |
|
|
|
if (extractForm.account_ids.length === 0) { |
|
ElementPlus.ElMessage.warning('没有选择已提交的账号'); |
|
return; |
|
} |
|
|
|
dialogs.extract = true; |
|
}; |
|
|
|
const extractSingleAccount = (account) => { |
|
if (account.status !== 1) { |
|
ElementPlus.ElMessage.warning('只能为已提交的账号提取链接'); |
|
return; |
|
} |
|
|
|
extractForm.account_ids = [account.id]; |
|
dialogs.extract = true; |
|
}; |
|
|
|
const confirmExtract = async () => { |
|
loading.extract = true; |
|
try { |
|
const response = await axios.post(`${API_BASE_URL}/extract`, { |
|
account_ids: extractForm.account_ids, |
|
max_workers: extractForm.max_workers |
|
}); |
|
|
|
const taskId = response.data.task_id; |
|
ElementPlus.ElMessage.success('提取任务已启动,任务ID: ' + taskId); |
|
dialogs.extract = false; |
|
|
|
|
|
activeTab.value = 'tasks'; |
|
|
|
|
|
setTimeout(() => { |
|
loadTasks(); |
|
|
|
|
|
startTaskPolling(taskId); |
|
}, 1000); |
|
} catch (error) { |
|
console.error('提取链接失败:', error); |
|
ElementPlus.ElMessage.error('提取链接失败: ' + (error.response?.data?.detail || error.message)); |
|
} finally { |
|
loading.extract = false; |
|
} |
|
}; |
|
|
|
|
|
const toggleAccountUsed = async (account) => { |
|
try { |
|
const response = await axios.post(`${API_BASE_URL}/accounts/${account.id}/toggle-used`); |
|
account.used = response.data.used; |
|
ElementPlus.ElMessage.success('已切换使用状态'); |
|
} catch (error) { |
|
ElementPlus.ElMessage.error('切换失败: ' + (error.response?.data?.detail || error.message)); |
|
} |
|
}; |
|
|
|
|
|
const batchToggleUsed = async (usedValue) => { |
|
if (selectedAccounts.value.length === 0) return; |
|
const ids = selectedAccounts.value.map(acc => acc.id); |
|
const confirmMsg = usedValue ? `确定将选中账号批量标记为“已使用”?` : `确定将选中账号批量标记为“未使用”?`; |
|
ElementPlus.ElMessageBox.confirm(confirmMsg, '批量操作确认', { |
|
confirmButtonText: '确定', |
|
cancelButtonText: '取消', |
|
type: 'warning', |
|
}).then(async () => { |
|
try { |
|
await axios.post(`${API_BASE_URL}/accounts/batch-toggle-used`, { ids, used: usedValue }); |
|
ElementPlus.ElMessage.success('批量操作成功'); |
|
loadAccounts(); |
|
} catch (error) { |
|
ElementPlus.ElMessage.error('批量操作失败: ' + (error.response?.data?.detail || error.message)); |
|
} |
|
}).catch(() => {}); |
|
}; |
|
|
|
|
|
const copyToClipboard = (text, account) => { |
|
navigator.clipboard.writeText(text).then(async () => { |
|
ElementPlus.ElMessage({ |
|
message: '已复制到剪贴板', |
|
type: 'success', |
|
duration: 1500 |
|
}); |
|
|
|
if (account && !account.used) { |
|
try { |
|
const response = await axios.post(`${API_BASE_URL}/accounts/${account.id}/toggle-used`); |
|
account.used = response.data.used; |
|
} catch (error) { |
|
|
|
} |
|
} |
|
}).catch(err => { |
|
console.error('复制失败:', err); |
|
ElementPlus.ElMessage.error('复制失败'); |
|
}); |
|
}; |
|
|
|
|
|
onUnmounted(() => { |
|
|
|
Object.values(runningTaskPolls).forEach(intervalId => { |
|
clearInterval(intervalId); |
|
}); |
|
}); |
|
|
|
|
|
const getStatusText = (status) => { |
|
const statusMap = { |
|
0: '未提交', |
|
1: '已提交', |
|
2: '提交失败', |
|
3: '已提取链接' |
|
}; |
|
return statusMap[status] || '未知状态'; |
|
}; |
|
|
|
|
|
const getStatusType = (status) => { |
|
const typeMap = { |
|
0: 'info', |
|
1: 'primary', |
|
2: 'danger', |
|
3: 'success' |
|
}; |
|
return typeMap[status] || 'info'; |
|
}; |
|
|
|
|
|
const getTaskStatusText = (status) => { |
|
const statusMap = { |
|
'running': '运行中', |
|
'completed': '已完成', |
|
'failed': '失败' |
|
}; |
|
return statusMap[status] || status; |
|
}; |
|
|
|
|
|
const getTaskStatusType = (status) => { |
|
const typeMap = { |
|
'running': 'primary', |
|
'completed': 'success', |
|
'failed': 'danger' |
|
}; |
|
return typeMap[status] || 'info'; |
|
}; |
|
|
|
|
|
const getTaskStatusIcon = (status) => { |
|
const iconMap = { |
|
'running': 'Loading', |
|
'completed': 'Check', |
|
'failed': 'Close' |
|
}; |
|
return iconMap[status] || ''; |
|
}; |
|
|
|
|
|
const getTaskTypeText = (type) => { |
|
const typeMap = { |
|
'submit': '提交邮箱', |
|
'extract': '提取链接' |
|
}; |
|
return typeMap[type] || type; |
|
}; |
|
|
|
|
|
const getOperationText = (operation) => { |
|
const operationMap = { |
|
'submit_email': '提交邮箱', |
|
'extract_link': '提取链接', |
|
'submit_email_background': '后台提交', |
|
'extract_link_background': '后台提取', |
|
'update_status': '更新状态' |
|
}; |
|
return operationMap[operation] || operation; |
|
}; |
|
|
|
|
|
const getMessageText = (message) => { |
|
|
|
return message || '-'; |
|
}; |
|
|
|
const getLogStatusType = (status) => { |
|
const typeMap = { |
|
'success': 'success', |
|
'failed': 'danger', |
|
'error': 'danger', |
|
'processing': 'warning' |
|
}; |
|
return typeMap[status] || 'info'; |
|
}; |
|
|
|
|
|
const handleStatusChange = (value) => { |
|
console.log('状态筛选变化:', value); |
|
}; |
|
|
|
return { |
|
activeTab, |
|
accounts, |
|
selectedAccounts, |
|
currentAccount, |
|
tasks, |
|
logs, |
|
statistics, |
|
pagination, |
|
logsPagination, |
|
accountsFilter, |
|
logsFilter, |
|
importForm, |
|
exportForm, |
|
statusUpdateForm, |
|
submitForm, |
|
extractForm, |
|
loginForm, |
|
dialogs, |
|
loading, |
|
|
|
|
|
loadAccounts, |
|
loadStatistics, |
|
loadTasks, |
|
loadLogs, |
|
handleSizeChange, |
|
handleCurrentChange, |
|
handleLogsSizeChange, |
|
handleLogsCurrentChange, |
|
handleSelectionChange, |
|
handleTabClick, |
|
showImportDialog, |
|
handleImportFileChange, |
|
importAccounts, |
|
showExportDialog, |
|
exportAccounts, |
|
resetFailedAccounts, |
|
viewAccountDetails, |
|
updateAccountStatus, |
|
deleteAccount, |
|
submitSelectedAccounts, |
|
submitSingleAccount, |
|
confirmSubmit, |
|
extractSelectedAccounts, |
|
extractSingleAccount, |
|
confirmExtract, |
|
toggleAccountUsed, |
|
batchToggleUsed, |
|
copyToClipboard, |
|
handleLogin, |
|
logout, |
|
checkLogin, |
|
|
|
|
|
getStatusText, |
|
getStatusType, |
|
getTaskStatusText, |
|
getTaskStatusType, |
|
getTaskStatusIcon, |
|
getTaskTypeText, |
|
getOperationText, |
|
getLogStatusType, |
|
getMessageText, |
|
handleStatusChange |
|
}; |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { |
|
app.component(key, component); |
|
} |
|
|
|
|
|
app.use(ElementPlus); |
|
|
|
app.mount('#app'); |
|
</script> |
|
</body> |
|
</html> |