This commit is contained in:
sjk
2026-01-13 18:59:26 +08:00
commit 7feccf246d
56 changed files with 11596 additions and 0 deletions

40
static/README.txt Normal file
View File

@@ -0,0 +1,40 @@
MIP广告点击系统 - 前端控制面板使用说明
===========================================
启动方式:
---------
1. 确保后端服务已启动:
python app.py
2. 打开浏览器访问:
http://127.0.0.1:5000
功能说明:
---------
1. 调度器控制
- 启动调度器:开始自动点击任务
- 停止调度器:暂停所有自动任务
- 实时显示调度器运行状态
2. 链接管理
- 添加单个链接输入MIP页面链接点击添加
- 批量添加链接:每行一个链接,支持批量导入
- 重置链接:清空该链接的点击记录,重新开始
- 删除链接:从系统中移除该链接
3. 统计数据
- 总链接数:系统中管理的链接总数
- 总点击次数:累计执行的点击次数
- 获得回复:收到广告主回复的次数
- 回复率:回复次数/点击次数的百分比
4. 自动刷新
- 页面每5秒自动刷新一次数据
- 无需手动刷新页面
注意事项:
---------
- 调度器仅在09:00-21:00时间段执行任务
- 每个链接每30分钟点击一次
- 每次点击随机执行1-10次
- 每次点击后等待30秒检查回复

112
static/browser.html Normal file
View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器测试 - MIP广告点击管理系统</title>
<link rel="stylesheet" href="css/common.css">
</head>
<body>
<div class="layout">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">MIP管理系统</div>
<div class="menu">
<div class="menu-item" onclick="location.href='dashboard.html'">
<span class="menu-icon">📊</span>
<span>数据概览</span>
</div>
<div class="menu-item" onclick="location.href='scheduler.html'">
<span class="menu-icon">⚙️</span>
<span>调度器管理</span>
</div>
<div class="menu-item" onclick="location.href='urls.html'">
<span class="menu-icon">🔗</span>
<span>链接管理</span>
</div>
<div class="menu-item active" onclick="location.href='browser.html'">
<span class="menu-icon">🌐</span>
<span>浏览器测试</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main">
<!-- 顶部导航栏 -->
<div class="navbar">
<div class="breadcrumb">浏览器测试</div>
<div class="user-info">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">系统运行中</span>
</div>
</div>
<!-- 内容区 -->
<div class="content">
<div class="card">
<div class="card-header">Profile管理</div>
<div class="card-body">
<div class="btn-group">
<button class="btn btn-primary" onclick="listProfiles()">查询Profile列表</button>
<button class="btn btn-success" onclick="startBrowser()">启动浏览器</button>
<button class="btn btn-danger" onclick="stopBrowser()">停止浏览器</button>
</div>
<div id="profileResult" style="margin-top: 16px;"></div>
</div>
</div>
<div class="card">
<div class="card-header">代理管理</div>
<div class="card-body">
<div class="btn-group">
<button class="btn btn-primary" onclick="getDamaiProxy()">获取大麦IP</button>
<button class="btn btn-success" onclick="createProxy()">创建代理</button>
<button class="btn btn-warning" onclick="listProxies()">查询代理列表</button>
</div>
<div id="proxyResult" style="margin-top: 16px;"></div>
</div>
</div>
<div class="card">
<div class="card-header">Profile配置</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Profile ID</label>
<input type="text" id="profileId" class="form-input" placeholder="输入Profile ID" />
</div>
<div class="form-group">
<label class="form-label">代理IDAPI v2方式</label>
<input type="text" id="proxyId" class="form-input" placeholder="输入代理ID" />
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="updateProfileProxy()">更新API v2</button>
<button class="btn btn-success" onclick="updateProfileProxyV1()">更新API v1</button>
</div>
<div style="margin-top: 12px; padding: 8px; background: #f0f0f0; border-radius: 4px; font-size: 12px; color: #666;">
<div><strong>API v2:</strong> 使用proxy_id引用已创建的代理</div>
<div style="margin-top: 4px;"><strong>API v1:</strong> 直接传入代理配置需先获取大麦IP</div>
</div>
<div id="updateResult" style="margin-top: 16px;"></div>
</div>
</div>
<div class="card">
<div class="card-header">完整测试流程</div>
<div class="card-body">
<p style="color: #666; margin-bottom: 16px;">完整测试:获取代理 → 创建代理 → 更新Profile → 启动浏览器</p>
<div class="btn-group">
<button class="btn btn-success" onclick="fullTest(false)">完整测试(不使用代理)</button>
<button class="btn btn-warning" onclick="fullTest(true)">完整测试(使用代理)</button>
</div>
<div id="fullTestResult" style="margin-top: 16px;"></div>
</div>
</div>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script src="js/browser.js"></script>
</body>
</html>

366
static/css/common.css Normal file
View File

@@ -0,0 +1,366 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: #f0f2f5;
color: #333;
overflow: hidden;
}
/* 布局 */
.layout {
display: flex;
height: 100vh;
overflow: hidden;
}
/* 侧边栏 */
.sidebar {
width: 200px;
background: #001529;
color: white;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.menu {
flex: 1;
padding: 16px 0;
}
.menu-item {
padding: 12px 24px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 12px;
color: rgba(255, 255, 255, 0.65);
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.menu-item.active {
background: #1890ff;
color: white;
}
.menu-icon {
font-size: 16px;
}
/* 主内容区 */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
/* 顶部导航栏 */
.navbar {
height: 64px;
background: white;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
flex-shrink: 0;
}
.breadcrumb {
font-size: 16px;
font-weight: 500;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #52c41a;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 内容区 */
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
/* 卡片 */
.card {
background: white;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
margin-bottom: 24px;
}
.card-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
font-size: 16px;
font-weight: 500;
}
.card-body {
padding: 24px;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
border-left: 3px solid #1890ff;
}
.stat-card:nth-child(2) {
border-left-color: #52c41a;
}
.stat-card:nth-child(3) {
border-left-color: #faad14;
}
.stat-card:nth-child(4) {
border-left-color: #f5222d;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.stat-value {
font-size: 30px;
font-weight: 600;
color: #333;
}
/* 表单 */
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #333;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
transition: all 0.3s;
}
.form-input:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
resize: vertical;
min-height: 100px;
font-family: inherit;
}
.form-textarea:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 按钮 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 2px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: #1890ff;
color: white;
}
.btn-primary:hover {
background: #40a9ff;
}
.btn-success {
background: #52c41a;
color: white;
}
.btn-success:hover {
background: #73d13d;
}
.btn-danger {
background: #ff4d4f;
color: white;
}
.btn-danger:hover {
background: #ff7875;
}
.btn-warning {
background: #faad14;
color: white;
}
.btn-warning:hover {
background: #ffc53d;
}
.btn-group {
display: flex;
gap: 12px;
}
/* 表格 */
.table-container {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
background: #fafafa;
padding: 12px 16px;
text-align: left;
font-size: 14px;
font-weight: 500;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
.table td {
padding: 12px 16px;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
}
.table tr:hover {
background: #fafafa;
}
.table-url {
color: #1890ff;
text-decoration: none;
max-width: 400px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-url:hover {
text-decoration: underline;
}
/* Toast提示 */
.toast {
position: fixed;
top: 24px;
right: 24px;
padding: 16px 24px;
border-radius: 2px;
color: white;
font-size: 14px;
z-index: 1000;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease;
}
.toast-success {
background: #52c41a;
}
.toast-error {
background: #ff4d4f;
}
.toast-info {
background: #1890ff;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 48px;
color: #999;
}

98
static/dashboard.html Normal file
View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据概览 - MIP广告点击管理系统</title>
<link rel="stylesheet" href="css/common.css">
</head>
<body>
<div class="layout">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">MIP管理系统</div>
<div class="menu">
<div class="menu-item active" onclick="location.href='dashboard.html'">
<span class="menu-icon">📊</span>
<span>数据概览</span>
</div>
<div class="menu-item" onclick="location.href='scheduler.html'">
<span class="menu-icon">⚙️</span>
<span>调度器管理</span>
</div>
<div class="menu-item" onclick="location.href='urls.html'">
<span class="menu-icon">🔗</span>
<span>链接管理</span>
</div>
<div class="menu-item" onclick="location.href='browser.html'">
<span class="menu-icon">🌐</span>
<span>浏览器测试</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main">
<!-- 顶部导航栏 -->
<div class="navbar">
<div class="breadcrumb">数据概览</div>
<div class="user-info">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">系统运行中</span>
</div>
</div>
<!-- 内容区 -->
<div class="content">
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">总链接数</div>
<div class="stat-value" id="totalUrls">0</div>
</div>
<div class="stat-card">
<div class="stat-label">总点击次数</div>
<div class="stat-value" id="totalClicks">0</div>
</div>
<div class="stat-card">
<div class="stat-label">获得回复</div>
<div class="stat-value" id="totalReplies">0</div>
</div>
<div class="stat-card">
<div class="stat-label">回复率</div>
<div class="stat-value" id="replyRate">0%</div>
</div>
</div>
<!-- 链接列表 -->
<div class="card">
<div class="card-header">链接列表</div>
<div class="card-body">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>链接地址</th>
<th>点击次数</th>
<th>回复次数</th>
<th>上次点击时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="urlTableBody">
<tr>
<td colspan="5" class="empty-state">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script src="js/dashboard.js"></script>
</body>
</html>

843
static/index.html Normal file
View File

@@ -0,0 +1,843 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MIP广告点击管理系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: #f0f2f5;
color: #333;
}
/* 布局 */
.layout {
display: flex;
min-height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 200px;
background: #001529;
color: white;
display: flex;
flex-direction: column;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.menu {
flex: 1;
padding: 16px 0;
}
.menu-item {
padding: 12px 24px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 12px;
color: rgba(255, 255, 255, 0.65);
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.menu-item.active {
background: #1890ff;
color: white;
}
.menu-icon {
font-size: 16px;
}
/* 主内容区 */
.main {
flex: 1;
display: flex;
flex-direction: column;
}
/* 顶部导航栏 */
.navbar {
height: 64px;
background: white;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
}
.breadcrumb {
font-size: 16px;
font-weight: 500;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #52c41a;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 内容区 */
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
/* 卡片 */
.card {
background: white;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
margin-bottom: 24px;
}
.card-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
font-size: 16px;
font-weight: 500;
}
.card-body {
padding: 24px;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
border-left: 3px solid #1890ff;
}
.stat-card:nth-child(2) {
border-left-color: #52c41a;
}
.stat-card:nth-child(3) {
border-left-color: #faad14;
}
.stat-card:nth-child(4) {
border-left-color: #f5222d;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.stat-value {
font-size: 30px;
font-weight: 600;
color: #333;
}
/* 表单 */
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #333;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
transition: all 0.3s;
}
.form-input:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
resize: vertical;
min-height: 100px;
font-family: inherit;
}
.form-textarea:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 按钮 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 2px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: #1890ff;
color: white;
}
.btn-primary:hover {
background: #40a9ff;
}
.btn-success {
background: #52c41a;
color: white;
}
.btn-success:hover {
background: #73d13d;
}
.btn-danger {
background: #ff4d4f;
color: white;
}
.btn-danger:hover {
background: #ff7875;
}
.btn-warning {
background: #faad14;
color: white;
}
.btn-warning:hover {
background: #ffc53d;
}
.btn-group {
display: flex;
gap: 12px;
}
/* 表格 */
.table-container {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
background: #fafafa;
padding: 12px 16px;
text-align: left;
font-size: 14px;
font-weight: 500;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
.table td {
padding: 12px 16px;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
}
.table tr:hover {
background: #fafafa;
}
.table-url {
color: #1890ff;
text-decoration: none;
max-width: 400px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-url:hover {
text-decoration: underline;
}
/* 标签 */
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 2px;
font-size: 12px;
}
.tag-success {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.tag-error {
background: #fff1f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* Toast提示 */
.toast {
position: fixed;
top: 24px;
right: 24px;
padding: 16px 24px;
border-radius: 2px;
color: white;
font-size: 14px;
z-index: 1000;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease;
}
.toast-success {
background: #52c41a;
}
.toast-error {
background: #ff4d4f;
}
.toast-info {
background: #1890ff;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 48px;
color: #999;
}
/* 隐藏内容 */
.page-content {
display: none;
}
.page-content.active {
display: block;
}
</style>
</head>
<body>
<div class="layout">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">MIP管理系统</div>
<div class="menu">
<div class="menu-item active" onclick="switchPage('dashboard')">
<span class="menu-icon">📊</span>
<span>数据概览</span>
</div>
<div class="menu-item" onclick="switchPage('scheduler')">
<span class="menu-icon">⚙️</span>
<span>调度器管理</span>
</div>
<div class="menu-item" onclick="switchPage('urls')">
<span class="menu-icon">🔗</span>
<span>链接管理</span>
</div>
<div class="menu-item" onclick="switchPage('browser')">
<span class="menu-icon">🌐</span>
<span>浏览器测试</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main">
<!-- 顶部导航栏 -->
<div class="navbar">
<div class="breadcrumb" id="breadcrumb">数据概览</div>
<div class="user-info">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">系统运行中</span>
</div>
</div>
<!-- 内容区 -->
<div class="content">
<!-- 数据概览页面 -->
<div id="dashboard" class="page-content active">
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">总链接数</div>
<div class="stat-value" id="totalUrls">0</div>
</div>
<div class="stat-card">
<div class="stat-label">总点击次数</div>
<div class="stat-value" id="totalClicks">0</div>
</div>
<div class="stat-card">
<div class="stat-label">获得回复</div>
<div class="stat-value" id="totalReplies">0</div>
</div>
<div class="stat-card">
<div class="stat-label">回复率</div>
<div class="stat-value" id="replyRate">0%</div>
</div>
</div>
<!-- 链接列表 -->
<div class="card">
<div class="card-header">链接列表</div>
<div class="card-body">
<div class="table-container">
<table class="table" id="urlTable">
<thead>
<tr>
<th>链接地址</th>
<th>点击次数</th>
<th>回复次数</th>
<th>上次点击时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="urlTableBody">
<tr>
<td colspan="5" class="empty-state">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 调度器管理页面 -->
<div id="scheduler" class="page-content">
<div class="card">
<div class="card-header">调度器控制</div>
<div class="card-body">
<div class="btn-group">
<button class="btn btn-success" onclick="startScheduler()">启动调度器</button>
<button class="btn btn-danger" onclick="stopScheduler()">停止调度器</button>
</div>
<div style="padding: 16px; background: #fafafa; border-radius: 2px; margin-top: 16px;">
<h4 style="margin-bottom: 12px; font-size: 14px;">调度规则说明</h4>
<ul style="padding-left: 20px; line-height: 2;">
<li>每30分钟点击一次添加的链接</li>
<li>仅在09:00-21:00时间段执行</li>
<li>每个链接随机点击1-10次</li>
<li>等待最多30秒查看回复</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 链接管理页面 -->
<div id="urls" class="page-content">
<div class="card">
<div class="card-header">添加单个链接</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">链接地址</label>
<input type="text" id="singleUrl" class="form-input" placeholder="请输入MIP页面链接" />
</div>
<button class="btn btn-primary" onclick="addSingleUrl()">添加链接</button>
</div>
</div>
<div class="card">
<div class="card-header">批量添加链接</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">批量链接(每行一个)</label>
<textarea id="batchUrls" class="form-textarea" placeholder="每行输入一个链接,支持批量添加"></textarea>
</div>
<button class="btn btn-primary" onclick="addBatchUrls()">批量添加</button>
</div>
</div>
</div>
<!-- 浏览器测试页面 -->
<div id="browser" class="page-content">
<div class="card">
<div class="card-header">AdsPower浏览器测试</div>
<div class="card-body">
<p style="color: #666; margin-bottom: 16px;">此功能用于测试AdsPower浏览器连接是否正常</p>
<div class="btn-group">
<button class="btn btn-primary" onclick="testBrowser(false)">测试浏览器(不使用代理)</button>
<button class="btn btn-warning" onclick="testBrowser(true)">测试浏览器(使用代理)</button>
</div>
<div id="browserTestResult" style="margin-top: 16px;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const API_BASE = 'http://127.0.0.1:5000';
// 页面切换
function switchPage(page) {
// 隐藏所有页面
document.querySelectorAll('.page-content').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
// 显示目标页面
document.getElementById(page).classList.add('active');
event.currentTarget.classList.add('active');
// 更新面包屑
const breadcrumbMap = {
'dashboard': '数据概览',
'scheduler': '调度器管理',
'urls': '链接管理',
'browser': '浏览器测试'
};
document.getElementById('breadcrumb').textContent = breadcrumbMap[page];
}
// 显示Toast提示
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 获取调度器状态
async function getSchedulerStatus() {
try {
const response = await fetch(`${API_BASE}/api/scheduler/status`);
const data = await response.json();
if (data.success) {
const isRunning = data.data.status === 'running';
const indicator = document.getElementById('statusIndicator');
const text = document.getElementById('statusText');
indicator.style.background = isRunning ? '#52c41a' : '#ff4d4f';
text.textContent = isRunning ? '调度器运行中' : '调度器已停止';
}
} catch (error) {
console.error('获取调度器状态失败:', error);
}
}
// 启动调度器
async function startScheduler() {
try {
const response = await fetch(`${API_BASE}/api/scheduler/start`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('调度器已启动', 'success');
getSchedulerStatus();
} else {
showToast(data.message || '启动失败', 'error');
}
} catch (error) {
showToast('启动失败: ' + error.message, 'error');
}
}
// 停止调度器
async function stopScheduler() {
try {
const response = await fetch(`${API_BASE}/api/scheduler/stop`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('调度器已停止', 'success');
getSchedulerStatus();
} else {
showToast(data.message || '停止失败', 'error');
}
} catch (error) {
showToast('停止失败: ' + error.message, 'error');
}
}
// 获取统计数据
async function getStatistics() {
try {
const response = await fetch(`${API_BASE}/api/statistics`);
const data = await response.json();
if (data.success) {
const stats = data.data;
document.getElementById('totalUrls').textContent = stats.total_urls;
document.getElementById('totalClicks').textContent = stats.total_clicks;
document.getElementById('totalReplies').textContent = stats.total_replies;
document.getElementById('replyRate').textContent = stats.reply_rate;
}
} catch (error) {
console.error('获取统计数据失败:', error);
}
}
// 添加单个URL
async function addSingleUrl() {
const url = document.getElementById('singleUrl').value.trim();
if (!url) {
showToast('请输入链接', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
});
const data = await response.json();
if (data.success) {
showToast('添加成功', 'success');
document.getElementById('singleUrl').value = '';
loadUrlList();
getStatistics();
} else {
showToast(data.message || '添加失败', 'error');
}
} catch (error) {
showToast('添加失败: ' + error.message, 'error');
}
}
// 批量添加URL
async function addBatchUrls() {
const text = document.getElementById('batchUrls').value.trim();
if (!text) {
showToast('请输入链接', 'error');
return;
}
const urls = text.split('\n').map(u => u.trim()).filter(u => u);
if (urls.length === 0) {
showToast('请输入有效链接', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ urls })
});
const data = await response.json();
if (data.success) {
showToast(`成功添加 ${data.added_count}/${data.total_count} 个链接`, 'success');
document.getElementById('batchUrls').value = '';
loadUrlList();
getStatistics();
} else {
showToast(data.message || '添加失败', 'error');
}
} catch (error) {
showToast('添加失败: ' + error.message, 'error');
}
}
// 加载URL列表
async function loadUrlList() {
try {
const response = await fetch(`${API_BASE}/api/urls`);
const data = await response.json();
if (data.success) {
const tbody = document.getElementById('urlTableBody');
if (data.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.data.map(item => `
<tr>
<td>
<a href="${item.url}" target="_blank" class="table-url" title="${item.url}">${item.url}</a>
</td>
<td>${item.click_count}</td>
<td>${item.reply_count}</td>
<td>${item.last_click_time || '未点击'}</td>
<td>
<button class="btn btn-warning" style="padding: 4px 12px; font-size: 12px; margin-right: 8px;" onclick="resetUrl('${encodeURIComponent(item.url)}')">重置</button>
<button class="btn btn-danger" style="padding: 4px 12px; font-size: 12px;" onclick="deleteUrl('${encodeURIComponent(item.url)}')">删除</button>
</td>
</tr>
`).join('');
}
} catch (error) {
console.error('加载URL列表失败:', error);
}
}
// 重置URL
async function resetUrl(encodedUrl) {
if (!confirm('确定要重置该链接吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls/${encodedUrl}/reset`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('重置成功', 'success');
loadUrlList();
getStatistics();
} else {
showToast(data.message || '重置失败', 'error');
}
} catch (error) {
showToast('重置失败: ' + error.message, 'error');
}
}
// 删除URL
async function deleteUrl(encodedUrl) {
if (!confirm('确定要删除该链接吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls/${encodedUrl}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
loadUrlList();
getStatistics();
} else {
showToast(data.message || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败: ' + error.message, 'error');
}
}
// 浏览器测试
async function testBrowser(useProxy) {
const resultDiv = document.getElementById('browserTestResult');
resultDiv.innerHTML = '<p style="color: #1890ff;">测试中,请稍候...</p>';
showToast('浏览器测试功能待开发,请使用 test_adspower_playwright.py 进行测试', 'info');
setTimeout(() => {
resultDiv.innerHTML = '<p style="color: #666;">请使用命令行运行: python test_adspower_playwright.py</p>';
}, 1000);
}
// 初始化
function init() {
getSchedulerStatus();
getStatistics();
loadUrlList();
// 定时刷新状态
setInterval(() => {
getSchedulerStatus();
getStatistics();
loadUrlList();
}, 5000);
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

515
static/js/browser.js Normal file
View File

@@ -0,0 +1,515 @@
// 全局变量存储当前选中的Profile和代理
let currentProfileId = null;
let currentProxyInfo = null;
// 格式化JSON显示
function formatResult(data, elementId) {
const el = document.getElementById(elementId);
el.innerHTML = `<pre style="background: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; max-height: 400px;">${JSON.stringify(data, null, 2)}</pre>`;
}
// 查询Profile列表
async function listProfiles() {
try {
showToast('正在查询Profile列表...', 'info');
const response = await fetch(`${API_BASE}/api/adspower/profiles`);
const data = await response.json();
if (data.success) {
const profiles = data.data?.data?.list || [];
if (profiles.length > 0) {
// 美化显示Profile信息
let html = '<div style="background: #f5f5f5; padding: 12px; border-radius: 4px; max-height: 500px; overflow-y: auto;">';
html += `<p style="margin-bottom: 12px; font-weight: 500; color: #333;">找到 ${profiles.length} 个Profile环境</p>`;
profiles.forEach((profile, idx) => {
const proxyConfig = profile.user_proxy_config || {};
const hasProxy = proxyConfig.proxy_host && proxyConfig.proxy_port;
html += `
<div style="background: white; padding: 14px; margin-bottom: 10px; border-radius: 4px; border-left: 4px solid ${hasProxy ? '#52c41a' : '#faad14'};">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
<div style="flex: 1;">
<div style="font-weight: 600; color: #333; font-size: 14px; margin-bottom: 4px;">
#${idx + 1} ${profile.name}
<span style="color: #999; font-size: 12px; font-weight: 400; margin-left: 8px;">No.${profile.profile_no}</span>
</div>
<div style="color: #666; font-size: 12px;">
Profile ID: <span style="font-family: monospace; background: #f0f0f0; padding: 2px 6px; border-radius: 2px;">${profile.profile_id}</span>
</div>
</div>
<button onclick="selectProfile('${profile.profile_id}', '${profile.name}')"
style="padding: 6px 12px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; white-space: nowrap;">
选择环境
</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 12px; padding: 8px; background: #fafafa; border-radius: 3px;">
<div>
<span style="color: #999;">当前IP:</span>
<span style="color: #333; font-weight: 500; margin-left: 4px;">${profile.ip || 'N/A'}</span>
${profile.ip_country ? `<span style="color: #999; margin-left: 4px;">(${profile.ip_country.toUpperCase()})</span>` : ''}
</div>
<div>
<span style="color: #999;">最后打开:</span>
<span style="color: #666; margin-left: 4px;">${profile.last_open_time ? new Date(parseInt(profile.last_open_time) * 1000).toLocaleString('zh-CN') : 'N/A'}</span>
</div>
</div>
${hasProxy ? `
<div style="margin-top: 10px; padding: 10px; background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 3px;">
<div style="font-weight: 500; color: #52c41a; margin-bottom: 6px; font-size: 12px;">✓ 代理配置</div>
<div style="font-size: 12px; color: #666; line-height: 1.6;">
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
<div>
<span style="color: #999;">类型:</span>
<span style="color: #333; margin-left: 4px;">${proxyConfig.proxy_type?.toUpperCase() || 'N/A'}</span>
</div>
<div>
<span style="color: #999;">地址:</span>
<span style="color: #333; margin-left: 4px; font-family: monospace;">${proxyConfig.proxy_host}:${proxyConfig.proxy_port}</span>
</div>
${proxyConfig.latest_ip ? `
<div>
<span style="color: #999;">最新IP:</span>
<span style="color: #333; margin-left: 4px; font-family: monospace;">${proxyConfig.latest_ip}</span>
</div>
` : ''}
</div>
${proxyConfig.proxy_user ? `
<div style="margin-top: 4px;">
<span style="color: #999;">认证:</span>
<span style="color: #333; margin-left: 4px; font-family: monospace;">${proxyConfig.proxy_user}</span>
</div>
` : ''}
</div>
</div>
` : `
<div style="margin-top: 10px; padding: 8px; background: #fffbe6; border: 1px solid #ffe58f; border-radius: 3px; font-size: 12px; color: #d48806;">
⚠ 未配置代理
</div>
`}
${profile.remark ? `
<div style="margin-top: 8px; padding: 6px 8px; background: #f0f0f0; border-radius: 3px; font-size: 11px; color: #666;">
备注: ${profile.remark}
</div>
` : ''}
</div>
`;
});
html += '</div>';
document.getElementById('profileResult').innerHTML = html;
// 自动选择第一个Profile
currentProfileId = profiles[0].profile_id;
document.getElementById('profileId').value = currentProfileId;
showToast(`找到 ${profiles.length} 个Profile环境`, 'success');
} else {
document.getElementById('profileResult').innerHTML = '<p style="color: #999; text-align: center; padding: 24px;">未找到Profile环境</p>';
showToast('未找到Profile', 'info');
}
} else {
showToast(data.message || '查询失败', 'error');
}
} catch (error) {
console.error('查询Profile列表错误:', error);
showToast('查询失败: ' + error.message, 'error');
}
}
// 选择Profile
function selectProfile(profileId, profileName) {
currentProfileId = profileId;
document.getElementById('profileId').value = profileId;
showToast(`已选择环境: ${profileName}`, 'success');
}
// 启动浏览器
async function startBrowser() {
const profileId = document.getElementById('profileId').value || currentProfileId;
if (!profileId) {
showToast('请先查询Profile列表或输入Profile ID', 'error');
return;
}
try {
showToast('正在启动浏览器...', 'info');
const response = await fetch(`${API_BASE}/api/adspower/browser/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: profileId })
});
const data = await response.json();
if (data.success) {
formatResult(data.data, 'profileResult');
showToast('浏览器启动成功', 'success');
} else {
showToast(data.message || '启动失败', 'error');
}
} catch (error) {
showToast('启动失败: ' + error.message, 'error');
}
}
// 停止浏览器
async function stopBrowser() {
const profileId = document.getElementById('profileId').value || currentProfileId;
if (!profileId) {
showToast('请先输入Profile ID', 'error');
return;
}
try {
showToast('正在停止浏览器...', 'info');
const response = await fetch(`${API_BASE}/api/adspower/browser/stop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: profileId })
});
const data = await response.json();
if (data.success) {
formatResult(data.data, 'profileResult');
showToast('浏览器已停止', 'success');
} else {
showToast(data.message || '停止失败', 'error');
}
} catch (error) {
showToast('停止失败: ' + error.message, 'error');
}
}
// 获取大麦IP
async function getDamaiProxy() {
try {
showToast('正在获取大麦IP...', 'info');
const response = await fetch(`${API_BASE}/api/adspower/proxy/damai`);
const data = await response.json();
if (data.success) {
currentProxyInfo = data.data;
formatResult(data.data, 'proxyResult');
showToast(`获取成功: ${data.data.host}:${data.data.port}`, 'success');
} else {
showToast(data.message || '获取失败', 'error');
}
} catch (error) {
showToast('获取失败: ' + error.message, 'error');
}
}
// 创建代理
async function createProxy() {
if (!currentProxyInfo) {
showToast('请先获取大麦IP', 'error');
return;
}
try {
showToast('正在创建代理...', 'info');
const proxyConfig = {
type: 'http',
host: currentProxyInfo.host,
port: currentProxyInfo.port,
user: '69538fdef04e1',
password: '63v0kQBr2yJXnjf',
ipchecker: 'ip2location',
remark: 'Damai Auto Proxy'
};
const response = await fetch(`${API_BASE}/api/adspower/proxy/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_config: proxyConfig })
});
const data = await response.json();
if (data.success) {
document.getElementById('proxyId').value = data.data.proxy_id;
formatResult(data.data, 'proxyResult');
showToast(`代理创建成功ID: ${data.data.proxy_id}`, 'success');
} else {
showToast(data.message || '创建失败', 'error');
}
} catch (error) {
showToast('创建失败: ' + error.message, 'error');
}
}
// 查询代理列表
async function listProxies() {
try {
showToast('正在查询代理列表...', 'info');
const response = await fetch(`${API_BASE}/api/adspower/proxy/list?page=1&limit=50`);
// 检查响应类型
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
console.error('服务器返回非JSON响应:', contentType);
const text = await response.text();
console.error('响应内容:', text.substring(0, 200));
showToast('服务器响应错误,请检查后端服务', 'error');
return;
}
const data = await response.json();
if (data.success && data.data) {
const proxies = data.data?.data?.list || [];
if (proxies.length > 0) {
// 格式化显示代理信息
let html = '<div style="background: #f5f5f5; padding: 12px; border-radius: 4px; max-height: 400px; overflow-y: auto;">';
html += `<p style="margin-bottom: 12px; font-weight: 500;">找到 ${proxies.length} 个代理</p>`;
proxies.forEach((proxy, idx) => {
html += `
<div style="background: white; padding: 10px; margin-bottom: 8px; border-radius: 4px; border-left: 3px solid #1890ff;">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div style="font-weight: 500; color: #333;">#${idx + 1} ID: ${proxy.proxy_id}</div>
<div style="color: #666; font-size: 13px; margin-top: 4px;">
${proxy.type} - ${proxy.host}:${proxy.port}
</div>
${proxy.remark ? `<div style="color: #999; font-size: 12px; margin-top: 2px;">备注: ${proxy.remark}</div>` : ''}
<div style="color: #999; font-size: 12px; margin-top: 2px;">
关联环境数: ${proxy.profile_count || 0}
</div>
</div>
<button onclick="document.getElementById('proxyId').value='${proxy.proxy_id}'"
style="padding: 4px 8px; background: #1890ff; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 12px;">
选择
</button>
</div>
</div>
`;
});
html += '</div>';
document.getElementById('proxyResult').innerHTML = html;
showToast(`找到 ${proxies.length} 个代理`, 'success');
} else {
document.getElementById('proxyResult').innerHTML = '<p style="color: #999; text-align: center; padding: 24px;">暂无代理,请先创建代理</p>';
showToast('暂无代理', 'info');
}
} else {
showToast(data.message || '查询失败', 'error');
document.getElementById('proxyResult').innerHTML = `<p style="color: #ff4d4f; padding: 12px;">${data.message || '查询失败'}</p>`;
}
} catch (error) {
console.error('查询代理列表错误:', error);
showToast('查询失败: ' + error.message, 'error');
document.getElementById('proxyResult').innerHTML = `<p style="color: #ff4d4f; padding: 12px;">错误: ${error.message}</p>`;
}
}
// 更新Profile代理API v2方式
async function updateProfileProxy() {
const profileId = document.getElementById('profileId').value;
const proxyId = document.getElementById('proxyId').value;
if (!profileId || !proxyId) {
showToast('请输入Profile ID和代理ID', 'error');
return;
}
try {
showToast('正在更新Profile代理 (API v2)...', 'info');
const response = await fetch(`${API_BASE}/api/adspower/profile/update`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_id: profileId, proxy_id: proxyId })
});
const data = await response.json();
if (data.success) {
formatResult(data.data, 'updateResult');
showToast('更新成功 (API v2)', 'success');
} else {
showToast(data.message || '更新失败', 'error');
}
} catch (error) {
showToast('更新失败: ' + error.message, 'error');
}
}
// 更新Profile代理API v1方式
async function updateProfileProxyV1() {
const profileId = document.getElementById('profileId').value;
if (!profileId) {
showToast('请输入Profile ID', 'error');
return;
}
// 获取当前代理信息
if (!currentProxyInfo) {
showToast('请先获取大麦IP', 'error');
return;
}
try {
showToast('正在更新Profile代理 (API v1)...', 'info');
// 构建 proxy_config
const proxyConfig = {
proxy_type: 'http',
proxy_host: currentProxyInfo.host,
proxy_port: currentProxyInfo.port,
proxy_user: '69538fdef04e1',
proxy_password: '63v0kQBr2yJXnjf',
proxy_soft: 'other'
};
const response = await fetch(`${API_BASE}/api/adspower/profile/update-v1`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_id: profileId, proxy_config: proxyConfig })
});
const data = await response.json();
if (data.success) {
let resultHtml = '<div style="background: #f6ffed; padding: 12px; border-radius: 4px; border: 1px solid #b7eb8f;">';
resultHtml += '<div style="color: #52c41a; font-weight: 500; margin-bottom: 8px;">✅ 代理更新成功 (API v1)</div>';
resultHtml += '<div style="font-size: 12px; color: #666;">';
resultHtml += `<div>Profile ID: ${profileId}</div>`;
resultHtml += `<div>代理地址: ${proxyConfig.proxy_host}:${proxyConfig.proxy_port}</div>`;
resultHtml += `<div>认证用户: ${proxyConfig.proxy_user}</div>`;
resultHtml += '</div></div>';
document.getElementById('updateResult').innerHTML = resultHtml;
showToast('更新成功 (API v1)', 'success');
} else {
showToast(data.message || '更新失败', 'error');
document.getElementById('updateResult').innerHTML = `<p style="color: #ff4d4f; padding: 12px;">${data.message || '更新失败'}</p>`;
}
} catch (error) {
console.error('更新Profile代理错误 (v1):', error);
showToast('更新失败: ' + error.message, 'error');
document.getElementById('updateResult').innerHTML = `<p style="color: #ff4d4f; padding: 12px;">错误: ${error.message}</p>`;
}
}
// 完整测试流程
async function fullTest(useProxy) {
const resultDiv = document.getElementById('fullTestResult');
let log = [];
function addLog(msg, type = 'info') {
const time = new Date().toLocaleTimeString();
const color = type === 'error' ? '#ff4d4f' : type === 'success' ? '#52c41a' : '#666';
log.push(`<div style="color: ${color}; margin: 4px 0;">[${time}] ${msg}</div>`);
resultDiv.innerHTML = log.join('');
}
try {
addLog('开始完整测试流程...');
// 1. 查询Profile
addLog('步骤1: 查询Profile列表');
const profileRes = await fetch(`${API_BASE}/api/adspower/profiles`);
const profileData = await profileRes.json();
if (!profileData.success) {
addLog('查询Profile失败: ' + profileData.message, 'error');
return;
}
const profiles = profileData.data?.data?.list || [];
if (profiles.length === 0) {
addLog('未找到Profile', 'error');
return;
}
currentProfileId = profiles[0].profile_id;
document.getElementById('profileId').value = currentProfileId;
addLog(`找到Profile: ${currentProfileId}`, 'success');
if (useProxy) {
// 2. 获取大麦IP
addLog('步骤2: 获取大麦IP代理');
const proxyRes = await fetch(`${API_BASE}/api/adspower/proxy/damai`);
const proxyData = await proxyRes.json();
if (!proxyData.success) {
addLog('获取代理失败: ' + proxyData.message, 'error');
return;
}
currentProxyInfo = proxyData.data;
addLog(`获取代理: ${currentProxyInfo.host}:${currentProxyInfo.port}`, 'success');
// 3. 创建代理
addLog('步骤3: 创建AdsPower代理');
const proxyConfig = {
type: 'http',
host: currentProxyInfo.host,
port: currentProxyInfo.port,
user: '69538fdef04e1',
password: '63v0kQBr2yJXnjf',
ipchecker: 'ip2location',
remark: 'Damai Auto Proxy'
};
const createProxyRes = await fetch(`${API_BASE}/api/adspower/proxy/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_config: proxyConfig })
});
const createProxyData = await createProxyRes.json();
if (!createProxyData.success) {
addLog('创建代理失败: ' + createProxyData.message, 'error');
return;
}
const proxyId = createProxyData.data.proxy_id;
document.getElementById('proxyId').value = proxyId;
addLog(`创建代理成功ID: ${proxyId}`, 'success');
// 4. 更新Profile
addLog('步骤4: 更新Profile代理配置');
const updateRes = await fetch(`${API_BASE}/api/adspower/profile/update`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_id: currentProfileId, proxy_id: proxyId })
});
const updateData = await updateRes.json();
if (!updateData.success) {
addLog('更新Profile失败: ' + updateData.message, 'error');
return;
}
addLog('更新Profile成功', 'success');
}
// 5. 启动浏览器
addLog(`步骤${useProxy ? 5 : 2}: 启动浏览器`);
const startRes = await fetch(`${API_BASE}/api/adspower/browser/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: currentProfileId })
});
const startData = await startRes.json();
if (!startData.success) {
addLog('启动浏览器失败: ' + startData.message, 'error');
return;
}
addLog('浏览器启动成功', 'success');
addLog('测试流程完成!', 'success');
showToast('完整测试流程执行成功', 'success');
} catch (error) {
addLog('测试异常: ' + error.message, 'error');
showToast('测试失败: ' + error.message, 'error');
}
}

44
static/js/common.js Normal file
View File

@@ -0,0 +1,44 @@
const API_BASE = 'http://127.0.0.1:5000';
// 显示Toast提示
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 获取调度器状态
async function getSchedulerStatus() {
try {
const response = await fetch(`${API_BASE}/api/scheduler/status`);
const data = await response.json();
if (data.success) {
const isRunning = data.data.status === 'running';
const indicator = document.getElementById('statusIndicator');
const text = document.getElementById('statusText');
if (indicator && text) {
indicator.style.background = isRunning ? '#52c41a' : '#ff4d4f';
text.textContent = isRunning ? '调度器运行中' : '调度器已停止';
}
}
} catch (error) {
console.error('获取调度器状态失败:', error);
}
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
getSchedulerStatus();
// 定时刷新状态
setInterval(() => {
getSchedulerStatus();
}, 5000);
});

111
static/js/dashboard.js Normal file
View File

@@ -0,0 +1,111 @@
// 获取统计数据
async function getStatistics() {
try {
const response = await fetch(`${API_BASE}/api/statistics`);
const data = await response.json();
if (data.success) {
const stats = data.data;
document.getElementById('totalUrls').textContent = stats.total_urls;
document.getElementById('totalClicks').textContent = stats.total_clicks;
document.getElementById('totalReplies').textContent = stats.total_replies;
document.getElementById('replyRate').textContent = stats.reply_rate;
}
} catch (error) {
console.error('获取统计数据失败:', error);
}
}
// 加载URL列表
async function loadUrlList() {
try {
const response = await fetch(`${API_BASE}/api/urls`);
const data = await response.json();
if (data.success) {
const tbody = document.getElementById('urlTableBody');
if (data.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.data.map(item => `
<tr>
<td>
<a href="${item.url}" target="_blank" class="table-url" title="${item.url}">${item.url}</a>
</td>
<td>${item.click_count}</td>
<td>${item.reply_count}</td>
<td>${item.last_click_time || '未点击'}</td>
<td>
<button class="btn btn-warning" style="padding: 4px 12px; font-size: 12px; margin-right: 8px;" onclick="resetUrl('${encodeURIComponent(item.url)}')">重置</button>
<button class="btn btn-danger" style="padding: 4px 12px; font-size: 12px;" onclick="deleteUrl('${encodeURIComponent(item.url)}')">删除</button>
</td>
</tr>
`).join('');
}
} catch (error) {
console.error('加载URL列表失败:', error);
}
}
// 重置URL
async function resetUrl(encodedUrl) {
if (!confirm('确定要重置该链接吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls/${encodedUrl}/reset`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('重置成功', 'success');
loadUrlList();
getStatistics();
} else {
showToast(data.message || '重置失败', 'error');
}
} catch (error) {
showToast('重置失败: ' + error.message, 'error');
}
}
// 删除URL
async function deleteUrl(encodedUrl) {
if (!confirm('确定要删除该链接吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls/${encodedUrl}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
loadUrlList();
getStatistics();
} else {
showToast(data.message || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败: ' + error.message, 'error');
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
getStatistics();
loadUrlList();
// 定时刷新
setInterval(() => {
getStatistics();
loadUrlList();
}, 5000);
});

37
static/js/scheduler.js Normal file
View File

@@ -0,0 +1,37 @@
// 启动调度器
async function startScheduler() {
try {
const response = await fetch(`${API_BASE}/api/scheduler/start`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('调度器已启动', 'success');
getSchedulerStatus();
} else {
showToast(data.message || '启动失败', 'error');
}
} catch (error) {
showToast('启动失败: ' + error.message, 'error');
}
}
// 停止调度器
async function stopScheduler() {
try {
const response = await fetch(`${API_BASE}/api/scheduler/stop`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('调度器已停止', 'success');
getSchedulerStatus();
} else {
showToast(data.message || '停止失败', 'error');
}
} catch (error) {
showToast('停止失败: ' + error.message, 'error');
}
}

66
static/js/urls.js Normal file
View File

@@ -0,0 +1,66 @@
// 添加单个URL
async function addSingleUrl() {
const url = document.getElementById('singleUrl').value.trim();
if (!url) {
showToast('请输入链接', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
});
const data = await response.json();
if (data.success) {
showToast('添加成功', 'success');
document.getElementById('singleUrl').value = '';
} else {
showToast(data.message || '添加失败', 'error');
}
} catch (error) {
showToast('添加失败: ' + error.message, 'error');
}
}
// 批量添加URL
async function addBatchUrls() {
const text = document.getElementById('batchUrls').value.trim();
if (!text) {
showToast('请输入链接', 'error');
return;
}
const urls = text.split('\n').map(u => u.trim()).filter(u => u);
if (urls.length === 0) {
showToast('请输入有效链接', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/urls`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ urls })
});
const data = await response.json();
if (data.success) {
showToast(`成功添加 ${data.added_count}/${data.total_count} 个链接`, 'success');
document.getElementById('batchUrls').value = '';
} else {
showToast(data.message || '添加失败', 'error');
}
} catch (error) {
showToast('添加失败: ' + error.message, 'error');
}
}

72
static/scheduler.html Normal file
View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>调度器管理 - MIP广告点击管理系统</title>
<link rel="stylesheet" href="css/common.css">
</head>
<body>
<div class="layout">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">MIP管理系统</div>
<div class="menu">
<div class="menu-item" onclick="location.href='dashboard.html'">
<span class="menu-icon">📊</span>
<span>数据概览</span>
</div>
<div class="menu-item active" onclick="location.href='scheduler.html'">
<span class="menu-icon">⚙️</span>
<span>调度器管理</span>
</div>
<div class="menu-item" onclick="location.href='urls.html'">
<span class="menu-icon">🔗</span>
<span>链接管理</span>
</div>
<div class="menu-item" onclick="location.href='browser.html'">
<span class="menu-icon">🌐</span>
<span>浏览器测试</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main">
<!-- 顶部导航栏 -->
<div class="navbar">
<div class="breadcrumb">调度器管理</div>
<div class="user-info">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">系统运行中</span>
</div>
</div>
<!-- 内容区 -->
<div class="content">
<div class="card">
<div class="card-header">调度器控制</div>
<div class="card-body">
<div class="btn-group">
<button class="btn btn-success" onclick="startScheduler()">启动调度器</button>
<button class="btn btn-danger" onclick="stopScheduler()">停止调度器</button>
</div>
<div style="padding: 16px; background: #fafafa; border-radius: 2px; margin-top: 16px;">
<h4 style="margin-bottom: 12px; font-size: 14px;">调度规则说明</h4>
<ul style="padding-left: 20px; line-height: 2;">
<li>每30分钟点击一次添加的链接</li>
<li>仅在09:00-21:00时间段执行</li>
<li>每个链接随机点击1-10次</li>
<li>等待最多30秒查看回复</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script src="js/scheduler.js"></script>
</body>
</html>

75
static/urls.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>链接管理 - MIP广告点击管理系统</title>
<link rel="stylesheet" href="css/common.css">
</head>
<body>
<div class="layout">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">MIP管理系统</div>
<div class="menu">
<div class="menu-item" onclick="location.href='dashboard.html'">
<span class="menu-icon">📊</span>
<span>数据概览</span>
</div>
<div class="menu-item" onclick="location.href='scheduler.html'">
<span class="menu-icon">⚙️</span>
<span>调度器管理</span>
</div>
<div class="menu-item active" onclick="location.href='urls.html'">
<span class="menu-icon">🔗</span>
<span>链接管理</span>
</div>
<div class="menu-item" onclick="location.href='browser.html'">
<span class="menu-icon">🌐</span>
<span>浏览器测试</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main">
<!-- 顶部导航栏 -->
<div class="navbar">
<div class="breadcrumb">链接管理</div>
<div class="user-info">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">系统运行中</span>
</div>
</div>
<!-- 内容区 -->
<div class="content">
<div class="card">
<div class="card-header">添加单个链接</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">链接地址</label>
<input type="text" id="singleUrl" class="form-input" placeholder="请输入MIP页面链接" />
</div>
<button class="btn btn-primary" onclick="addSingleUrl()">添加链接</button>
</div>
</div>
<div class="card">
<div class="card-header">批量添加链接</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">批量链接(每行一个)</label>
<textarea id="batchUrls" class="form-textarea" placeholder="每行输入一个链接,支持批量添加"></textarea>
</div>
<button class="btn btn-primary" onclick="addBatchUrls()">批量添加</button>
</div>
</div>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script src="js/urls.js"></script>
</body>
</html>