'init'
This commit is contained in:
40
static/README.txt
Normal file
40
static/README.txt
Normal 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
112
static/browser.html
Normal 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">代理ID(API 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
366
static/css/common.css
Normal 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
98
static/dashboard.html
Normal 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
843
static/index.html
Normal 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
515
static/js/browser.js
Normal 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
44
static/js/common.js
Normal 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
111
static/js/dashboard.js
Normal 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
37
static/js/scheduler.js
Normal 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
66
static/js/urls.js
Normal 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
72
static/scheduler.html
Normal 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
75
static/urls.html
Normal 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>
|
||||
Reference in New Issue
Block a user