1051 lines
40 KiB
HTML
1051 lines
40 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>职位信息爬虫工具</title>
|
||
<!-- SheetJS库用于导出XLSX -->
|
||
<script src="/static/js/xlsx.full.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
padding: 30px;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.45);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 999;
|
||
}
|
||
|
||
.modal {
|
||
background: #fff;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
width: 360px;
|
||
max-width: 90%;
|
||
}
|
||
|
||
.modal h3 {
|
||
margin-bottom: 12px;
|
||
font-size: 18px;
|
||
color: #333;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
h1 {
|
||
color: #333;
|
||
margin-bottom: 30px;
|
||
text-align: center;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.form-section {
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: #f9f9f9;
|
||
border-radius: 6px;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.form-section h2 {
|
||
font-size: 18px;
|
||
color: #555;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid #4CAF50;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-group textarea {
|
||
min-height: 100px;
|
||
font-family: monospace;
|
||
resize: vertical;
|
||
}
|
||
|
||
.form-group small {
|
||
display: block;
|
||
margin-top: 5px;
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.btn:hover {
|
||
background: #45a049;
|
||
}
|
||
|
||
.btn:disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #2196F3;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #0b7dda;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f44336;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #da190b;
|
||
}
|
||
|
||
.checkbox-group {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
background: #fff;
|
||
}
|
||
|
||
.checkbox-item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.checkbox-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.checkbox-item label {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.checkbox-item input[type="checkbox"] {
|
||
width: auto;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.message {
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.message.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.message.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.message.info {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
border: 1px solid #bee5eb;
|
||
}
|
||
|
||
.table-container {
|
||
margin-top: 30px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: #fff;
|
||
}
|
||
|
||
table th,
|
||
table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
table th {
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
|
||
table tr:nth-child(even) {
|
||
background: #f9f9f9;
|
||
}
|
||
|
||
table tr:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #666;
|
||
}
|
||
|
||
.loading::after {
|
||
content: '...';
|
||
animation: dots 1.5s steps(4, end) infinite;
|
||
}
|
||
|
||
@keyframes dots {
|
||
0%, 20% { content: '.'; }
|
||
40% { content: '..'; }
|
||
60%, 100% { content: '...'; }
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.select-all {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.dsdm-checkbox-item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.dsdm-checkbox-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.dsdm-checkbox-item label {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
margin: 0;
|
||
}
|
||
|
||
.dsdm-checkbox-item input[type="checkbox"] {
|
||
width: auto;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
#dsdm-list {
|
||
background: #fff;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h1 style="margin: 0;">职位信息爬虫工具</h1>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button class="btn btn-secondary" style="background:#2196F3;" onclick="openPwdModal()">修改密码</button>
|
||
<a href="/auth/logout" style="color: #f44336; text-decoration: none; padding: 8px 15px; border-radius: 4px; background: #ffebee;">退出登录</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第一步:填写基础信息 -->
|
||
<div class="form-section">
|
||
<h2>第一步:填写基础信息</h2>
|
||
|
||
|
||
<div class="form-group">
|
||
<label for="cookie-jsessionid2">JSESSIONID1:</label>
|
||
<input type="text" id="cookie-jsessionid2" placeholder="">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="cookie-jsessionid">JSESSIONID2:</label>
|
||
<input type="text" id="cookie-jsessionid" placeholder="">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="cookie-serverid">SERVERID:</label>
|
||
<input type="text" id="cookie-serverid" placeholder="">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="examid">examid:</label>
|
||
<input type="text" id="examid" placeholder="">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="bmid">bmid:</label>
|
||
<input type="text" id="bmid" placeholder="">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="userid">userid:</label>
|
||
<input type="text" id="userid" placeholder="">
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<button class="btn btn-secondary" onclick="saveUserConfig()">保存配置</button>
|
||
<button class="btn" onclick="getDsdmOptions()">获取地区选项</button>
|
||
</div>
|
||
<div id="config-message"></div>
|
||
</div>
|
||
|
||
<!-- 第二步:选择地区并自动抓取 -->
|
||
<div class="form-section">
|
||
<h2>第二步:选择地区并自动抓取</h2>
|
||
<div id="dsdm-message"></div>
|
||
<div class="form-group">
|
||
<label>地区代码(dsdm):</label>
|
||
<div class="select-all">
|
||
<label style="display: inline-flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" id="select-all-dsdm" onchange="toggleAllDsdm()" style="width: auto; margin-right: 8px;">
|
||
<span>全选</span>
|
||
</label>
|
||
</div>
|
||
<div id="dsdm-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 10px;">
|
||
<div style="color: #999; text-align: center; padding: 20px;">请先获取地区选项</div>
|
||
</div>
|
||
</div>
|
||
<div class="action-buttons">
|
||
<button class="btn" id="fetch-all-btn" onclick="debouncedFetchAllPositions()">自动抓取全部职位</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 结果显示 -->
|
||
<div class="form-section">
|
||
<h2>职位信息结果</h2>
|
||
<div class="action-buttons" style="margin-bottom: 15px;">
|
||
<button class="btn btn-secondary" id="export-btn" onclick="exportXlsx()" disabled>导出XLSX</button>
|
||
</div>
|
||
<div id="result-message"></div>
|
||
<div class="table-container" id="result-table" style="display: none;">
|
||
<table id="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>省份</th>
|
||
<th>地区</th>
|
||
<th>招聘单位/用人司局</th>
|
||
<th>职位名称</th>
|
||
<th>职位代码</th>
|
||
<th>招聘人数</th>
|
||
<th>审核通过人数</th>
|
||
<th>竞争比</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="data-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// API基础路径配置
|
||
const API_BASE_URL = ''; // 空字符串表示使用相对路径,如需跨域可修改为完整URL,如:'http://your-domain.com'
|
||
|
||
// 复用同一个aa,确保selectPosition与getPositionTree的Referer一致
|
||
let lastAa = '';
|
||
let lastResults = [];
|
||
let isCrawling = false; // 爬取状态标志
|
||
|
||
// 防抖函数
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
|
||
// 防抖包装的抓取函数(300ms防抖)
|
||
const debouncedFetchAllPositions = debounce(() => {
|
||
fetchAllPositions();
|
||
}, 300);
|
||
|
||
// 页面加载时自动加载用户配置
|
||
window.onload = function() {
|
||
loadUserConfig();
|
||
};
|
||
|
||
// 加载用户配置
|
||
function loadUserConfig() {
|
||
fetch(API_BASE_URL + '/crawler/getUserConfig', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.code === 1 && data.data) {
|
||
// 填充表单
|
||
document.getElementById('cookie-jsessionid2').value = data.data.jsessionid1 || '';
|
||
document.getElementById('cookie-jsessionid').value = data.data.jsessionid2 || '';
|
||
document.getElementById('cookie-serverid').value = data.data.serverid || '';
|
||
document.getElementById('examid').value = data.data.examid || '';
|
||
document.getElementById('bmid').value = data.data.bmid || '';
|
||
document.getElementById('userid').value = data.data.userid || '';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('加载配置失败:', error);
|
||
});
|
||
}
|
||
|
||
// 保存用户配置
|
||
function saveUserConfig() {
|
||
const config = {
|
||
jsessionid1: document.getElementById('cookie-jsessionid2').value.trim(),
|
||
jsessionid2: document.getElementById('cookie-jsessionid').value.trim(),
|
||
serverid: document.getElementById('cookie-serverid').value.trim(),
|
||
examid: document.getElementById('examid').value.trim(),
|
||
bmid: document.getElementById('bmid').value.trim(),
|
||
userid: document.getElementById('userid').value.trim(),
|
||
};
|
||
|
||
fetch(API_BASE_URL + '/crawler/saveUserConfig', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
body: `jsessionid1=${encodeURIComponent(config.jsessionid1)}&jsessionid2=${encodeURIComponent(config.jsessionid2)}&serverid=${encodeURIComponent(config.serverid)}&examid=${encodeURIComponent(config.examid)}&bmid=${encodeURIComponent(config.bmid)}&userid=${encodeURIComponent(config.userid)}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.code === 1) {
|
||
showMessage('config-message', data.msg || '保存成功', 'success');
|
||
} else {
|
||
showMessage('config-message', data.msg || '保存失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('config-message', '请求失败: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
// 获取地区选项
|
||
function getDsdmOptions() {
|
||
const examid = document.getElementById('examid').value.trim();
|
||
const bmid = document.getElementById('bmid').value.trim();
|
||
const userid = document.getElementById('userid').value.trim();
|
||
const aa = Date.now().toString();
|
||
lastAa = aa;
|
||
const cookieData = buildCookiesPayload('dsdm-message');
|
||
|
||
if (!examid || !bmid || !userid) {
|
||
showMessage('dsdm-message', '请先填写examid、bmid和userid', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!cookieData) {
|
||
return;
|
||
}
|
||
|
||
showMessage('dsdm-message', '正在获取地区选项...', 'info');
|
||
|
||
fetch(API_BASE_URL + '/crawler/getDsdmOptions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `examid=${encodeURIComponent(examid)}&bmid=${encodeURIComponent(bmid)}&userid=${encodeURIComponent(userid)}&aa=${aa}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.code === 1 && Array.isArray(data.data)) {
|
||
const dsdmList = document.getElementById('dsdm-list');
|
||
dsdmList.innerHTML = '';
|
||
data.data.forEach(option => {
|
||
const div = document.createElement('div');
|
||
div.className = 'dsdm-checkbox-item';
|
||
div.innerHTML = `
|
||
<label>
|
||
<input type="checkbox" class="dsdm-checkbox" value="${option.value}" onchange="updateSelectAllDsdm()">
|
||
<span>${option.label || option.text} (${option.value})</span>
|
||
</label>
|
||
`;
|
||
dsdmList.appendChild(div);
|
||
});
|
||
showMessage('dsdm-message', '获取地区选项成功,请选择地区', 'success');
|
||
} else {
|
||
showMessage('dsdm-message', data.msg || '获取失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('dsdm-message', '请求失败: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
// 全选/取消全选地区
|
||
function toggleAllDsdm() {
|
||
const selectAll = document.getElementById('select-all-dsdm').checked;
|
||
const checkboxes = document.querySelectorAll('.dsdm-checkbox');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = selectAll;
|
||
});
|
||
}
|
||
|
||
// 更新全选复选框状态
|
||
function updateSelectAllDsdm() {
|
||
const checkboxes = document.querySelectorAll('.dsdm-checkbox');
|
||
const checkedCount = document.querySelectorAll('.dsdm-checkbox:checked').length;
|
||
const selectAllCheckbox = document.getElementById('select-all-dsdm');
|
||
selectAllCheckbox.checked = checkboxes.length > 0 && checkedCount === checkboxes.length;
|
||
}
|
||
|
||
// 获取选中的地区代码数组
|
||
function getSelectedDsdm() {
|
||
const checkboxes = document.querySelectorAll('.dsdm-checkbox:checked');
|
||
return Array.from(checkboxes).map(cb => cb.value);
|
||
}
|
||
|
||
// 获取职位代码列表
|
||
function getZwdmList() {
|
||
const dsdm = document.getElementById('dsdm').value;
|
||
const examid = document.getElementById('examid').value.trim();
|
||
const bmid = document.getElementById('bmid').value.trim();
|
||
const userid = document.getElementById('userid').value.trim();
|
||
const cookieData = buildCookiesPayload('zwdm-message');
|
||
const aa = lastAa || Date.now().toString();
|
||
|
||
if (!dsdm) {
|
||
showMessage('zwdm-message', '请先选择地区', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!examid || !bmid || !userid) {
|
||
showMessage('zwdm-message', '请先填写examid、bmid和userid', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!cookieData) {
|
||
return;
|
||
}
|
||
|
||
showMessage('zwdm-message', '正在获取职位代码列表...', 'info');
|
||
|
||
fetch(API_BASE_URL + '/crawler/getZwdmList', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `dsdm=${encodeURIComponent(dsdm)}&examid=${encodeURIComponent(examid)}&bmid=${encodeURIComponent(bmid)}&userid=${encodeURIComponent(userid)}&aa=${encodeURIComponent(aa)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.code === 1) {
|
||
const container = document.getElementById('zwdm-list');
|
||
container.innerHTML = '';
|
||
|
||
if (data.data.length === 0) {
|
||
container.innerHTML = '<div class="loading">未找到职位代码</div>';
|
||
showMessage('zwdm-message', '未找到职位代码', 'info');
|
||
return;
|
||
}
|
||
|
||
data.data.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.className = 'checkbox-item';
|
||
div.innerHTML = `
|
||
<label>
|
||
<input type="checkbox" value="${item.zwdm}" class="zwdm-checkbox">
|
||
${item.title}
|
||
</label>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
|
||
showMessage('zwdm-message', `获取成功,共找到 ${data.data.length} 个职位代码`, 'success');
|
||
} else {
|
||
showMessage('zwdm-message', data.msg || '获取失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('zwdm-message', '请求失败: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
// 组装Cookie数据
|
||
function buildCookiesPayload(messageContainerId) {
|
||
const jsessionid = document.getElementById('cookie-jsessionid').value.trim();
|
||
const jsessionid2 = document.getElementById('cookie-jsessionid2').value.trim();
|
||
const serverid = document.getElementById('cookie-serverid').value.trim();
|
||
|
||
if (!jsessionid || !serverid) {
|
||
showMessage(messageContainerId, '请填写JSESSIONID和SERVERID', 'error');
|
||
return null;
|
||
}
|
||
|
||
const cookies = { "请求 Cookie": {} };
|
||
// 支持双 JSESSIONID
|
||
cookies["请求 Cookie"]["JSESSIONID"] = jsessionid2 ? [jsessionid, jsessionid2] : jsessionid;
|
||
cookies["请求 Cookie"]["SERVERID"] = serverid;
|
||
return cookies;
|
||
}
|
||
|
||
// 全选/取消全选
|
||
function toggleAllZwdm() {
|
||
const selectAll = document.getElementById('select-all-zwdm').checked;
|
||
const checkboxes = document.querySelectorAll('.zwdm-checkbox');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = selectAll;
|
||
});
|
||
}
|
||
|
||
// 批量获取职位信息
|
||
function batchGetPositionInfo() {
|
||
const examid = document.getElementById('examid').value.trim();
|
||
const cookieData = buildCookiesPayload('result-message');
|
||
|
||
if (!examid) {
|
||
showMessage('result-message', '请先填写examid', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!cookieData) {
|
||
return;
|
||
}
|
||
|
||
const checkboxes = document.querySelectorAll('.zwdm-checkbox:checked');
|
||
if (checkboxes.length === 0) {
|
||
showMessage('result-message', '请至少选择一个职位代码', 'error');
|
||
return;
|
||
}
|
||
|
||
const zwdmList = Array.from(checkboxes).map(cb => cb.value);
|
||
|
||
showMessage('result-message', `正在获取 ${zwdmList.length} 个职位的信息,请稍候...`, 'info');
|
||
|
||
fetch(API_BASE_URL + '/crawler/batchGetPositionInfo', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `examid=${encodeURIComponent(examid)}&zwdm_list=${encodeURIComponent(JSON.stringify(zwdmList))}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.code === 1) {
|
||
lastResults = data.data;
|
||
displayResults(data.data);
|
||
showMessage('result-message', `成功获取 ${data.data.length} 条职位信息`, 'success');
|
||
} else {
|
||
showMessage('result-message', data.msg || '获取失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('result-message', '请求失败: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
// 显示结果表格
|
||
function displayResults(results) {
|
||
const tbody = document.getElementById('data-table-body');
|
||
tbody.innerHTML = '';
|
||
|
||
results.forEach(item => {
|
||
if (item.error) {
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `
|
||
<td colspan="8" style="color: red;">职位代码 ${item.zwdm}: ${item.error}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
} else {
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `
|
||
<td>${item.sbmc || ''}</td>
|
||
<td>${item.dsmc || ''}</td>
|
||
<td>${item.zpdwmc || ''}</td>
|
||
<td>${item.zwmc || ''}</td>
|
||
<td>${item.zwdm || ''}</td>
|
||
<td>${item.zprs || 0}</td>
|
||
<td>${item.bkrs || 0}</td>
|
||
<td>${item.competition_ratio || '0:0'}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
}
|
||
});
|
||
|
||
document.getElementById('result-table').style.display = 'block';
|
||
}
|
||
|
||
// 追加成功行
|
||
function appendResultRow(item) {
|
||
const tbody = document.getElementById('data-table-body');
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `
|
||
<td>${item.sbmc || ''}</td>
|
||
<td>${item.dsmc || ''}</td>
|
||
<td>${item.zpdwmc || ''}</td>
|
||
<td>${item.zwmc || ''}</td>
|
||
<td>${item.zwdm || ''}</td>
|
||
<td>${item.zprs || 0}</td>
|
||
<td>${item.bkrs || 0}</td>
|
||
<td>${item.competition_ratio || '0:0'}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
document.getElementById('result-table').style.display = 'block';
|
||
}
|
||
|
||
// 追加失败行
|
||
function appendErrorRow(code, msg) {
|
||
const tbody = document.getElementById('data-table-body');
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `<td colspan="8" style="color:red;">职位代码 ${code}: ${msg}</td>`;
|
||
tbody.appendChild(tr);
|
||
document.getElementById('result-table').style.display = 'block';
|
||
}
|
||
|
||
// 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时)
|
||
async function fetchAllPositions() {
|
||
// 如果正在爬取,直接返回
|
||
if (isCrawling) {
|
||
showMessage('result-message', '正在爬取中,请勿重复点击', 'error');
|
||
return;
|
||
}
|
||
|
||
const selectedDsdm = getSelectedDsdm();
|
||
const examid = document.getElementById('examid').value.trim();
|
||
const bmid = document.getElementById('bmid').value.trim();
|
||
const userid = document.getElementById('userid').value.trim();
|
||
const cookieData = buildCookiesPayload('result-message');
|
||
const aa = lastAa;
|
||
|
||
if (!selectedDsdm || selectedDsdm.length === 0) {
|
||
showMessage('dsdm-message', '请先选择至少一个地区', 'error');
|
||
return;
|
||
}
|
||
if (!examid || !bmid || !userid) {
|
||
showMessage('dsdm-message', '请先填写examid、bmid和userid', 'error');
|
||
return;
|
||
}
|
||
if (!aa) {
|
||
showMessage('result-message', '请先点击"获取地区选项"生成aa', 'error');
|
||
return;
|
||
}
|
||
if (!cookieData) {
|
||
return;
|
||
}
|
||
|
||
// 开始爬取,禁用所有相关按钮
|
||
isCrawling = true;
|
||
const fetchBtnStart = document.getElementById('fetch-all-btn');
|
||
const exportBtnStart = document.getElementById('export-btn');
|
||
|
||
if (fetchBtnStart) {
|
||
fetchBtnStart.disabled = true;
|
||
fetchBtnStart.textContent = '爬取中...';
|
||
}
|
||
if (exportBtnStart) {
|
||
exportBtnStart.disabled = true;
|
||
exportBtnStart.textContent = '爬取中...';
|
||
}
|
||
|
||
// 清空旧数据
|
||
lastResults = [];
|
||
const tbody = document.getElementById('data-table-body');
|
||
tbody.innerHTML = '';
|
||
document.getElementById('result-table').style.display = 'block';
|
||
|
||
let totalCodes = 0;
|
||
let processedCodes = 0;
|
||
|
||
// 遍历所有选中的地区
|
||
for (let dsdmIndex = 0; dsdmIndex < selectedDsdm.length; dsdmIndex++) {
|
||
const dsdm = selectedDsdm[dsdmIndex];
|
||
showMessage('result-message', `正在处理地区 ${dsdm} (${dsdmIndex + 1}/${selectedDsdm.length})...`, 'info');
|
||
|
||
// 获取该地区的全部职位代码
|
||
const listResp = await fetch(API_BASE_URL + '/crawler/getZwdmList', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: `dsdm=${encodeURIComponent(dsdm)}&examid=${encodeURIComponent(examid)}&bmid=${encodeURIComponent(bmid)}&userid=${encodeURIComponent(userid)}&aa=${encodeURIComponent(aa)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}`
|
||
}).then(r => r.json()).catch(e => ({ code: 0, msg: e.message }));
|
||
|
||
if (listResp.code !== 1 || !Array.isArray(listResp.data) || listResp.data.length === 0) {
|
||
showMessage('result-message', `地区 ${dsdm} 获取职位代码失败: ${listResp.msg || '未获取到职位代码'}`, 'error');
|
||
continue;
|
||
}
|
||
|
||
// 过滤掉152开头的职位代码
|
||
const codes = listResp.data
|
||
.map(it => it.zwdm)
|
||
.filter(code => !code.startsWith('152'));
|
||
|
||
totalCodes += codes.length;
|
||
showMessage('result-message', `地区 ${dsdm}: 共 ${codes.length} 个职位(已跳过152开头),开始逐条获取...`, 'info');
|
||
|
||
// 逐条获取职位详情
|
||
for (let i = 0; i < codes.length; i++) {
|
||
const code = codes[i];
|
||
const infoResp = await fetch(API_BASE_URL + '/crawler/getPositionInfo', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: `zwdm=${encodeURIComponent(code)}&examid=${encodeURIComponent(examid)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}`
|
||
}).then(r => r.json()).catch(e => ({ code: 0, msg: e.message }));
|
||
|
||
if (infoResp.code === 1 && infoResp.data) {
|
||
const item = infoResp.data;
|
||
lastResults.push(item);
|
||
appendResultRow(item);
|
||
} else {
|
||
appendErrorRow(code, infoResp.msg || '获取失败');
|
||
}
|
||
|
||
processedCodes++;
|
||
showMessage('result-message', `总进度:${processedCodes}/${totalCodes} (地区 ${dsdmIndex + 1}/${selectedDsdm.length})`, 'info');
|
||
}
|
||
}
|
||
|
||
// 爬取完成,恢复按钮状态
|
||
isCrawling = false;
|
||
const fetchBtnEnd = document.getElementById('fetch-all-btn');
|
||
const exportBtnEnd = document.getElementById('export-btn');
|
||
|
||
if (fetchBtnEnd) {
|
||
fetchBtnEnd.disabled = false;
|
||
fetchBtnEnd.textContent = '自动抓取全部职位';
|
||
}
|
||
if (exportBtnEnd) {
|
||
exportBtnEnd.disabled = false;
|
||
exportBtnEnd.textContent = '导出XLSX';
|
||
}
|
||
showMessage('result-message', `爬取完成!共处理 ${processedCodes} 个职位`, 'success');
|
||
}
|
||
|
||
// 导出XLSX
|
||
function exportXlsx() {
|
||
if (isCrawling) {
|
||
showMessage('result-message', '爬取进行中,请等待完成后再导出', 'error');
|
||
return;
|
||
}
|
||
if (!lastResults || lastResults.length === 0) {
|
||
showMessage('result-message', '暂无数据可导出', 'error');
|
||
return;
|
||
}
|
||
|
||
// 检查SheetJS库是否加载
|
||
if (typeof XLSX === 'undefined') {
|
||
showMessage('result-message', '导出库加载失败,请刷新页面重试', 'error');
|
||
return;
|
||
}
|
||
|
||
// 准备数据
|
||
const headers = ['省份','地区','招聘单位/用人司局','职位名称','职位代码','招聘人数','审核通过人数','竞争比'];
|
||
const data = [headers];
|
||
|
||
lastResults.forEach(item => {
|
||
const row = [
|
||
item.sbmc || '',
|
||
item.dsmc || '',
|
||
item.zpdwmc || '',
|
||
item.zwmc || '',
|
||
item.zwdm || '',
|
||
item.zprs || 0,
|
||
item.bkrs || 0,
|
||
item.competition_ratio || '0:0' // 使用与网页显示一致的竞争比格式
|
||
];
|
||
data.push(row);
|
||
});
|
||
|
||
// 创建工作簿
|
||
const wb = XLSX.utils.book_new();
|
||
const ws = XLSX.utils.aoa_to_sheet(data);
|
||
|
||
// 设置列宽
|
||
const colWidths = [
|
||
{ wch: 10 }, // 省份
|
||
{ wch: 15 }, // 地区
|
||
{ wch: 25 }, // 招聘单位/用人司局
|
||
{ wch: 25 }, // 职位名称
|
||
{ wch: 15 }, // 职位代码
|
||
{ wch: 12 }, // 招聘人数
|
||
{ wch: 15 }, // 审核通过人数
|
||
{ wch: 12 } // 竞争比
|
||
];
|
||
ws['!cols'] = colWidths;
|
||
|
||
// 设置表头样式(如果需要)
|
||
const headerRange = XLSX.utils.decode_range(ws['!ref']);
|
||
for (let col = headerRange.s.c; col <= headerRange.e.c; col++) {
|
||
const cellAddress = XLSX.utils.encode_cell({ r: 0, c: col });
|
||
if (!ws[cellAddress]) continue;
|
||
ws[cellAddress].s = {
|
||
font: { bold: true },
|
||
fill: { fgColor: { rgb: "E0E0E0" } },
|
||
alignment: { horizontal: "center", vertical: "center" }
|
||
};
|
||
}
|
||
|
||
// 竞争比列设置为文本格式,防止被当作数字或日期
|
||
const competitionColIndex = 7; // 第8列
|
||
for (let r = 1; r < data.length; r++) { // 从第2行开始,跳过表头
|
||
const addr = XLSX.utils.encode_cell({ r, c: competitionColIndex });
|
||
if (ws[addr]) {
|
||
ws[addr].t = 's';
|
||
ws[addr].z = '@';
|
||
}
|
||
}
|
||
|
||
// 将工作表添加到工作簿
|
||
XLSX.utils.book_append_sheet(wb, ws, '职位信息');
|
||
|
||
// 导出文件
|
||
const fileName = 'positions_' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '.xlsx';
|
||
XLSX.writeFile(wb, fileName);
|
||
|
||
showMessage('result-message', '已导出XLSX', 'success');
|
||
}
|
||
|
||
// 显示消息
|
||
function showMessage(containerId, message, type) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||
}
|
||
|
||
// 弹窗:打开、关闭
|
||
function openPwdModal() {
|
||
document.getElementById('pwd-modal').style.display = 'flex';
|
||
}
|
||
function closePwdModal() {
|
||
document.getElementById('pwd-modal').style.display = 'none';
|
||
document.getElementById('modal-old-password').value = '';
|
||
document.getElementById('modal-new-password').value = '';
|
||
document.getElementById('modal-new-password-confirm').value = '';
|
||
document.getElementById('modal-pwd-message').innerHTML = '';
|
||
}
|
||
|
||
// 弹窗内修改密码
|
||
function changePasswordModal() {
|
||
const oldPassword = document.getElementById('modal-old-password').value.trim();
|
||
const newPassword = document.getElementById('modal-new-password').value.trim();
|
||
const newPasswordConfirm = document.getElementById('modal-new-password-confirm').value.trim();
|
||
|
||
if (!oldPassword || !newPassword || !newPasswordConfirm) {
|
||
showMessage('modal-pwd-message', '请填写完整的旧密码和新密码', 'error');
|
||
return;
|
||
}
|
||
if (newPassword !== newPasswordConfirm) {
|
||
showMessage('modal-pwd-message', '两次输入的新密码不一致', 'error');
|
||
return;
|
||
}
|
||
|
||
showMessage('modal-pwd-message', '正在修改密码...', 'info');
|
||
fetch(API_BASE_URL + '/crawler/changePassword', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
body: `old_password=${encodeURIComponent(oldPassword)}&new_password=${encodeURIComponent(newPassword)}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.code === 1) {
|
||
showMessage('modal-pwd-message', data.msg || '密码修改成功', 'success');
|
||
setTimeout(() => {
|
||
closePwdModal();
|
||
}, 600);
|
||
} else {
|
||
showMessage('modal-pwd-message', data.msg || '密码修改失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('modal-pwd-message', '请求失败: ' + error.message, 'error');
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<!-- 密码修改弹窗 -->
|
||
<div class="modal-overlay" id="pwd-modal">
|
||
<div class="modal">
|
||
<h3>修改密码</h3>
|
||
<div id="modal-pwd-message"></div>
|
||
<div class="form-group">
|
||
<label for="modal-old-password">旧密码:</label>
|
||
<input type="password" id="modal-old-password" placeholder="请输入旧密码">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="modal-new-password">新密码:</label>
|
||
<input type="password" id="modal-new-password" placeholder="请输入新密码">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="modal-new-password-confirm">确认新密码:</label>
|
||
<input type="password" id="modal-new-password-confirm" placeholder="请再次输入新密码">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" style="background:#9e9e9e;" onclick="closePwdModal()">取消</button>
|
||
<button class="btn" onclick="changePasswordModal()">确定修改</button>
|
||
</div>
|
||
</div>
|
||
</div> |