修复竞争比和按钮问题
This commit is contained in:
11
.example.env
11
.example.env
@@ -1,11 +0,0 @@
|
|||||||
APP_DEBUG = true
|
|
||||||
|
|
||||||
DB_TYPE = mysql
|
|
||||||
DB_HOST = 127.0.0.1
|
|
||||||
DB_NAME = test
|
|
||||||
DB_USER = username
|
|
||||||
DB_PASS = password
|
|
||||||
DB_PORT = 3306
|
|
||||||
DB_CHARSET = utf8
|
|
||||||
|
|
||||||
DEFAULT_LANG = zh-cn
|
|
||||||
@@ -292,7 +292,7 @@ class CrawlerService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算竞争比(格式:招聘人数:审核通过人数,简化比例,保留2位小数)
|
* 计算竞争比(格式:1:比例,保留2位小数)
|
||||||
* @param int $zprs 招聘人数
|
* @param int $zprs 招聘人数
|
||||||
* @param int $bkrs 审核通过人数
|
* @param int $bkrs 审核通过人数
|
||||||
* @return string
|
* @return string
|
||||||
@@ -304,33 +304,13 @@ class CrawlerService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($bkrs <= 0) {
|
if ($bkrs <= 0) {
|
||||||
return $zprs . ':0';
|
return '1:0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算最大公约数
|
// 将招聘人数简化为1,计算审核通过人数与招聘人数的比例
|
||||||
$gcd = $this->gcd($zprs, $bkrs);
|
$ratio = $bkrs / $zprs;
|
||||||
|
|
||||||
// 简化比例
|
// 保留2位小数
|
||||||
$simplifiedZprs = $zprs / $gcd;
|
|
||||||
$simplifiedBkrs = $bkrs / $gcd;
|
|
||||||
|
|
||||||
// 如果两个数都是整数,直接返回
|
|
||||||
if ($simplifiedZprs == intval($simplifiedZprs) && $simplifiedBkrs == intval($simplifiedBkrs)) {
|
|
||||||
return intval($simplifiedZprs) . ':' . intval($simplifiedBkrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果招聘人数是1,审核通过人数保留2位小数
|
|
||||||
if ($simplifiedZprs == 1) {
|
|
||||||
$bkrsFormatted = number_format($simplifiedBkrs, 2, '.', '');
|
|
||||||
// 如果小数部分是.00,则显示为整数
|
|
||||||
if (floatval($bkrsFormatted) == intval($bkrsFormatted)) {
|
|
||||||
return '1:' . intval($bkrsFormatted);
|
|
||||||
}
|
|
||||||
return '1:' . $bkrsFormatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则,将招聘人数简化为1,审核通过人数按比例计算并保留2位小数
|
|
||||||
$ratio = $simplifiedBkrs / $simplifiedZprs;
|
|
||||||
$ratioFormatted = number_format($ratio, 2, '.', '');
|
$ratioFormatted = number_format($ratio, 2, '.', '');
|
||||||
|
|
||||||
// 如果小数部分是.00,则显示为整数
|
// 如果小数部分是.00,则显示为整数
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
"php": ">=8.0.0",
|
"php": ">=8.0.0",
|
||||||
"topthink/framework": "^8.0",
|
"topthink/framework": "^8.0",
|
||||||
"topthink/think-orm": "^3.0|^4.0",
|
"topthink/think-orm": "^3.0|^4.0",
|
||||||
"topthink/think-filesystem": "^2.0|^3.0"
|
"topthink/think-filesystem": "^2.0|^3.0",
|
||||||
|
"topthink/think-view": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"topthink/think-dumper": "^1.0",
|
"topthink/think-dumper": "^1.0",
|
||||||
|
|||||||
4
omposer
Normal file
4
omposer
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>职位信息爬虫工具</title>
|
<title>职位信息爬虫工具</title>
|
||||||
|
<!-- SheetJS库用于导出XLSX -->
|
||||||
|
<script src="/static/js/xlsx.full.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -331,7 +333,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn" onclick="fetchAllPositions()">自动抓取全部职位</button>
|
<button class="btn" id="fetch-all-btn" onclick="debouncedFetchAllPositions()">自动抓取全部职位</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -339,7 +341,7 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2>职位信息结果</h2>
|
<h2>职位信息结果</h2>
|
||||||
<div class="action-buttons" style="margin-bottom: 15px;">
|
<div class="action-buttons" style="margin-bottom: 15px;">
|
||||||
<button class="btn btn-secondary" id="export-btn" onclick="exportCsv()" disabled>导出CSV</button>
|
<button class="btn btn-secondary" id="export-btn" onclick="exportXlsx()" disabled>导出XLSX</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="result-message"></div>
|
<div id="result-message"></div>
|
||||||
<div class="table-container" id="result-table" style="display: none;">
|
<div class="table-container" id="result-table" style="display: none;">
|
||||||
@@ -372,6 +374,24 @@
|
|||||||
let lastResults = [];
|
let lastResults = [];
|
||||||
let isCrawling = false; // 爬取状态标志
|
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() {
|
window.onload = function() {
|
||||||
loadUserConfig();
|
loadUserConfig();
|
||||||
@@ -710,6 +730,12 @@
|
|||||||
|
|
||||||
// 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时)
|
// 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时)
|
||||||
async function fetchAllPositions() {
|
async function fetchAllPositions() {
|
||||||
|
// 如果正在爬取,直接返回
|
||||||
|
if (isCrawling) {
|
||||||
|
showMessage('result-message', '正在爬取中,请勿重复点击', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedDsdm = getSelectedDsdm();
|
const selectedDsdm = getSelectedDsdm();
|
||||||
const examid = document.getElementById('examid').value.trim();
|
const examid = document.getElementById('examid').value.trim();
|
||||||
const bmid = document.getElementById('bmid').value.trim();
|
const bmid = document.getElementById('bmid').value.trim();
|
||||||
@@ -733,10 +759,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始爬取,禁用导出按钮
|
// 开始爬取,禁用所有相关按钮
|
||||||
isCrawling = true;
|
isCrawling = true;
|
||||||
document.getElementById('export-btn').disabled = true;
|
const fetchBtnStart = document.getElementById('fetch-all-btn');
|
||||||
document.getElementById('export-btn').textContent = '爬取中...';
|
const exportBtnStart = document.getElementById('export-btn');
|
||||||
|
|
||||||
|
if (fetchBtnStart) {
|
||||||
|
fetchBtnStart.disabled = true;
|
||||||
|
fetchBtnStart.textContent = '爬取中...';
|
||||||
|
}
|
||||||
|
if (exportBtnStart) {
|
||||||
|
exportBtnStart.disabled = true;
|
||||||
|
exportBtnStart.textContent = '爬取中...';
|
||||||
|
}
|
||||||
|
|
||||||
// 清空旧数据
|
// 清空旧数据
|
||||||
lastResults = [];
|
lastResults = [];
|
||||||
@@ -796,20 +831,22 @@
|
|||||||
|
|
||||||
// 爬取完成,恢复按钮状态
|
// 爬取完成,恢复按钮状态
|
||||||
isCrawling = false;
|
isCrawling = false;
|
||||||
document.getElementById('export-btn').disabled = false;
|
const fetchBtnEnd = document.getElementById('fetch-all-btn');
|
||||||
document.getElementById('export-btn').textContent = '导出CSV';
|
const exportBtnEnd = document.getElementById('export-btn');
|
||||||
showMessage('result-message', `爬取完成!共处理 ${processedCodes} 个职位`, 'success');
|
|
||||||
|
|
||||||
showMessage('result-message', `完成,共 ${lastResults.length} 条成功,失败 ${codes.length - lastResults.length} 条`, 'success');
|
|
||||||
|
|
||||||
// 爬取完成,启用导出按钮
|
if (fetchBtnEnd) {
|
||||||
isCrawling = false;
|
fetchBtnEnd.disabled = false;
|
||||||
document.getElementById('export-btn').disabled = false;
|
fetchBtnEnd.textContent = '自动抓取全部职位';
|
||||||
document.getElementById('export-btn').textContent = '导出CSV';
|
}
|
||||||
|
if (exportBtnEnd) {
|
||||||
|
exportBtnEnd.disabled = false;
|
||||||
|
exportBtnEnd.textContent = '导出XLSX';
|
||||||
|
}
|
||||||
|
showMessage('result-message', `爬取完成!共处理 ${processedCodes} 个职位`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出CSV
|
// 导出XLSX
|
||||||
function exportCsv() {
|
function exportXlsx() {
|
||||||
if (isCrawling) {
|
if (isCrawling) {
|
||||||
showMessage('result-message', '爬取进行中,请等待完成后再导出', 'error');
|
showMessage('result-message', '爬取进行中,请等待完成后再导出', 'error');
|
||||||
return;
|
return;
|
||||||
@@ -818,8 +855,17 @@
|
|||||||
showMessage('result-message', '暂无数据可导出', 'error');
|
showMessage('result-message', '暂无数据可导出', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查SheetJS库是否加载
|
||||||
|
if (typeof XLSX === 'undefined') {
|
||||||
|
showMessage('result-message', '导出库加载失败,请刷新页面重试', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
const headers = ['省份','地区','招聘单位/用人司局','职位名称','职位代码','招聘人数','审核通过人数','竞争比'];
|
const headers = ['省份','地区','招聘单位/用人司局','职位名称','职位代码','招聘人数','审核通过人数','竞争比'];
|
||||||
const lines = [headers.join(',')];
|
const data = [headers];
|
||||||
|
|
||||||
lastResults.forEach(item => {
|
lastResults.forEach(item => {
|
||||||
const row = [
|
const row = [
|
||||||
item.sbmc || '',
|
item.sbmc || '',
|
||||||
@@ -829,18 +875,58 @@
|
|||||||
item.zwdm || '',
|
item.zwdm || '',
|
||||||
item.zprs || 0,
|
item.zprs || 0,
|
||||||
item.bkrs || 0,
|
item.bkrs || 0,
|
||||||
item.competition_ratio || '0:0'
|
item.competition_ratio || '0:0' // 使用与网页显示一致的竞争比格式
|
||||||
];
|
];
|
||||||
lines.push(row.map(v => `"${String(v).replace(/"/g, '""')}"`).join(','));
|
data.push(row);
|
||||||
});
|
});
|
||||||
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
// 创建工作簿
|
||||||
const link = document.createElement('a');
|
const wb = XLSX.utils.book_new();
|
||||||
link.href = url;
|
const ws = XLSX.utils.aoa_to_sheet(data);
|
||||||
link.download = 'positions.csv';
|
|
||||||
link.click();
|
// 设置列宽
|
||||||
URL.revokeObjectURL(url);
|
const colWidths = [
|
||||||
showMessage('result-message', '已导出CSV', 'success');
|
{ 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示消息
|
// 显示消息
|
||||||
|
|||||||
Reference in New Issue
Block a user